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