]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/SIP/Item.pm
Fix in-transit hold retarget
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / SIP / Item.pm
1 package OpenILS::SIP::Item;
2 use strict; use warnings;
3
4 use Sys::Syslog qw(syslog);
5 use Carp;
6
7 use OpenILS::SIP;
8 use OpenILS::SIP::Transaction;
9 use OpenILS::Application::AppUtils;
10 use OpenILS::Application::Circ::ScriptBuilder;
11 # use Data::Dumper;
12 use OpenILS::Const qw/:const/;
13 use OpenSRF::Utils qw/:datetime/;
14 use DateTime::Format::ISO8601;
15 use OpenSRF::Utils::SettingsClient;
16 my $U = 'OpenILS::Application::AppUtils';
17
18 my %item_db;
19
20 # 0 means read-only
21 # 1 means read/write    Actually, gloves are off.  Set what you like.
22
23 my %fields = (
24     id => 0,
25 #   sip_media_type     => 0,
26     sip_item_properties => 0,
27 #   magnetic_media     => 0,
28     permanent_location => 0,
29     current_location   => 0,
30 #   print_line         => 1,
31 #   screen_msg         => 1,
32 #   itemnumber         => 0,
33 #   biblionumber       => 0,
34     hold               => 0,
35     hold_patron_bcode  => 0,
36     hold_patron_name   => 0,
37     barcode            => 0,
38     onloan             => 0,
39     collection_code    => 0,
40     destination_loc    => 0,
41     call_number        => 0,
42     enumchron          => 0,
43     location           => 0,
44     author             => 0,
45     title              => 0,
46     copy               => 0,
47     volume             => 0,
48     record             => 0,
49     mods               => 0,
50 );
51
52 our $AUTOLOAD;
53 sub DESTROY { } # keeps AUTOLOAD from catching inherent DESTROY calls
54
55 sub AUTOLOAD {
56     my $self = shift;
57     my $class = ref($self) or croak "$self is not an object";
58     my $name = $AUTOLOAD;
59
60     $name =~ s/.*://;
61
62     unless (exists $fields{$name}) {
63         croak "Cannot access '$name' field of class '$class'";
64     }
65
66     if (@_) {
67         # $fields{$name} or croak "Field '$name' of class '$class' is READ ONLY.";  # nah, go ahead
68         return $self->{$name} = shift;
69     } else {
70         return $self->{$name};
71     }
72 }
73
74
75 sub new {
76     my ($class, $item_id) = @_;
77     my $type = ref($class) || $class;
78     my $self = bless( {}, $type );
79
80     syslog('LOG_DEBUG', "OILS: Loading item $item_id...");
81     return undef unless $item_id;
82
83     my $e = OpenILS::SIP->editor();
84
85     my $copy = $e->search_asset_copy(
86                 [
87                         { barcode => $item_id, deleted => 'f' },
88                         {
89                                 flesh => 3,
90                                 flesh_fields => {
91                                         acp => [ 'circ_lib', 'call_number', 'status', 'stat_cat_entry_copy_maps' ],
92                                         acn => [ 'owning_lib', 'record' ],
93                     ascecm => [ 'stat_cat', 'stat_cat_entry' ],
94                                 }
95                         }
96                 ]
97     )->[0];
98
99         if(!$copy) {
100                 syslog("LOG_DEBUG", "OILS: Item '%s' : not found", $item_id);
101                 return undef;
102         }
103
104     my $circ = $e->search_action_circulation([
105         {
106             target_copy => $copy->id,
107             stop_fines_time => undef, 
108             checkin_time => undef
109         },
110         {
111             flesh => 2,
112             flesh_fields => {
113                 circ => ['usr'],
114                 au => ['card']
115             }
116         }
117     ])->[0];
118
119     if($circ) {
120
121         my $user = $circ->usr;
122         my $bc = ($user->card) ? $user->card->barcode : '';
123         $self->{patron} = $bc;
124         $self->{patron_object} = $user;
125
126         syslog('LOG_DEBUG', "OILS: Open circulation exists on $item_id : user = $bc");
127     }
128
129     $self->{id}         = $item_id;
130     $self->{copy}       = $copy;
131     $self->{volume}     = $copy->call_number;
132     $self->{record}     = $copy->call_number->record;
133     $self->{call_number} = $copy->call_number->label;
134     $self->{mods}       = $U->record_to_mvr($self->{record}) if $self->{record}->marc;
135     $self->{transit}    = $self->fetch_transit;
136     $self->{hold}       = $self->fetch_hold;
137
138
139     # use the non-translated version of the copy location as the
140     # collection code, since it may be used for additional routing
141     # purposes by the SIP client.  Config option?
142     $self->{collection_code} = 
143         $e->retrieve_asset_copy_location([
144             $copy->location, {no_i18n => 1}])->name;
145
146
147     if($self->{transit}) {
148         $self->{destination_loc} = $self->{transit}->dest->shortname;
149
150     } elsif($self->{hold}) {
151         $self->{destination_loc} = $self->{hold}->pickup_lib->shortname;
152     }
153
154     syslog("LOG_DEBUG", "OILS: Item('$item_id'): found with title '%s'", $self->title_id);
155
156     my $config = OpenILS::SIP->config();    # FIXME : will not always match!
157     my $legacy = $config->{implementation_config}->{legacy_script_support} || undef;
158
159     if( defined $legacy ) {
160         $self->{legacy_script_support} = ($legacy =~ /t(rue)?/io) ? 1 : 0;
161         syslog("LOG_DEBUG", "legacy_script_support is set in SIP config: " . $self->{legacy_script_support});
162
163     } else {
164         my $lss = OpenSRF::Utils::SettingsClient->new->config_value(
165             apps         => 'open-ils.circ',
166             app_settings => 'legacy_script_support'
167         );
168         $self->{legacy_script_support} = ($lss =~ /t(rue)?/io) ? 1 : 0;
169         syslog("LOG_DEBUG", "legacy_script_support is set in SRF config: " . $self->{legacy_script_support});
170     }
171
172     return $self;
173 }
174
175 # fetch copy transit
176 sub fetch_transit {
177     my $self = shift;
178     my $copy = $self->{copy} or return;
179     my $e = OpenILS::SIP->editor();
180
181     if ($copy->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
182         my $transit = $e->search_action_transit_copy([
183             {
184                 target_copy    => $copy->id,    # NOT barcode ($self->id)
185                 dest_recv_time => undef
186             },
187             {
188                 flesh => 1,
189                 flesh_fields => {
190                     atc => ['dest']
191                 }
192             }
193         ])->[0];
194
195         syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
196             ") status is In Transit, but no action.transit_copy found!") unless $transit;
197             
198         return $transit;
199     }
200     
201     return undef;
202 }
203
204 # fetch captured hold.
205 # Assume transit has already beeen fetched
206 sub fetch_hold {
207     my $self = shift;
208     my $copy = $self->{copy} or return;
209     my $e = OpenILS::SIP->editor();
210
211     if( ($copy->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF) ||
212         ($self->{transit} and $self->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) {
213         # item has been captured for a hold
214
215         my $hold = $e->search_action_hold_request([
216             {
217                 current_copy        => $copy->id,
218                 capture_time        => {'!=' => undef},
219                 cancel_time         => undef,
220                 fulfillment_time    => undef
221             },
222             {
223                 limit => 1,
224                 flesh => 1,
225                 flesh_fields => {
226                     ahr => ['pickup_lib']
227                 }
228             }
229         ])->[0];
230
231         syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
232             ") is captured for a hold, but there is no matching hold request") unless $hold;
233
234         return $hold;
235     }
236
237     return undef;
238 }
239
240 sub run_attr_script {
241         my $self = shift;
242         return 1 if $self->{ran_script};
243         $self->{ran_script} = 1;
244
245     if($self->{legacy_script_support}){
246
247         syslog('LOG_DEBUG', "Legacy script support is ON");
248         my $config = OpenILS::SIP->config();
249         my $path               = $config->{implementation_config}->{scripts}->{path};
250         my $item_config_script = $config->{implementation_config}->{scripts}->{item_config};
251
252         $path = ref($path) eq 'ARRAY' ? $path : [$path];
253         my $path_str = join(", ", @$path);
254
255         syslog('LOG_DEBUG', "OILS: Script path = [$path_str], Item config script = $item_config_script");
256
257         my $runner = OpenILS::Application::Circ::ScriptBuilder->build({
258             copy   => $self->{copy},
259             editor => OpenILS::SIP->editor(),
260         });
261
262         $runner->add_path($_) for @$path;
263         $runner->load($item_config_script);
264
265         unless( $self->{item_config_result} = $runner->run ) {      # assignment, not comparison
266             $runner->cleanup;
267             warn "Item config script [$path_str : $item_config_script] failed to run: $@\n";
268             syslog('LOG_ERR', "OILS: Item config script [$path_str : $item_config_script] failed to run: $@");
269             return undef;
270         }
271
272         $runner->cleanup;
273
274     } else {
275
276         # use the in-db circ modifier configuration 
277         my $config = {magneticMedia => 'f', SIPMediaType => '001'};     # defaults
278         my $mod = $self->{copy}->circ_modifier;
279
280         if($mod) {
281             my $mod_obj = OpenILS::SIP->editor()->retrieve_config_circ_modifier($mod);
282             if($mod_obj) {
283                 $config->{magneticMedia} = $mod_obj->magnetic_media;
284                 $config->{SIPMediaType}  = $mod_obj->sip2_media_type;
285             }
286         }
287
288         $self->{item_config_result} = { item_config => $config };
289     }
290
291         return 1;
292 }
293
294 sub magnetic_media {
295     my $self = shift;
296     $self->magnetic(@_);
297 }
298 sub magnetic {
299     my $self = shift;
300     return 0 unless $self->run_attr_script;
301     my $mag = $self->{item_config_result}->{item_config}->{magneticMedia} || '';
302     syslog('LOG_DEBUG', "OILS: magnetic = $mag");
303     return ($mag and $mag =~ /t(rue)?/io) ? 1 : 0;
304 }
305
306 sub sip_media_type {
307     my $self = shift;
308     return 0 unless $self->run_attr_script;
309     my $media = $self->{item_config_result}->{item_config}->{SIPMediaType} || '';
310     syslog('LOG_DEBUG', "OILS: media type = $media");
311     return ($media) ? $media : '001';
312 }
313
314 sub title_id {
315     my $self = shift;
316     my $t =  ($self->{mods}) ? $self->{mods}->title : $self->{copy}->dummy_title;
317     return OpenILS::SIP::clean_text($t);
318 }
319
320 sub permanent_location {
321     my $self = shift;
322     return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
323 }
324
325 sub current_location {
326     my $self = shift;
327     return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
328 }
329
330
331 # 2 chars 0-99 
332 # 01 Other
333 # 02 On order
334 # 03 Available
335 # 04 Charged
336 # 05 Charged; not to be recalled until earliest recall date
337 # 06 In process
338 # 07 Recalled
339 # 08 Waiting on hold shelf
340 # 09 Waiting to be re-shelved
341 # 10 In transit between library locations
342 # 11 Claimed returned
343 # 12 Lost
344 # 13 Missing 
345 sub sip_circulation_status {
346     my $self = shift;
347     my $stat = $self->{copy}->status->id;
348
349     return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
350     return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
351     return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
352     return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
353     return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
354     return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
355     return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
356     return '12' if $stat == OILS_COPY_STATUS_LOST;
357     return '13' if $stat == OILS_COPY_STATUS_MISSING;
358         
359     return '01';
360 }
361
362 sub sip_security_marker {
363     return '02';    # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
364 }
365
366 sub sip_fee_type {
367     my $self = shift;
368     # Return '06' for rental unless the fee is a deposit, or there is
369     # no fee. In the latter cases, return '01'.
370     return ($self->{copy}->deposit_amount > 0.0 && $self->{copy}->deposit =~ /^f/i) ? '06' : '01';
371 }
372
373 sub fee {
374     my $self = shift;
375     return $self->{copy}->deposit_amount;
376 }
377
378
379 sub fee_currency {
380         my $self = shift;
381         return OpenILS::SIP->config()->{implementation_config}->{currency};
382 }
383
384 sub owner {
385     my $self = shift;
386     return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
387 }
388
389 sub hold_queue {
390     my $self = shift;
391     return [];
392 }
393
394 sub hold_queue_position {       # TODO
395     my ($self, $patron_id) = @_;
396     return 1;
397 }
398
399 sub due_date {
400     my $self = shift;
401
402     # this should force correct circ fetching
403     require OpenILS::Utils::CStoreEditor;
404     my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
405     #my $e = OpenILS::SIP->editor();
406
407     my $circ = $e->search_action_circulation(
408         { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
409
410     $e->rollback;
411
412     if( !$circ ) {
413         syslog('LOG_INFO', "OILS: No open circ found for copy");
414         return 0;
415     }
416
417     my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
418     syslog('LOG_DEBUG', "OILS: Found item due date = $due");
419     return $due;
420 }
421
422 sub recall_date {       # TODO
423     my $self = shift;
424     return 0;
425 }
426
427
428 # Note: If the held item is in transit, this will be an approximation of shelf 
429 # expire time, since the time is not set until the item is  checked in at the pickup location
430 my %shelf_expire_setting_cache;
431 sub hold_pickup_date {  
432     my $self = shift;
433     my $copy = $self->{copy};
434     my $hold = $self->{hold} or return 0;
435
436     my $date = $hold->shelf_expire_time;
437
438     if(!$date) {
439         # hold has not hit the shelf.  create a best guess.
440
441         my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
442             $U->ou_ancestor_setting_value(
443                 $hold->pickup_lib->id, 
444                 'circ.holds.default_shelf_expire_interval');
445
446         $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
447
448         if($interval) {
449             my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
450             $date = DateTime->now->add(seconds => $seconds);
451             $date = $date->strftime('%FT%T%z') if $date;
452         }
453     }
454
455     return OpenILS::SIP->format_date($date) if $date;
456
457     return 0;
458 }
459
460 # message to display on console
461 sub screen_msg {
462     my $self = shift;
463     return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
464 }
465
466
467 # reciept printer
468 sub print_line {
469     my $self = shift;
470     return OpenILS::SIP::clean_text($self->{print_line}) || '';
471 }
472
473
474 # An item is available for a patron if
475 # 1) It's not checked out and (there's no hold queue OR patron
476 #    is at the front of the queue)
477 # OR
478 # 2) It's checked out to the patron and there's no hold queue
479 sub available {
480     my ($self, $for_patron) = @_;
481
482     my $stat = $self->{copy}->status->id;
483     return 1 if 
484         $stat == OILS_COPY_STATUS_AVAILABLE or
485         $stat == OILS_COPY_STATUS_RESHELVING;
486
487     return 0;
488 }
489
490 sub extra_fields {
491     my( $self ) = @_;
492     my $extra_fields = {};
493     my $c = $self->{copy};
494     foreach my $stat_cat_entry (@{$c->stat_cat_entry_copy_maps}) {
495         my $stat_cat = $stat_cat_entry->stat_cat;
496         next unless ($stat_cat->sip_field);
497         my $value = $stat_cat_entry->stat_cat_entry->value;
498         if(defined $stat_cat->sip_format && length($stat_cat->sip_format) > 0) { # Has a format string?
499             if($stat_cat->sip_format =~ /^\|(.*)\|$/) { # Regex match?
500                 if($value =~ /($1)/) { # If we have a match
501                     if(defined $2) { # Check to see if they embedded a capture group
502                         $value = $2; # If so, use it
503                     }
504                     else { # No embedded capture group?
505                         $value = $1; # Use our outer one
506                     }
507                 }
508                 else { # No match?
509                     $value = ''; # Empty string. Will be checked for below.
510                 }
511             }
512             else { # Not a regex match - Try sprintf match (looking for a %s, if any)
513                 $value = sprintf($stat_cat->sip_format, $value);
514             }
515         }
516         next unless length($value) > 0; # No value = no export
517         $value =~ s/\|//g; # Remove all lingering pipe chars for sane output purposes
518         $extra_fields->{ $stat_cat->sip_field } = [] unless (defined $extra_fields->{$stat_cat->sip_field});
519         push(@{$extra_fields->{ $stat_cat->sip_field}}, $value);
520     }
521     return $extra_fields;
522 }
523
524
525 1;
526 __END__
527
528 =head1 NAME
529
530 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
531
532 =head1 DESCRIPTION
533
534 =head2 owning_lib vs. circ_lib
535
536 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item 
537 should return after it's done rotating/floating to other branches (via staff intervention),
538 or some combination of those.  The owning_lib, however, is not necessarily where the item
539 should be going "right now" or where it should return to by default.  That would be the copy
540 circ_lib or the transit destination.  (In fact, the item may B<never> go to the owning_lib for
541 its entire existence).  In the context of SIP, the circ_lib more accurately describes the item's
542 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
543
544 This confusion extends also to the SIP extension field of "owner".  It means that the SIP owner does not 
545 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
546 owner" but not necessarily the "current owner".  Because we populate SIP fields with circ_lib, the
547 owning_lib is unused by SIP.  
548
549 =head1 TODO
550
551 Holds queue logic
552
553 =cut