]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/SIP/Item.pm
Whitespace. gah.
[Evergreen.git] / Open-ILS / src / perlmods / 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' ],
92                                         acn => [ 'owning_lib', 'record' ],
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             stop_fines_time => undef, 
107             checkin_time => undef
108         },
109         {
110             flesh => 2,
111             flesh_fields => {
112                 circ => ['usr'],
113                 au => ['card']
114             }
115         }
116     ])->[0];
117
118     if($circ) {
119
120         my $user = $circ->usr;
121         my $bc = ($user->card) ? $user->card->barcode : '';
122         $self->{patron} = $bc;
123         $self->{patron_object} = $user;
124
125         syslog('LOG_DEBUG', "OILS: Open circulation exists on $item_id : user = $bc");
126     }
127
128     $self->{id}         = $item_id;
129     $self->{copy}       = $copy;
130     $self->{volume}     = $copy->call_number;
131     $self->{record}     = $copy->call_number->record;
132     $self->{call_number} = $copy->call_number->label;
133     $self->{mods}       = $U->record_to_mvr($self->{record}) if $self->{record}->marc;
134     $self->{transit}    = $self->fetch_transit;
135     $self->{hold}       = $self->fetch_hold;
136
137
138     # use the non-translated version of the copy location as the
139     # collection code, since it may be used for additional routing
140     # purposes by the SIP client.  Config option?
141     $self->{collection_code} = 
142         $e->retrieve_asset_copy_location([
143             $copy->location, {no_i18n => 1}])->name;
144
145
146     if($self->{transit}) {
147         $self->{destination_loc} = $self->{transit}->dest->shortname;
148
149     } elsif($self->{hold}) {
150         $self->{destination_loc} = $self->{hold}->pickup_lib->shortname;
151     }
152
153     syslog("LOG_DEBUG", "OILS: Item('$item_id'): found with title '%s'", $self->title_id);
154
155     my $config = OpenILS::SIP->config();    # FIXME : will not always match!
156     my $legacy = $config->{implementation_config}->{legacy_script_support} || undef;
157
158     if( defined $legacy ) {
159         $self->{legacy_script_support} = ($legacy =~ /t(rue)?/io) ? 1 : 0;
160         syslog("LOG_DEBUG", "legacy_script_support is set in SIP config: " . $self->{legacy_script_support});
161
162     } else {
163         my $lss = OpenSRF::Utils::SettingsClient->new->config_value(
164             apps         => 'open-ils.circ',
165             app_settings => 'legacy_script_support'
166         );
167         $self->{legacy_script_support} = ($lss =~ /t(rue)?/io) ? 1 : 0;
168         syslog("LOG_DEBUG", "legacy_script_support is set in SRF config: " . $self->{legacy_script_support});
169     }
170
171     return $self;
172 }
173
174 # fetch copy transit
175 sub fetch_transit {
176     my $self = shift;
177     my $copy = $self->{copy} or return;
178     my $e = OpenILS::SIP->editor();
179
180     if ($copy->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
181         my $transit = $e->search_action_transit_copy([
182             {
183                 target_copy    => $copy->id,    # NOT barcode ($self->id)
184                 dest_recv_time => undef
185             },
186             {
187                 flesh => 1,
188                 flesh_fields => {
189                     atc => ['dest']
190                 }
191             }
192         ])->[0];
193
194         syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
195             ") status is In Transit, but no action.transit_copy found!") unless $transit;
196             
197         return $transit;
198     }
199     
200     return undef;
201 }
202
203 # fetch captured hold.
204 # Assume transit has already beeen fetched
205 sub fetch_hold {
206     my $self = shift;
207     my $copy = $self->{copy} or return;
208     my $e = OpenILS::SIP->editor();
209
210     if( ($copy->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF) ||
211         ($self->{transit} and $self->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) {
212         # item has been captured for a hold
213
214         my $hold = $e->search_action_hold_request([
215             {
216                 current_copy        => $copy->id,
217                 capture_time        => {'!=' => undef},
218                 cancel_time         => undef,
219                 fulfillment_time    => undef
220             },
221             {
222                 limit => 1,
223                 flesh => 1,
224                 flesh_fields => {
225                     ahr => ['pickup_lib']
226                 }
227             }
228         ])->[0];
229
230         syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
231             ") is captured for a hold, but there is no matching hold request") unless $hold;
232
233         return $hold;
234     }
235
236     return undef;
237 }
238
239 sub run_attr_script {
240         my $self = shift;
241         return 1 if $self->{ran_script};
242         $self->{ran_script} = 1;
243
244     if($self->{legacy_script_support}){
245
246         syslog('LOG_DEBUG', "Legacy script support is ON");
247         my $config = OpenILS::SIP->config();
248         my $path               = $config->{implementation_config}->{scripts}->{path};
249         my $item_config_script = $config->{implementation_config}->{scripts}->{item_config};
250
251         $path = ref($path) eq 'ARRAY' ? $path : [$path];
252         my $path_str = join(", ", @$path);
253
254         syslog('LOG_DEBUG', "OILS: Script path = [$path_str], Item config script = $item_config_script");
255
256         my $runner = OpenILS::Application::Circ::ScriptBuilder->build({
257             copy   => $self->{copy},
258             editor => OpenILS::SIP->editor(),
259         });
260
261         $runner->add_path($_) for @$path;
262         $runner->load($item_config_script);
263
264         unless( $self->{item_config_result} = $runner->run ) {      # assignment, not comparison
265             $runner->cleanup;
266             warn "Item config script [$path_str : $item_config_script] failed to run: $@\n";
267             syslog('LOG_ERR', "OILS: Item config script [$path_str : $item_config_script] failed to run: $@");
268             return undef;
269         }
270
271         $runner->cleanup;
272
273     } else {
274
275         # use the in-db circ modifier configuration 
276         my $config = {magneticMedia => 'f', SIPMediaType => '001'};     # defaults
277         my $mod = $self->{copy}->circ_modifier;
278
279         if($mod) {
280             my $mod_obj = OpenILS::SIP->editor()->retrieve_config_circ_modifier($mod);
281             if($mod_obj) {
282                 $config->{magneticMedia} = $mod_obj->magnetic_media;
283                 $config->{SIPMediaType}  = $mod_obj->sip2_media_type;
284             }
285         }
286
287         $self->{item_config_result} = { item_config => $config };
288     }
289
290         return 1;
291 }
292
293 sub magnetic_media {
294     my $self = shift;
295     $self->magnetic(@_);
296 }
297 sub magnetic {
298     my $self = shift;
299     return 0 unless $self->run_attr_script;
300     my $mag = $self->{item_config_result}->{item_config}->{magneticMedia} || '';
301     syslog('LOG_DEBUG', "OILS: magnetic = $mag");
302     return ($mag and $mag =~ /t(rue)?/io) ? 1 : 0;
303 }
304
305 sub sip_media_type {
306     my $self = shift;
307     return 0 unless $self->run_attr_script;
308     my $media = $self->{item_config_result}->{item_config}->{SIPMediaType} || '';
309     syslog('LOG_DEBUG', "OILS: media type = $media");
310     return ($media) ? $media : '001';
311 }
312
313 sub title_id {
314     my $self = shift;
315     my $t =  ($self->{mods}) ? $self->{mods}->title : $self->{copy}->dummy_title;
316     return OpenILS::SIP::clean_text($t);
317 }
318
319 sub permanent_location {
320     my $self = shift;
321     return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
322 }
323
324 sub current_location {
325     my $self = shift;
326     return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
327 }
328
329
330 # 2 chars 0-99 
331 # 01 Other
332 # 02 On order
333 # 03 Available
334 # 04 Charged
335 # 05 Charged; not to be recalled until earliest recall date
336 # 06 In process
337 # 07 Recalled
338 # 08 Waiting on hold shelf
339 # 09 Waiting to be re-shelved
340 # 10 In transit between library locations
341 # 11 Claimed returned
342 # 12 Lost
343 # 13 Missing 
344 sub sip_circulation_status {
345     my $self = shift;
346     my $stat = $self->{copy}->status->id;
347
348     return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
349     return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
350     return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
351     return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
352     return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
353     return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
354     return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
355     return '12' if $stat == OILS_COPY_STATUS_LOST;
356     return '13' if $stat == OILS_COPY_STATUS_MISSING;
357         
358     return 01;
359 }
360
361 sub sip_security_marker {
362     return '02';    # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
363 }
364
365 sub sip_fee_type {
366     return '01';    # FIXME? 01-09 enumerated in spec.  We just use O1-other/unknown.
367 }
368
369 sub fee {           # TODO
370     my $self = shift;
371     return 0;
372 }
373
374
375 sub fee_currency {
376         my $self = shift;
377         return OpenILS::SIP->config()->{implementation_config}->{currency};
378 }
379
380 sub owner {
381     my $self = shift;
382     return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
383 }
384
385 sub hold_queue {
386     my $self = shift;
387     return [];
388 }
389
390 sub hold_queue_position {       # TODO
391     my ($self, $patron_id) = @_;
392     return 1;
393 }
394
395 sub due_date {
396     my $self = shift;
397
398     # this should force correct circ fetching
399     require OpenILS::Utils::CStoreEditor;
400     my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
401     #my $e = OpenILS::SIP->editor();
402
403     my $circ = $e->search_action_circulation(
404         { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
405
406     $e->rollback;
407
408     if( !$circ ) {
409         syslog('LOG_INFO', "OILS: No open circ found for copy");
410         return 0;
411     }
412
413     my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
414     syslog('LOG_DEBUG', "OILS: Found item due date = $due");
415     return $due;
416 }
417
418 sub recall_date {       # TODO
419     my $self = shift;
420     return 0;
421 }
422
423
424 # Note: If the held item is in transit, this will be an approximation of shelf 
425 # expire time, since the time is not set until the item is  checked in at the pickup location
426 my %shelf_expire_setting_cache;
427 sub hold_pickup_date {  
428     my $self = shift;
429     my $copy = $self->{copy};
430     my $hold = $self->{hold} or return 0;
431
432     my $date = $hold->shelf_expire_time;
433
434     if(!$date) {
435         # hold has not hit the shelf.  create a best guess.
436
437         my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
438             $U->ou_ancestor_setting_value(
439                 $hold->pickup_lib->id, 
440                 'circ.holds.default_shelf_expire_interval');
441
442         $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
443
444         if($interval) {
445             my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
446             $date = DateTime->now->add(seconds => $seconds);
447             $date = $date->strftime('%FT%T%z') if $date;
448         }
449     }
450
451     return OpenILS::SIP->format_date($date) if $date;
452
453     return 0;
454 }
455
456 # message to display on console
457 sub screen_msg {
458     my $self = shift;
459     return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
460 }
461
462
463 # reciept printer
464 sub print_line {
465     my $self = shift;
466     return OpenILS::SIP::clean_text($self->{print_line}) || '';
467 }
468
469
470 # An item is available for a patron if
471 # 1) It's not checked out and (there's no hold queue OR patron
472 #    is at the front of the queue)
473 # OR
474 # 2) It's checked out to the patron and there's no hold queue
475 sub available {
476     my ($self, $for_patron) = @_;
477
478     my $stat = $self->{copy}->status->id;
479     return 1 if 
480         $stat == OILS_COPY_STATUS_AVAILABLE or
481         $stat == OILS_COPY_STATUS_RESHELVING;
482
483     return 0;
484 }
485
486
487 1;
488 __END__
489
490 =head1 NAME
491
492 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
493
494 =head1 DESCRIPTION
495
496 =head2 owning_lib vs. circ_lib
497
498 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item 
499 should return after it's done rotating/floating to other branches (via staff intervention),
500 or some combination of those.  The owning_lib, however, is not necessarily where the item
501 should be going "right now" or where it should return to by default.  That would be the copy
502 circ_lib or the transit destination.  (In fact, the item may B<never> go to the owning_lib for
503 its entire existence).  In the context of SIP, the circ_lib more accurately describes the item's
504 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
505
506 This confusion extends also to the SIP extension field of "owner".  It means that the SIP owner does not 
507 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
508 owner" but not necessarily the "current owner".  Because we populate SIP fields with circ_lib, the
509 owning_lib is unused by SIP.  
510
511 =head1 TODO
512
513 Holds queue logic
514
515 =cut