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