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