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;
11 use OpenILS::Const qw/:const/;
12 use OpenILS::Utils::DateTime qw/:datetime/;
13 use DateTime::Format::ISO8601;
14 use OpenSRF::Utils::SettingsClient;
15 my $U = 'OpenILS::Application::AppUtils';
20 # 1 means read/write Actually, gloves are off. Set what you like.
24 # sip_media_type => 0,
25 sip_item_properties => 0,
26 # magnetic_media => 0,
27 permanent_location => 0,
28 current_location => 0,
34 hold_patron_bcode => 0,
35 hold_patron_name => 0,
49 hold_patron_phone => 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!
167 my $copy = $self->{copy} or return;
168 my $e = OpenILS::SIP->editor();
170 if ($copy->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
171 my $transit = $e->search_action_transit_copy([
173 target_copy => $copy->id, # NOT barcode ($self->id)
174 dest_recv_time => undef,
185 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
186 ") status is In Transit, but no action.transit_copy found!") unless $transit;
194 # fetch captured hold.
195 # Assume transit has already beeen fetched
198 my $copy = $self->{copy} or return;
199 my $e = OpenILS::SIP->editor();
201 if( ($copy->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF) ||
202 ($self->{transit} and $self->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) {
203 # item has been captured for a hold
205 my $hold = $e->search_action_hold_request([
207 current_copy => $copy->id,
208 capture_time => {'!=' => undef},
209 cancel_time => undef,
210 fulfillment_time => undef
216 ahr => ['pickup_lib']
221 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
222 ") is captured for a hold, but there is no matching hold request") unless $hold;
223 $self->{hold_patron_phone} = $hold->phone_notify unless !$hold;
231 sub run_attr_script {
233 return 1 if $self->{ran_script};
234 $self->{ran_script} = 1;
236 # use the in-db circ modifier configuration
237 my $config = {magneticMedia => 'f', SIPMediaType => '001'}; # defaults
238 my $mod = $self->{copy}->circ_modifier;
241 my $mod_obj = OpenILS::SIP->editor()->retrieve_config_circ_modifier($mod);
243 $config->{magneticMedia} = $mod_obj->magnetic_media;
244 $config->{SIPMediaType} = $mod_obj->sip2_media_type;
248 $self->{item_config_result} = { item_config => $config };
259 return 0 unless $self->run_attr_script;
260 my $mag = $self->{item_config_result}->{item_config}->{magneticMedia} || '';
261 syslog('LOG_DEBUG', "OILS: magnetic = $mag");
262 return ($mag and $mag =~ /t(rue)?/io) ? 1 : 0;
267 return 0 unless $self->run_attr_script;
268 my $media = $self->{item_config_result}->{item_config}->{SIPMediaType} || '';
269 syslog('LOG_DEBUG', "OILS: media type = $media");
270 return ($media) ? $media : '001';
275 my $t = ($self->{mods}) ? $self->{mods}->title : $self->{copy}->dummy_title;
279 sub permanent_location {
281 return $self->{copy}->circ_lib->shortname;
284 sub current_location {
286 return $self->{copy}->circ_lib->shortname;
295 # 05 Charged; not to be recalled until earliest recall date
298 # 08 Waiting on hold shelf
299 # 09 Waiting to be re-shelved
300 # 10 In transit between library locations
301 # 11 Claimed returned
304 sub sip_circulation_status {
306 my $stat = $self->{copy}->status->id;
308 return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
309 return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
310 return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
311 return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
312 return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
313 return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
314 return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
315 return '12' if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID);
316 return '13' if $stat == OILS_COPY_STATUS_MISSING;
321 sub sip_security_marker {
322 return '02'; # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
327 # Return '06' for rental unless the fee is a deposit, or there is
328 # no fee. In the latter cases, return '01'.
329 return ($self->{copy}->deposit_amount > 0.0 && $self->{copy}->deposit =~ /^f/i) ? '06' : '01';
334 return $self->{copy}->deposit_amount;
340 return OpenILS::SIP->config()->{implementation_config}->{currency};
345 return $self->{copy}->circ_lib->shortname;
350 return [$self->{hold}->id] if $self->{hold};
354 sub hold_queue_position { # TODO
355 my ($self, $patron_id) = @_;
362 # this should force correct circ fetching
363 require OpenILS::Utils::CStoreEditor;
364 my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
365 #my $e = OpenILS::SIP->editor();
367 my $circ = $e->search_action_circulation(
368 { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
373 syslog('LOG_INFO', "OILS: No open circ found for copy");
377 my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
378 syslog('LOG_DEBUG', "OILS: Found item due date = $due");
382 sub recall_date { # TODO
388 # Note: If the held item is in transit, this will be an approximation of shelf
389 # expire time, since the time is not set until the item is checked in at the pickup location
390 my %shelf_expire_setting_cache;
391 sub hold_pickup_date {
393 my $copy = $self->{copy};
394 my $hold = $self->{hold} or return 0;
396 my $date = $hold->shelf_expire_time;
399 # hold has not hit the shelf. create a best guess.
401 my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
402 $U->ou_ancestor_setting_value(
403 $hold->pickup_lib->id,
404 'circ.holds.default_shelf_expire_interval');
406 $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
409 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($interval);
410 $date = DateTime->now->add(seconds => $seconds);
411 $date = $date->strftime('%FT%T%z') if $date;
415 return OpenILS::SIP->format_date($date) if $date;
420 # message to display on console
423 return $self->{screen_msg} || '';
430 return $self->{print_line} || '';
434 # An item is available for a patron if
435 # 1) It's not checked out and (there's no hold queue OR patron
436 # is at the front of the queue)
438 # 2) It's checked out to the patron and there's no hold queue
440 my ($self, $for_patron) = @_;
442 my $stat = $self->{copy}->status->id;
444 $stat == OILS_COPY_STATUS_AVAILABLE or
445 $stat == OILS_COPY_STATUS_RESHELVING;
452 my $extra_fields = {};
453 my $c = $self->{copy};
454 foreach my $stat_cat_entry (@{$c->stat_cat_entry_copy_maps}) {
455 my $stat_cat = $stat_cat_entry->stat_cat;
456 next unless ($stat_cat->sip_field);
457 my $value = $stat_cat_entry->stat_cat_entry->value;
458 if(defined $stat_cat->sip_format && length($stat_cat->sip_format) > 0) { # Has a format string?
459 if($stat_cat->sip_format =~ /^\|(.*)\|$/) { # Regex match?
460 if($value =~ /($1)/) { # If we have a match
461 if(defined $2) { # Check to see if they embedded a capture group
462 $value = $2; # If so, use it
464 else { # No embedded capture group?
465 $value = $1; # Use our outer one
469 $value = ''; # Empty string. Will be checked for below.
472 else { # Not a regex match - Try sprintf match (looking for a %s, if any)
473 $value = sprintf($stat_cat->sip_format, $value);
476 next unless length($value) > 0; # No value = no export
477 $value =~ s/\|//g; # Remove all lingering pipe chars for sane output purposes
478 $extra_fields->{ $stat_cat->sip_field } = [] unless (defined $extra_fields->{$stat_cat->sip_field});
479 push(@{$extra_fields->{ $stat_cat->sip_field}}, $value);
481 return $extra_fields;
490 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
494 =head2 owning_lib vs. circ_lib
496 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item
497 should return after it's done rotating/floating to other branches (via staff intervention),
498 or some combination of those. The owning_lib, however, is not necessarily where the item
499 should be going "right now" or where it should return to by default. That would be the copy
500 circ_lib or the transit destination. (In fact, the item may B<never> go to the owning_lib for
501 its entire existence). In the context of SIP, the circ_lib more accurately describes the item's
502 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
504 This confusion extends also to the SIP extension field of "owner". It means that the SIP owner does not
505 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
506 owner" but not necessarily the "current owner". Because we populate SIP fields with circ_lib, the
507 owning_lib is unused by SIP.