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 OpenSRF::Utils 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,
52 sub DESTROY { } # keeps AUTOLOAD from catching inherent DESTROY calls
56 my $class = ref($self) or croak "$self is not an object";
61 unless (exists $fields{$name}) {
62 croak "Cannot access '$name' field of class '$class'";
66 # $fields{$name} or croak "Field '$name' of class '$class' is READ ONLY."; # nah, go ahead
67 return $self->{$name} = shift;
69 return $self->{$name};
75 my ($class, $item_id) = @_;
76 my $type = ref($class) || $class;
77 my $self = bless( {}, $type );
79 syslog('LOG_DEBUG', "OILS: Loading item $item_id...");
80 return undef unless $item_id;
82 my $e = OpenILS::SIP->editor();
84 my $copy = $e->search_asset_copy(
86 { barcode => $item_id, deleted => 'f' },
90 acp => [ 'circ_lib', 'call_number', 'status', 'stat_cat_entry_copy_maps' ],
91 acn => [ 'owning_lib', 'record' ],
92 ascecm => [ 'stat_cat', 'stat_cat_entry' ],
99 syslog("LOG_DEBUG", "OILS: Item '%s' : not found", $item_id);
103 my $circ = $e->search_action_circulation([
105 target_copy => $copy->id,
106 checkin_time => undef,
108 {stop_fines => undef},
109 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
123 my $user = $circ->usr;
124 my $bc = ($user->card) ? $user->card->barcode : '';
125 $self->{patron} = $bc;
126 $self->{patron_object} = $user;
128 syslog('LOG_DEBUG', "OILS: Open circulation exists on $item_id : user = $bc");
131 $self->{id} = $item_id;
132 $self->{copy} = $copy;
133 $self->{volume} = $copy->call_number;
134 $self->{record} = $copy->call_number->record;
135 $self->{call_number} = $copy->call_number->label;
136 $self->{mods} = $U->record_to_mvr($self->{record}) if $self->{record}->marc;
137 $self->{transit} = $self->fetch_transit;
138 $self->{hold} = $self->fetch_hold;
141 # use the non-translated version of the copy location as the
142 # collection code, since it may be used for additional routing
143 # purposes by the SIP client. Config option?
144 $self->{collection_code} =
145 $e->retrieve_asset_copy_location([
146 $copy->location, {no_i18n => 1}])->name;
149 if($self->{transit}) {
150 $self->{destination_loc} = $self->{transit}->dest->shortname;
152 } elsif($self->{hold}) {
153 $self->{destination_loc} = $self->{hold}->pickup_lib->shortname;
156 syslog("LOG_DEBUG", "OILS: Item('$item_id'): found with title '%s'", $self->title_id);
158 my $config = OpenILS::SIP->config(); # FIXME : will not always match!
166 my $copy = $self->{copy} or return;
167 my $e = OpenILS::SIP->editor();
169 if ($copy->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
170 my $transit = $e->search_action_transit_copy([
172 target_copy => $copy->id, # NOT barcode ($self->id)
173 dest_recv_time => undef,
184 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
185 ") status is In Transit, but no action.transit_copy found!") unless $transit;
193 # fetch captured hold.
194 # Assume transit has already beeen fetched
197 my $copy = $self->{copy} or return;
198 my $e = OpenILS::SIP->editor();
200 if( ($copy->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF) ||
201 ($self->{transit} and $self->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) {
202 # item has been captured for a hold
204 my $hold = $e->search_action_hold_request([
206 current_copy => $copy->id,
207 capture_time => {'!=' => undef},
208 cancel_time => undef,
209 fulfillment_time => undef
215 ahr => ['pickup_lib']
220 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
221 ") is captured for a hold, but there is no matching hold request") unless $hold;
229 sub run_attr_script {
231 return 1 if $self->{ran_script};
232 $self->{ran_script} = 1;
234 # use the in-db circ modifier configuration
235 my $config = {magneticMedia => 'f', SIPMediaType => '001'}; # defaults
236 my $mod = $self->{copy}->circ_modifier;
239 my $mod_obj = OpenILS::SIP->editor()->retrieve_config_circ_modifier($mod);
241 $config->{magneticMedia} = $mod_obj->magnetic_media;
242 $config->{SIPMediaType} = $mod_obj->sip2_media_type;
246 $self->{item_config_result} = { item_config => $config };
257 return 0 unless $self->run_attr_script;
258 my $mag = $self->{item_config_result}->{item_config}->{magneticMedia} || '';
259 syslog('LOG_DEBUG', "OILS: magnetic = $mag");
260 return ($mag and $mag =~ /t(rue)?/io) ? 1 : 0;
265 return 0 unless $self->run_attr_script;
266 my $media = $self->{item_config_result}->{item_config}->{SIPMediaType} || '';
267 syslog('LOG_DEBUG', "OILS: media type = $media");
268 return ($media) ? $media : '001';
273 my $t = ($self->{mods}) ? $self->{mods}->title : $self->{copy}->dummy_title;
274 return OpenILS::SIP::clean_text($t);
277 sub permanent_location {
279 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
282 sub current_location {
284 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
293 # 05 Charged; not to be recalled until earliest recall date
296 # 08 Waiting on hold shelf
297 # 09 Waiting to be re-shelved
298 # 10 In transit between library locations
299 # 11 Claimed returned
302 sub sip_circulation_status {
304 my $stat = $self->{copy}->status->id;
306 return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
307 return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
308 return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
309 return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
310 return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
311 return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
312 return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
313 return '12' if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID);
314 return '13' if $stat == OILS_COPY_STATUS_MISSING;
319 sub sip_security_marker {
320 return '02'; # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
325 # Return '06' for rental unless the fee is a deposit, or there is
326 # no fee. In the latter cases, return '01'.
327 return ($self->{copy}->deposit_amount > 0.0 && $self->{copy}->deposit =~ /^f/i) ? '06' : '01';
332 return $self->{copy}->deposit_amount;
338 return OpenILS::SIP->config()->{implementation_config}->{currency};
343 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
348 return [$self->{hold}->id] if $self->{hold};
352 sub hold_queue_position { # TODO
353 my ($self, $patron_id) = @_;
360 # this should force correct circ fetching
361 require OpenILS::Utils::CStoreEditor;
362 my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
363 #my $e = OpenILS::SIP->editor();
365 my $circ = $e->search_action_circulation(
366 { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
371 syslog('LOG_INFO', "OILS: No open circ found for copy");
375 my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
376 syslog('LOG_DEBUG', "OILS: Found item due date = $due");
380 sub recall_date { # TODO
386 # Note: If the held item is in transit, this will be an approximation of shelf
387 # expire time, since the time is not set until the item is checked in at the pickup location
388 my %shelf_expire_setting_cache;
389 sub hold_pickup_date {
391 my $copy = $self->{copy};
392 my $hold = $self->{hold} or return 0;
394 my $date = $hold->shelf_expire_time;
397 # hold has not hit the shelf. create a best guess.
399 my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
400 $U->ou_ancestor_setting_value(
401 $hold->pickup_lib->id,
402 'circ.holds.default_shelf_expire_interval');
404 $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
407 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
408 $date = DateTime->now->add(seconds => $seconds);
409 $date = $date->strftime('%FT%T%z') if $date;
413 return OpenILS::SIP->format_date($date) if $date;
418 # message to display on console
421 return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
428 return OpenILS::SIP::clean_text($self->{print_line}) || '';
432 # An item is available for a patron if
433 # 1) It's not checked out and (there's no hold queue OR patron
434 # is at the front of the queue)
436 # 2) It's checked out to the patron and there's no hold queue
438 my ($self, $for_patron) = @_;
440 my $stat = $self->{copy}->status->id;
442 $stat == OILS_COPY_STATUS_AVAILABLE or
443 $stat == OILS_COPY_STATUS_RESHELVING;
450 my $extra_fields = {};
451 my $c = $self->{copy};
452 foreach my $stat_cat_entry (@{$c->stat_cat_entry_copy_maps}) {
453 my $stat_cat = $stat_cat_entry->stat_cat;
454 next unless ($stat_cat->sip_field);
455 my $value = $stat_cat_entry->stat_cat_entry->value;
456 if(defined $stat_cat->sip_format && length($stat_cat->sip_format) > 0) { # Has a format string?
457 if($stat_cat->sip_format =~ /^\|(.*)\|$/) { # Regex match?
458 if($value =~ /($1)/) { # If we have a match
459 if(defined $2) { # Check to see if they embedded a capture group
460 $value = $2; # If so, use it
462 else { # No embedded capture group?
463 $value = $1; # Use our outer one
467 $value = ''; # Empty string. Will be checked for below.
470 else { # Not a regex match - Try sprintf match (looking for a %s, if any)
471 $value = sprintf($stat_cat->sip_format, $value);
474 next unless length($value) > 0; # No value = no export
475 $value =~ s/\|//g; # Remove all lingering pipe chars for sane output purposes
476 $extra_fields->{ $stat_cat->sip_field } = [] unless (defined $extra_fields->{$stat_cat->sip_field});
477 push(@{$extra_fields->{ $stat_cat->sip_field}}, $value);
479 return $extra_fields;
488 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
492 =head2 owning_lib vs. circ_lib
494 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item
495 should return after it's done rotating/floating to other branches (via staff intervention),
496 or some combination of those. The owning_lib, however, is not necessarily where the item
497 should be going "right now" or where it should return to by default. That would be the copy
498 circ_lib or the transit destination. (In fact, the item may B<never> go to the owning_lib for
499 its entire existence). In the context of SIP, the circ_lib more accurately describes the item's
500 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
502 This confusion extends also to the SIP extension field of "owner". It means that the SIP owner does not
503 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
504 owner" but not necessarily the "current owner". Because we populate SIP fields with circ_lib, the
505 owning_lib is unused by SIP.