]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/SIP/Item.pm
Merge branch 'master' of git.evergreen-ils.org:Evergreen into template-toolkit-opac
[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             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     return '01';    # FIXME? 01-09 enumerated in spec.  We just use O1-other/unknown.
368 }
369
370 sub fee {           # TODO
371     my $self = shift;
372     return 0;
373 }
374
375
376 sub fee_currency {
377         my $self = shift;
378         return OpenILS::SIP->config()->{implementation_config}->{currency};
379 }
380
381 sub owner {
382     my $self = shift;
383     return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
384 }
385
386 sub hold_queue {
387     my $self = shift;
388     return [];
389 }
390
391 sub hold_queue_position {       # TODO
392     my ($self, $patron_id) = @_;
393     return 1;
394 }
395
396 sub due_date {
397     my $self = shift;
398
399     # this should force correct circ fetching
400     require OpenILS::Utils::CStoreEditor;
401     my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
402     #my $e = OpenILS::SIP->editor();
403
404     my $circ = $e->search_action_circulation(
405         { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
406
407     $e->rollback;
408
409     if( !$circ ) {
410         syslog('LOG_INFO', "OILS: No open circ found for copy");
411         return 0;
412     }
413
414     my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
415     syslog('LOG_DEBUG', "OILS: Found item due date = $due");
416     return $due;
417 }
418
419 sub recall_date {       # TODO
420     my $self = shift;
421     return 0;
422 }
423
424
425 # Note: If the held item is in transit, this will be an approximation of shelf 
426 # expire time, since the time is not set until the item is  checked in at the pickup location
427 my %shelf_expire_setting_cache;
428 sub hold_pickup_date {  
429     my $self = shift;
430     my $copy = $self->{copy};
431     my $hold = $self->{hold} or return 0;
432
433     my $date = $hold->shelf_expire_time;
434
435     if(!$date) {
436         # hold has not hit the shelf.  create a best guess.
437
438         my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
439             $U->ou_ancestor_setting_value(
440                 $hold->pickup_lib->id, 
441                 'circ.holds.default_shelf_expire_interval');
442
443         $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
444
445         if($interval) {
446             my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
447             $date = DateTime->now->add(seconds => $seconds);
448             $date = $date->strftime('%FT%T%z') if $date;
449         }
450     }
451
452     return OpenILS::SIP->format_date($date) if $date;
453
454     return 0;
455 }
456
457 # message to display on console
458 sub screen_msg {
459     my $self = shift;
460     return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
461 }
462
463
464 # reciept printer
465 sub print_line {
466     my $self = shift;
467     return OpenILS::SIP::clean_text($self->{print_line}) || '';
468 }
469
470
471 # An item is available for a patron if
472 # 1) It's not checked out and (there's no hold queue OR patron
473 #    is at the front of the queue)
474 # OR
475 # 2) It's checked out to the patron and there's no hold queue
476 sub available {
477     my ($self, $for_patron) = @_;
478
479     my $stat = $self->{copy}->status->id;
480     return 1 if 
481         $stat == OILS_COPY_STATUS_AVAILABLE or
482         $stat == OILS_COPY_STATUS_RESHELVING;
483
484     return 0;
485 }
486
487 sub extra_fields {
488     my( $self ) = @_;
489     my $extra_fields = {};
490     my $c = $self->{copy};
491     foreach my $stat_cat_entry (@{$c->stat_cat_entry_copy_maps}) {
492         my $stat_cat = $stat_cat_entry->stat_cat;
493         next unless ($stat_cat->sip_field);
494         my $value = $stat_cat_entry->stat_cat_entry->value;
495         if(defined $stat_cat->sip_format && length($stat_cat->sip_format) > 0) { # Has a format string?
496             if($stat_cat->sip_format =~ /^\|(.*)\|$/) { # Regex match?
497                 if($value =~ /($1)/) { # If we have a match
498                     if(defined $2) { # Check to see if they embedded a capture group
499                         $value = $2; # If so, use it
500                     }
501                     else { # No embedded capture group?
502                         $value = $1; # Use our outer one
503                     }
504                 }
505                 else { # No match?
506                     $value = ''; # Empty string. Will be checked for below.
507                 }
508             }
509             else { # Not a regex match - Try sprintf match (looking for a %s, if any)
510                 $value = sprintf($stat_cat->sip_format, $value);
511             }
512         }
513         next unless length($value) > 0; # No value = no export
514         $value =~ s/\|//g; # Remove all lingering pipe chars for sane output purposes
515         $extra_fields->{ $stat_cat->sip_field } = [] unless (defined $extra_fields->{$stat_cat->sip_field});
516         push(@{$extra_fields->{ $stat_cat->sip_field}}, $value);
517     }
518     return $extra_fields;
519 }
520
521
522 1;
523 __END__
524
525 =head1 NAME
526
527 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
528
529 =head1 DESCRIPTION
530
531 =head2 owning_lib vs. circ_lib
532
533 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item 
534 should return after it's done rotating/floating to other branches (via staff intervention),
535 or some combination of those.  The owning_lib, however, is not necessarily where the item
536 should be going "right now" or where it should return to by default.  That would be the copy
537 circ_lib or the transit destination.  (In fact, the item may B<never> go to the owning_lib for
538 its entire existence).  In the context of SIP, the circ_lib more accurately describes the item's
539 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
540
541 This confusion extends also to the SIP extension field of "owner".  It means that the SIP owner does not 
542 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
543 owner" but not necessarily the "current owner".  Because we populate SIP fields with circ_lib, the
544 owning_lib is unused by SIP.  
545
546 =head1 TODO
547
548 Holds queue logic
549
550 =cut