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