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