1 package OpenILS::SIP::Item;
2 use strict; use warnings;
4 use Sys::Syslog qw(syslog);
8 use OpenILS::SIP::Transaction;
9 use OpenILS::Application::AppUtils;
10 use OpenILS::Application::Circ::ScriptBuilder;
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';
21 # 1 means read/write Actually, gloves are off. Set what you like.
25 # sip_media_type => 0,
26 sip_item_properties => 0,
27 # magnetic_media => 0,
28 permanent_location => 0,
29 current_location => 0,
35 hold_patron_bcode => 0,
36 hold_patron_name => 0,
53 sub DESTROY { } # keeps AUTOLOAD from catching inherent DESTROY calls
57 my $class = ref($self) or croak "$self is not an object";
62 unless (exists $fields{$name}) {
63 croak "Cannot access '$name' field of class '$class'";
67 # $fields{$name} or croak "Field '$name' of class '$class' is READ ONLY."; # nah, go ahead
68 return $self->{$name} = shift;
70 return $self->{$name};
76 my ($class, $item_id) = @_;
77 my $type = ref($class) || $class;
78 my $self = bless( {}, $type );
80 syslog('LOG_DEBUG', "OILS: Loading item $item_id...");
81 return undef unless $item_id;
83 my $e = OpenILS::SIP->editor();
85 my $copy = $e->search_asset_copy(
87 { barcode => $item_id, deleted => 'f' },
91 acp => [ 'circ_lib', 'call_number', 'status', 'stat_cat_entry_copy_maps' ],
92 acn => [ 'owning_lib', 'record' ],
93 ascecm => [ 'stat_cat', 'stat_cat_entry' ],
100 syslog("LOG_DEBUG", "OILS: Item '%s' : not found", $item_id);
104 my $circ = $e->search_action_circulation([
106 target_copy => $copy->id,
107 checkin_time => undef,
109 {stop_fines => undef},
110 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
124 my $user = $circ->usr;
125 my $bc = ($user->card) ? $user->card->barcode : '';
126 $self->{patron} = $bc;
127 $self->{patron_object} = $user;
129 syslog('LOG_DEBUG', "OILS: Open circulation exists on $item_id : user = $bc");
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;
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;
150 if($self->{transit}) {
151 $self->{destination_loc} = $self->{transit}->dest->shortname;
153 } elsif($self->{hold}) {
154 $self->{destination_loc} = $self->{hold}->pickup_lib->shortname;
157 syslog("LOG_DEBUG", "OILS: Item('$item_id'): found with title '%s'", $self->title_id);
159 my $config = OpenILS::SIP->config(); # FIXME : will not always match!
160 my $legacy = $config->{implementation_config}->{legacy_script_support} || undef;
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});
167 my $lss = OpenSRF::Utils::SettingsClient->new->config_value(
168 apps => 'open-ils.circ',
169 app_settings => 'legacy_script_support'
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});
181 my $copy = $self->{copy} or return;
182 my $e = OpenILS::SIP->editor();
184 if ($copy->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
185 my $transit = $e->search_action_transit_copy([
187 target_copy => $copy->id, # NOT barcode ($self->id)
188 dest_recv_time => undef
198 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
199 ") status is In Transit, but no action.transit_copy found!") unless $transit;
207 # fetch captured hold.
208 # Assume transit has already beeen fetched
211 my $copy = $self->{copy} or return;
212 my $e = OpenILS::SIP->editor();
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
218 my $hold = $e->search_action_hold_request([
220 current_copy => $copy->id,
221 capture_time => {'!=' => undef},
222 cancel_time => undef,
223 fulfillment_time => undef
229 ahr => ['pickup_lib']
234 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
235 ") is captured for a hold, but there is no matching hold request") unless $hold;
243 sub run_attr_script {
245 return 1 if $self->{ran_script};
246 $self->{ran_script} = 1;
248 if($self->{legacy_script_support}){
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};
255 $path = ref($path) eq 'ARRAY' ? $path : [$path];
256 my $path_str = join(", ", @$path);
258 syslog('LOG_DEBUG', "OILS: Script path = [$path_str], Item config script = $item_config_script");
260 my $runner = OpenILS::Application::Circ::ScriptBuilder->build({
261 copy => $self->{copy},
262 editor => OpenILS::SIP->editor(),
265 $runner->add_path($_) for @$path;
266 $runner->load($item_config_script);
268 unless( $self->{item_config_result} = $runner->run ) { # assignment, not comparison
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: $@");
279 # use the in-db circ modifier configuration
280 my $config = {magneticMedia => 'f', SIPMediaType => '001'}; # defaults
281 my $mod = $self->{copy}->circ_modifier;
284 my $mod_obj = OpenILS::SIP->editor()->retrieve_config_circ_modifier($mod);
286 $config->{magneticMedia} = $mod_obj->magnetic_media;
287 $config->{SIPMediaType} = $mod_obj->sip2_media_type;
291 $self->{item_config_result} = { item_config => $config };
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;
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';
319 my $t = ($self->{mods}) ? $self->{mods}->title : $self->{copy}->dummy_title;
320 return OpenILS::SIP::clean_text($t);
323 sub permanent_location {
325 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
328 sub current_location {
330 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
339 # 05 Charged; not to be recalled until earliest recall date
342 # 08 Waiting on hold shelf
343 # 09 Waiting to be re-shelved
344 # 10 In transit between library locations
345 # 11 Claimed returned
348 sub sip_circulation_status {
350 my $stat = $self->{copy}->status->id;
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;
365 sub sip_security_marker {
366 return '02'; # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
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';
378 return $self->{copy}->deposit_amount;
384 return OpenILS::SIP->config()->{implementation_config}->{currency};
389 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
397 sub hold_queue_position { # TODO
398 my ($self, $patron_id) = @_;
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();
410 my $circ = $e->search_action_circulation(
411 { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
416 syslog('LOG_INFO', "OILS: No open circ found for copy");
420 my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
421 syslog('LOG_DEBUG', "OILS: Found item due date = $due");
425 sub recall_date { # TODO
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 {
436 my $copy = $self->{copy};
437 my $hold = $self->{hold} or return 0;
439 my $date = $hold->shelf_expire_time;
442 # hold has not hit the shelf. create a best guess.
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');
449 $shelf_expire_setting_cache{$hold->pickup_lib->id} = $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;
458 return OpenILS::SIP->format_date($date) if $date;
463 # message to display on console
466 return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
473 return OpenILS::SIP::clean_text($self->{print_line}) || '';
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)
481 # 2) It's checked out to the patron and there's no hold queue
483 my ($self, $for_patron) = @_;
485 my $stat = $self->{copy}->status->id;
487 $stat == OILS_COPY_STATUS_AVAILABLE or
488 $stat == OILS_COPY_STATUS_RESHELVING;
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
507 else { # No embedded capture group?
508 $value = $1; # Use our outer one
512 $value = ''; # Empty string. Will be checked for below.
515 else { # Not a regex match - Try sprintf match (looking for a %s, if any)
516 $value = sprintf($stat_cat->sip_format, $value);
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);
524 return $extra_fields;
533 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
537 =head2 owning_lib vs. circ_lib
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.
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.