Bug 24165: Add ability to send any item field in a library chosen SIP field
[koha.git] / C4 / SIP / ILS / Item.pm
1 #
2 # ILS::Item.pm
3
4 # A Class for hiding the ILS's concept of the item from OpenSIP
5 #
6
7 package C4::SIP::ILS::Item;
8
9 use strict;
10 use warnings;
11
12 use C4::SIP::Sip qw(siplog);
13 use Carp;
14 use Template;
15
16 use C4::SIP::ILS::Transaction;
17 use C4::SIP::Sip qw(add_field);
18
19 use C4::Debug;
20 use C4::Context;
21 use C4::Biblio;
22 use C4::Items;
23 use C4::Circulation;
24 use C4::Members;
25 use C4::Reserves;
26 use Koha::Database;
27 use Koha::Biblios;
28 use Koha::Checkouts;
29 use Koha::DateUtils;
30 use Koha::Patrons;
31 use Koha::Items;
32 use Koha::Holds;
33
34 =encoding UTF-8
35
36 =head1 EXAMPLE
37
38  our %item_db = (
39     '1565921879' => {
40         title => "Perl 5 desktop reference",
41         id => '1565921879',
42         sip_media_type => '001',
43         magnetic_media => 0,
44         hold_queue => [],
45     },
46     '0440242746' => {
47         title => "The deep blue alibi",
48         id => '0440242746',
49         sip_media_type => '001',
50         magnetic_media => 0,
51         hold_queue => [
52             {
53             itemnumber => '823',
54             priority => '1',
55             reservenotes => undef,
56             reservedate => '2008-10-09',
57             found => undef,
58             rtimestamp => '2008-10-09 11:15:06',
59             biblionumber => '406',
60             borrowernumber => '756',
61             branchcode => 'CPL'
62             }
63         ],
64     },
65     '660' => {
66         title => "Harry Potter y el cáliz de fuego",
67         id => '660',
68         sip_media_type => '001',
69         magnetic_media => 0,
70         hold_queue => [],
71     },
72 );
73
74 =cut
75
76 sub new {
77     my ($class, $item_id) = @_;
78     my $type = ref($class) || $class;
79     my $item = Koha::Items->find( { barcode => barcodedecode( $item_id ) } );
80     unless ( $item ) {
81         siplog("LOG_DEBUG", "new ILS::Item('%s'): not found", $item_id);
82         warn "new ILS::Item($item_id) : No item '$item_id'.";
83         return;
84     }
85     my $self = $item->unblessed;
86     $self->{      'id'       } = $item->barcode;     # to SIP, the barcode IS the id.
87     $self->{permanent_location}= $item->homebranch;
88     $self->{'collection_code'} = $item->ccode;
89     $self->{  'call_number'  } = $item->itemcallnumber;
90
91     $self->{object} = $item;
92
93     my $it = $item->effective_itemtype;
94     my $itemtype = Koha::Database->new()->schema()->resultset('Itemtype')->find( $it );
95     $self->{sip_media_type} = $itemtype->sip_media_type() if $itemtype;
96
97     # check if its on issue and if so get the borrower
98     my $issue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
99     if ($issue) {
100         $self->{due_date} = dt_from_string( $issue->date_due, 'sql' )->truncate( to => 'minute' );
101         my $patron = Koha::Patrons->find( $issue->borrowernumber );
102         $self->{borrowernumber} = $patron->borrowernumber;
103     }
104     my $biblio = Koha::Biblios->find( $self->{biblionumber} );
105     my $holds = $biblio->current_holds->unblessed;
106     $self->{hold_queue} = $holds;
107     $self->{hold_shelf}    = [( grep {   defined $_->{found}  and $_->{found} eq 'W' } @{$self->{hold_queue}} )];
108     $self->{pending_queue} = [( grep {(! defined $_->{found}) or  $_->{found} ne 'W' } @{$self->{hold_queue}} )];
109     $self->{title} = $biblio->title;
110     $self->{author} = $biblio->author;
111     bless $self, $type;
112
113     siplog( "LOG_DEBUG", "new ILS::Item('%s'): found with title '%s'",
114         $item_id, $self->{title} // '' );
115
116     return $self;
117 }
118
119 # 0 means read-only
120 # 1 means read/write
121
122 my %fields = (
123     id                  => 0,
124     sip_media_type      => 0,
125     sip_item_properties => 0,
126     magnetic_media      => 0,
127     permanent_location  => 0,
128     current_location    => 0,
129     print_line          => 1,
130     screen_msg          => 1,
131     itemnumber          => 0,
132     biblionumber        => 0,
133     barcode             => 0,
134     onloan              => 0,
135     collection_code     => 0,
136     call_number         => 0,
137     enumchron           => 0,
138     location            => 0,
139     author              => 0,
140     title               => 0,
141 );
142
143 sub next_hold {
144     my $self = shift;
145     # use Data::Dumper; warn "next_hold() hold_shelf: " . Dumper($self->{hold_shelf}); warn "next_hold() pending_queue: " . $self->{pending_queue};
146     foreach (@{$self->hold_shelf}) {    # If this item was taken from the hold shelf, then that reserve still governs
147         next unless ($_->{itemnumber} and $_->{itemnumber} == $self->{itemnumber});
148         return $_;
149     }
150     if (scalar @{$self->{pending_queue}}) {    # Otherwise, if there is at least one hold, the first (best priority) gets it
151         return  $self->{pending_queue}->[0];
152     }
153     return;
154 }
155
156 # hold_patron_id is NOT the barcode.  It's the borrowernumber.
157 # If a return triggers capture for a hold the borrowernumber is passed
158 # and saved so that other hold info can be retrieved
159 sub hold_patron_id {
160     my $self = shift;
161     my $id   = shift;
162     if ($id) {
163         $self->{hold}->{borrowernumber} = $id;
164     }
165     if ($self->{hold} ) {
166         return $self->{hold}->{borrowernumber};
167     }
168     return;
169
170 }
171 sub hold_patron_name {
172     my ( $self, $template ) = @_;
173     my $borrowernumber = $self->hold_patron_id() or return q{};
174
175     if ($template) {
176         my $tt = Template->new();
177
178         my $patron = Koha::Patrons->find($borrowernumber);
179
180         my $output;
181         $tt->process( \$template, { patron => $patron }, \$output );
182         return $output;
183     }
184
185     my $holder = Koha::Patrons->find( $borrowernumber );
186     unless ($holder) {
187         siplog("LOG_ERR", "While checking hold, failed to retrieve the patron with borrowernumber '$borrowernumber'");
188         return q{};
189     }
190     my $email = $holder->email || '';
191     my $phone = $holder->phone || '';
192     my $extra = ($email and $phone) ? " ($email, $phone)" :  # both populated, employ comma
193                 ($email or  $phone) ? " ($email$phone)"   :  # only 1 populated, we don't care which: no comma
194                 "" ;                                         # neither populated, empty string
195     my $name = $holder->firstname ? $holder->firstname . ' ' : '';
196     $name .= $holder->surname . $extra;
197     return $name;
198 }
199
200 sub hold_patron_bcode {
201     my $self = shift;
202     my $borrowernumber = (@_ ? shift: $self->hold_patron_id()) or return q{};
203     my $holder = Koha::Patrons->find( $borrowernumber );
204     if ($holder and $holder->cardnumber ) {
205         return $holder->cardnumber;
206     }
207     return q();
208 }
209
210 sub destination_loc {
211     my $self = shift;
212     my $set_loc = shift;
213     if ($set_loc) {
214         $self->{dest_loc} = $set_loc;
215     }
216     if ($self->{dest_loc} ) {
217         return $self->{dest_loc};
218     }
219     return q{};
220 }
221
222 our $AUTOLOAD;
223
224 sub DESTROY { } # keeps AUTOLOAD from catching inherent DESTROY calls
225
226 sub AUTOLOAD {
227     my $self = shift;
228     my $class = ref($self) or croak "$self is not an object";
229     my $name = $AUTOLOAD;
230
231     $name =~ s/.*://;
232
233     unless (exists $fields{$name}) {
234                 croak "Cannot access '$name' field of class '$class'";
235     }
236
237         if (@_) {
238         $fields{$name} or croak "Field '$name' of class '$class' is READ ONLY.";
239                 return $self->{$name} = shift;
240         } else {
241                 return $self->{$name};
242         }
243 }
244
245 sub status_update {     # FIXME: this looks unimplemented
246     my ($self, $props) = @_;
247     my $status = C4::SIP::ILS::Transaction->new();
248     $self->{sip_item_properties} = $props;
249     $status->{ok} = 1;
250     return $status;
251 }
252
253 sub title_id {
254     my $self = shift;
255     return $self->{title};
256 }
257
258 sub sip_circulation_status {
259     my $self = shift;
260     if ( $self->{borrowernumber} ) {
261         return '04';    # charged
262     }
263     elsif ( grep { $_->{itemnumber} == $self->{itemnumber}  } @{ $self->{hold_shelf} } ) {
264         return '08';    # waiting on hold shelf
265     }
266     else {
267         return '03';    # available
268     }    # FIXME: 01-13 enumerated in spec.
269 }
270
271 sub sip_security_marker {
272     my $self = shift;
273     return '02';        # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
274 }
275 sub sip_fee_type {
276     my $self = shift;
277     return '01';    # FIXME? 01-09 enumerated in spec.  We just use O1-other/unknown.
278 }
279
280 sub fee {
281     my $self = shift;
282     return $self->{fee} || 0;
283 }
284 sub fee_currency {
285     my $self = shift;
286     return $self->{currency} || 'USD';
287 }
288 sub owner {
289     my $self = shift;
290     return $self->{homebranch};
291 }
292 sub hold_queue {
293     my $self = shift;
294         (defined $self->{hold_queue}) or return [];
295     return $self->{hold_queue};
296 }
297 sub pending_queue {
298     my $self = shift;
299         (defined $self->{pending_queue}) or return [];
300     return $self->{pending_queue};
301 }
302 sub hold_shelf {
303     my $self = shift;
304         (defined $self->{hold_shelf}) or return [];
305     return $self->{hold_shelf};
306 }
307
308 sub hold_queue_position {
309         my ($self, $patron_id) = @_;
310         ($self->{hold_queue}) or return 0;
311         my $i = 0;
312         foreach (@{$self->{hold_queue}}) {
313                 $i++;
314                 $_->{patron_id} or next;
315                 if ($self->barcode_is_borrowernumber($patron_id, $_->{borrowernumber})) {
316                         return $i;  # maybe should return $_->{priority}
317                 }
318         }
319     return 0;
320 }
321
322 sub due_date {
323     my $self = shift;
324     return $self->{due_date} || 0;
325 }
326 sub recall_date {
327     my $self = shift;
328     return $self->{recall_date} || 0;
329 }
330 sub hold_pickup_date {
331     my $self = shift;
332
333     my $hold = Koha::Holds->find({ itemnumber => $self->{itemnumber}, found => 'W' });
334     if ( $hold ) {
335         return $hold->expirationdate || 0;
336     }
337
338     return 0;
339 }
340
341 # This is a partial check of "availability".  It is not supposed to check everything here.
342 # An item is available for a patron if it is:
343 # 1) checked out to the same patron 
344 #    AND no pending (i.e. non-W) hold queue
345 # OR
346 # 2) not checked out
347 #    AND (not on hold_shelf OR is on hold_shelf for patron)
348 #
349 # What this means is we are consciously allowing the patron to checkout (but not renew) an item that DOES
350 # have non-W holds on it, but has not been "picked" from the stacks.  That is to say, the
351 # patron has retrieved the item before the librarian.
352 #
353 # We don't check if the patron is at the front of the pending queue in the first case, because
354 # they should not be able to place a hold on an item they already have.
355
356 sub available {
357         my ($self, $for_patron) = @_;
358         my $count  = (defined $self->{pending_queue}) ? scalar @{$self->{pending_queue}} : 0;
359         my $count2 = (defined $self->{hold_shelf}   ) ? scalar @{$self->{hold_shelf}   } : 0;
360         $debug and print STDERR "availability check: pending_queue size $count, hold_shelf size $count2\n";
361     if (defined($self->{borrowernumber})) {
362         ($self->{borrowernumber} eq $for_patron) or return 0;
363                 return ($count ? 0 : 1);
364         } else {        # not checked out
365         ($count2) and return $self->barcode_is_borrowernumber($for_patron, $self->{hold_shelf}[0]->{borrowernumber});
366         }
367         return 0;
368 }
369
370 sub _barcode_to_borrowernumber {
371     my $known = shift;
372     return unless defined $known;
373     my $patron = Koha::Patrons->find( { cardnumber => $known } ) or return;
374     return $patron->borrowernumber
375 }
376 sub barcode_is_borrowernumber {    # because hold_queue only has borrowernumber...
377     my $self = shift;
378     my $barcode = shift;
379     my $number  = shift or return;    # can't be zero
380     return unless defined $barcode; # might be 0 or 000 or 000000
381     my $converted = _barcode_to_borrowernumber($barcode);
382     return unless $converted;
383     return ($number == $converted);
384 }
385 sub fill_reserve {
386     my $self = shift;
387     my $hold = shift or return;
388     foreach (qw(biblionumber borrowernumber reservedate)) {
389         $hold->{$_} or return;
390     }
391     return ModReserveFill($hold);
392 }
393
394 =head2 build_additional_item_fields_string
395
396 This method builds the part of the sip message for additional item fields
397 to send in the item related message responses
398
399 =cut
400
401 sub build_additional_item_fields_string {
402     my ( $self, $server ) = @_;
403
404     my $string = q{};
405
406     if ( $server->{account}->{item_field} ) {
407         my @fields_to_send =
408           ref $server->{account}->{item_field} eq "ARRAY"
409           ? @{ $server->{account}->{item_field} }
410           : ( $server->{account}->{item_field} );
411
412         foreach my $f ( @fields_to_send ) {
413             my $code = $f->{code};
414             my $value = $self->{object}->$code;
415             $string .= add_field( $f->{field}, $value );
416         }
417     }
418
419     return $string;
420 }
421
422 1;
423 __END__
424