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 stop_fines_time => undef,
108 checkin_time => undef
121 my $user = $circ->usr;
122 my $bc = ($user->card) ? $user->card->barcode : '';
123 $self->{patron} = $bc;
124 $self->{patron_object} = $user;
126 syslog('LOG_DEBUG', "OILS: Open circulation exists on $item_id : user = $bc");
129 $self->{id} = $item_id;
130 $self->{copy} = $copy;
131 $self->{volume} = $copy->call_number;
132 $self->{record} = $copy->call_number->record;
133 $self->{call_number} = $copy->call_number->label;
134 $self->{mods} = $U->record_to_mvr($self->{record}) if $self->{record}->marc;
135 $self->{transit} = $self->fetch_transit;
136 $self->{hold} = $self->fetch_hold;
139 # use the non-translated version of the copy location as the
140 # collection code, since it may be used for additional routing
141 # purposes by the SIP client. Config option?
142 $self->{collection_code} =
143 $e->retrieve_asset_copy_location([
144 $copy->location, {no_i18n => 1}])->name;
147 if($self->{transit}) {
148 $self->{destination_loc} = $self->{transit}->dest->shortname;
150 } elsif($self->{hold}) {
151 $self->{destination_loc} = $self->{hold}->pickup_lib->shortname;
154 syslog("LOG_DEBUG", "OILS: Item('$item_id'): found with title '%s'", $self->title_id);
156 my $config = OpenILS::SIP->config(); # FIXME : will not always match!
157 my $legacy = $config->{implementation_config}->{legacy_script_support} || undef;
159 if( defined $legacy ) {
160 $self->{legacy_script_support} = ($legacy =~ /t(rue)?/io) ? 1 : 0;
161 syslog("LOG_DEBUG", "legacy_script_support is set in SIP config: " . $self->{legacy_script_support});
164 my $lss = OpenSRF::Utils::SettingsClient->new->config_value(
165 apps => 'open-ils.circ',
166 app_settings => 'legacy_script_support'
168 $self->{legacy_script_support} = ($lss =~ /t(rue)?/io) ? 1 : 0;
169 syslog("LOG_DEBUG", "legacy_script_support is set in SRF config: " . $self->{legacy_script_support});
178 my $copy = $self->{copy} or return;
179 my $e = OpenILS::SIP->editor();
181 if ($copy->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
182 my $transit = $e->search_action_transit_copy([
184 target_copy => $copy->id, # NOT barcode ($self->id)
185 dest_recv_time => undef
195 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
196 ") status is In Transit, but no action.transit_copy found!") unless $transit;
204 # fetch captured hold.
205 # Assume transit has already beeen fetched
208 my $copy = $self->{copy} or return;
209 my $e = OpenILS::SIP->editor();
211 if( ($copy->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF) ||
212 ($self->{transit} and $self->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) {
213 # item has been captured for a hold
215 my $hold = $e->search_action_hold_request([
217 current_copy => $copy->id,
218 capture_time => {'!=' => undef},
219 cancel_time => undef,
220 fulfillment_time => undef
226 ahr => ['pickup_lib']
231 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
232 ") is captured for a hold, but there is no matching hold request") unless $hold;
240 sub run_attr_script {
242 return 1 if $self->{ran_script};
243 $self->{ran_script} = 1;
245 if($self->{legacy_script_support}){
247 syslog('LOG_DEBUG', "Legacy script support is ON");
248 my $config = OpenILS::SIP->config();
249 my $path = $config->{implementation_config}->{scripts}->{path};
250 my $item_config_script = $config->{implementation_config}->{scripts}->{item_config};
252 $path = ref($path) eq 'ARRAY' ? $path : [$path];
253 my $path_str = join(", ", @$path);
255 syslog('LOG_DEBUG', "OILS: Script path = [$path_str], Item config script = $item_config_script");
257 my $runner = OpenILS::Application::Circ::ScriptBuilder->build({
258 copy => $self->{copy},
259 editor => OpenILS::SIP->editor(),
262 $runner->add_path($_) for @$path;
263 $runner->load($item_config_script);
265 unless( $self->{item_config_result} = $runner->run ) { # assignment, not comparison
267 warn "Item config script [$path_str : $item_config_script] failed to run: $@\n";
268 syslog('LOG_ERR', "OILS: Item config script [$path_str : $item_config_script] failed to run: $@");
276 # use the in-db circ modifier configuration
277 my $config = {magneticMedia => 'f', SIPMediaType => '001'}; # defaults
278 my $mod = $self->{copy}->circ_modifier;
281 my $mod_obj = OpenILS::SIP->editor()->retrieve_config_circ_modifier($mod);
283 $config->{magneticMedia} = $mod_obj->magnetic_media;
284 $config->{SIPMediaType} = $mod_obj->sip2_media_type;
288 $self->{item_config_result} = { item_config => $config };
300 return 0 unless $self->run_attr_script;
301 my $mag = $self->{item_config_result}->{item_config}->{magneticMedia} || '';
302 syslog('LOG_DEBUG', "OILS: magnetic = $mag");
303 return ($mag and $mag =~ /t(rue)?/io) ? 1 : 0;
308 return 0 unless $self->run_attr_script;
309 my $media = $self->{item_config_result}->{item_config}->{SIPMediaType} || '';
310 syslog('LOG_DEBUG', "OILS: media type = $media");
311 return ($media) ? $media : '001';
316 my $t = ($self->{mods}) ? $self->{mods}->title : $self->{copy}->dummy_title;
317 return OpenILS::SIP::clean_text($t);
320 sub permanent_location {
322 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
325 sub current_location {
327 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
336 # 05 Charged; not to be recalled until earliest recall date
339 # 08 Waiting on hold shelf
340 # 09 Waiting to be re-shelved
341 # 10 In transit between library locations
342 # 11 Claimed returned
345 sub sip_circulation_status {
347 my $stat = $self->{copy}->status->id;
349 return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
350 return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
351 return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
352 return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
353 return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
354 return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
355 return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
356 return '12' if $stat == OILS_COPY_STATUS_LOST;
357 return '13' if $stat == OILS_COPY_STATUS_MISSING;
362 sub sip_security_marker {
363 return '02'; # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
368 # Return '06' for rental unless the fee is a deposit, or there is
369 # no fee. In the latter cases, return '01'.
370 return ($self->{copy}->deposit_amount > 0.0 && $self->{copy}->deposit =~ /^f/i) ? '06' : '01';
375 return $self->{copy}->deposit_amount;
381 return OpenILS::SIP->config()->{implementation_config}->{currency};
386 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
394 sub hold_queue_position { # TODO
395 my ($self, $patron_id) = @_;
402 # this should force correct circ fetching
403 require OpenILS::Utils::CStoreEditor;
404 my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
405 #my $e = OpenILS::SIP->editor();
407 my $circ = $e->search_action_circulation(
408 { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
413 syslog('LOG_INFO', "OILS: No open circ found for copy");
417 my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
418 syslog('LOG_DEBUG', "OILS: Found item due date = $due");
422 sub recall_date { # TODO
428 # Note: If the held item is in transit, this will be an approximation of shelf
429 # expire time, since the time is not set until the item is checked in at the pickup location
430 my %shelf_expire_setting_cache;
431 sub hold_pickup_date {
433 my $copy = $self->{copy};
434 my $hold = $self->{hold} or return 0;
436 my $date = $hold->shelf_expire_time;
439 # hold has not hit the shelf. create a best guess.
441 my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
442 $U->ou_ancestor_setting_value(
443 $hold->pickup_lib->id,
444 'circ.holds.default_shelf_expire_interval');
446 $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
449 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
450 $date = DateTime->now->add(seconds => $seconds);
451 $date = $date->strftime('%FT%T%z') if $date;
455 return OpenILS::SIP->format_date($date) if $date;
460 # message to display on console
463 return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
470 return OpenILS::SIP::clean_text($self->{print_line}) || '';
474 # An item is available for a patron if
475 # 1) It's not checked out and (there's no hold queue OR patron
476 # is at the front of the queue)
478 # 2) It's checked out to the patron and there's no hold queue
480 my ($self, $for_patron) = @_;
482 my $stat = $self->{copy}->status->id;
484 $stat == OILS_COPY_STATUS_AVAILABLE or
485 $stat == OILS_COPY_STATUS_RESHELVING;
492 my $extra_fields = {};
493 my $c = $self->{copy};
494 foreach my $stat_cat_entry (@{$c->stat_cat_entry_copy_maps}) {
495 my $stat_cat = $stat_cat_entry->stat_cat;
496 next unless ($stat_cat->sip_field);
497 my $value = $stat_cat_entry->stat_cat_entry->value;
498 if(defined $stat_cat->sip_format && length($stat_cat->sip_format) > 0) { # Has a format string?
499 if($stat_cat->sip_format =~ /^\|(.*)\|$/) { # Regex match?
500 if($value =~ /($1)/) { # If we have a match
501 if(defined $2) { # Check to see if they embedded a capture group
502 $value = $2; # If so, use it
504 else { # No embedded capture group?
505 $value = $1; # Use our outer one
509 $value = ''; # Empty string. Will be checked for below.
512 else { # Not a regex match - Try sprintf match (looking for a %s, if any)
513 $value = sprintf($stat_cat->sip_format, $value);
516 next unless length($value) > 0; # No value = no export
517 $value =~ s/\|//g; # Remove all lingering pipe chars for sane output purposes
518 $extra_fields->{ $stat_cat->sip_field } = [] unless (defined $extra_fields->{$stat_cat->sip_field});
519 push(@{$extra_fields->{ $stat_cat->sip_field}}, $value);
521 return $extra_fields;
530 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
534 =head2 owning_lib vs. circ_lib
536 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item
537 should return after it's done rotating/floating to other branches (via staff intervention),
538 or some combination of those. The owning_lib, however, is not necessarily where the item
539 should be going "right now" or where it should return to by default. That would be the copy
540 circ_lib or the transit destination. (In fact, the item may B<never> go to the owning_lib for
541 its entire existence). In the context of SIP, the circ_lib more accurately describes the item's
542 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
544 This confusion extends also to the SIP extension field of "owner". It means that the SIP owner does not
545 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
546 owner" but not necessarily the "current owner". Because we populate SIP fields with circ_lib, the
547 owning_lib is unused by SIP.