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