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);
394 return [$self->{hold}->id] if $self->{hold};
398 sub hold_queue_position { # TODO
399 my ($self, $patron_id) = @_;
406 # this should force correct circ fetching
407 require OpenILS::Utils::CStoreEditor;
408 my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
409 #my $e = OpenILS::SIP->editor();
411 my $circ = $e->search_action_circulation(
412 { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
417 syslog('LOG_INFO', "OILS: No open circ found for copy");
421 my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
422 syslog('LOG_DEBUG', "OILS: Found item due date = $due");
426 sub recall_date { # TODO
432 # Note: If the held item is in transit, this will be an approximation of shelf
433 # expire time, since the time is not set until the item is checked in at the pickup location
434 my %shelf_expire_setting_cache;
435 sub hold_pickup_date {
437 my $copy = $self->{copy};
438 my $hold = $self->{hold} or return 0;
440 my $date = $hold->shelf_expire_time;
443 # hold has not hit the shelf. create a best guess.
445 my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
446 $U->ou_ancestor_setting_value(
447 $hold->pickup_lib->id,
448 'circ.holds.default_shelf_expire_interval');
450 $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
453 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
454 $date = DateTime->now->add(seconds => $seconds);
455 $date = $date->strftime('%FT%T%z') if $date;
459 return OpenILS::SIP->format_date($date) if $date;
464 # message to display on console
467 return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
474 return OpenILS::SIP::clean_text($self->{print_line}) || '';
478 # An item is available for a patron if
479 # 1) It's not checked out and (there's no hold queue OR patron
480 # is at the front of the queue)
482 # 2) It's checked out to the patron and there's no hold queue
484 my ($self, $for_patron) = @_;
486 my $stat = $self->{copy}->status->id;
488 $stat == OILS_COPY_STATUS_AVAILABLE or
489 $stat == OILS_COPY_STATUS_RESHELVING;
496 my $extra_fields = {};
497 my $c = $self->{copy};
498 foreach my $stat_cat_entry (@{$c->stat_cat_entry_copy_maps}) {
499 my $stat_cat = $stat_cat_entry->stat_cat;
500 next unless ($stat_cat->sip_field);
501 my $value = $stat_cat_entry->stat_cat_entry->value;
502 if(defined $stat_cat->sip_format && length($stat_cat->sip_format) > 0) { # Has a format string?
503 if($stat_cat->sip_format =~ /^\|(.*)\|$/) { # Regex match?
504 if($value =~ /($1)/) { # If we have a match
505 if(defined $2) { # Check to see if they embedded a capture group
506 $value = $2; # If so, use it
508 else { # No embedded capture group?
509 $value = $1; # Use our outer one
513 $value = ''; # Empty string. Will be checked for below.
516 else { # Not a regex match - Try sprintf match (looking for a %s, if any)
517 $value = sprintf($stat_cat->sip_format, $value);
520 next unless length($value) > 0; # No value = no export
521 $value =~ s/\|//g; # Remove all lingering pipe chars for sane output purposes
522 $extra_fields->{ $stat_cat->sip_field } = [] unless (defined $extra_fields->{$stat_cat->sip_field});
523 push(@{$extra_fields->{ $stat_cat->sip_field}}, $value);
525 return $extra_fields;
534 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
538 =head2 owning_lib vs. circ_lib
540 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item
541 should return after it's done rotating/floating to other branches (via staff intervention),
542 or some combination of those. The owning_lib, however, is not necessarily where the item
543 should be going "right now" or where it should return to by default. That would be the copy
544 circ_lib or the transit destination. (In fact, the item may B<never> go to the owning_lib for
545 its entire existence). In the context of SIP, the circ_lib more accurately describes the item's
546 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
548 This confusion extends also to the SIP extension field of "owner". It means that the SIP owner does not
549 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
550 owner" but not necessarily the "current owner". Because we populate SIP fields with circ_lib, the
551 owning_lib is unused by SIP.