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
183 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
184 ") status is In Transit, but no action.transit_copy found!") unless $transit;
192 # fetch captured hold.
193 # Assume transit has already beeen fetched
196 my $copy = $self->{copy} or return;
197 my $e = OpenILS::SIP->editor();
199 if( ($copy->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF) ||
200 ($self->{transit} and $self->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) {
201 # item has been captured for a hold
203 my $hold = $e->search_action_hold_request([
205 current_copy => $copy->id,
206 capture_time => {'!=' => undef},
207 cancel_time => undef,
208 fulfillment_time => undef
214 ahr => ['pickup_lib']
219 syslog('LOG_WARNING', "OILS: Item(".$copy->barcode.
220 ") is captured for a hold, but there is no matching hold request") unless $hold;
228 sub run_attr_script {
230 return 1 if $self->{ran_script};
231 $self->{ran_script} = 1;
233 # use the in-db circ modifier configuration
234 my $config = {magneticMedia => 'f', SIPMediaType => '001'}; # defaults
235 my $mod = $self->{copy}->circ_modifier;
238 my $mod_obj = OpenILS::SIP->editor()->retrieve_config_circ_modifier($mod);
240 $config->{magneticMedia} = $mod_obj->magnetic_media;
241 $config->{SIPMediaType} = $mod_obj->sip2_media_type;
245 $self->{item_config_result} = { item_config => $config };
256 return 0 unless $self->run_attr_script;
257 my $mag = $self->{item_config_result}->{item_config}->{magneticMedia} || '';
258 syslog('LOG_DEBUG', "OILS: magnetic = $mag");
259 return ($mag and $mag =~ /t(rue)?/io) ? 1 : 0;
264 return 0 unless $self->run_attr_script;
265 my $media = $self->{item_config_result}->{item_config}->{SIPMediaType} || '';
266 syslog('LOG_DEBUG', "OILS: media type = $media");
267 return ($media) ? $media : '001';
272 my $t = ($self->{mods}) ? $self->{mods}->title : $self->{copy}->dummy_title;
273 return OpenILS::SIP::clean_text($t);
276 sub permanent_location {
278 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
281 sub current_location {
283 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
292 # 05 Charged; not to be recalled until earliest recall date
295 # 08 Waiting on hold shelf
296 # 09 Waiting to be re-shelved
297 # 10 In transit between library locations
298 # 11 Claimed returned
301 sub sip_circulation_status {
303 my $stat = $self->{copy}->status->id;
305 return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
306 return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
307 return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
308 return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
309 return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
310 return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
311 return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
312 return '12' if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID);
313 return '13' if $stat == OILS_COPY_STATUS_MISSING;
318 sub sip_security_marker {
319 return '02'; # FIXME? 00-other; 01-None; 02-Tattle-Tape Security Strip (3M); 03-Whisper Tape (3M)
324 # Return '06' for rental unless the fee is a deposit, or there is
325 # no fee. In the latter cases, return '01'.
326 return ($self->{copy}->deposit_amount > 0.0 && $self->{copy}->deposit =~ /^f/i) ? '06' : '01';
331 return $self->{copy}->deposit_amount;
337 return OpenILS::SIP->config()->{implementation_config}->{currency};
342 return OpenILS::SIP::clean_text($self->{copy}->circ_lib->shortname);
347 return [$self->{hold}->id] if $self->{hold};
351 sub hold_queue_position { # TODO
352 my ($self, $patron_id) = @_;
359 # this should force correct circ fetching
360 require OpenILS::Utils::CStoreEditor;
361 my $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
362 #my $e = OpenILS::SIP->editor();
364 my $circ = $e->search_action_circulation(
365 { target_copy => $self->{copy}->id, checkin_time => undef } )->[0];
370 syslog('LOG_INFO', "OILS: No open circ found for copy");
374 my $due = OpenILS::SIP->format_date($circ->due_date, 'due');
375 syslog('LOG_DEBUG', "OILS: Found item due date = $due");
379 sub recall_date { # TODO
385 # Note: If the held item is in transit, this will be an approximation of shelf
386 # expire time, since the time is not set until the item is checked in at the pickup location
387 my %shelf_expire_setting_cache;
388 sub hold_pickup_date {
390 my $copy = $self->{copy};
391 my $hold = $self->{hold} or return 0;
393 my $date = $hold->shelf_expire_time;
396 # hold has not hit the shelf. create a best guess.
398 my $interval = $shelf_expire_setting_cache{$hold->pickup_lib->id} ||
399 $U->ou_ancestor_setting_value(
400 $hold->pickup_lib->id,
401 'circ.holds.default_shelf_expire_interval');
403 $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
406 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
407 $date = DateTime->now->add(seconds => $seconds);
408 $date = $date->strftime('%FT%T%z') if $date;
412 return OpenILS::SIP->format_date($date) if $date;
417 # message to display on console
420 return OpenILS::SIP::clean_text($self->{screen_msg}) || '';
427 return OpenILS::SIP::clean_text($self->{print_line}) || '';
431 # An item is available for a patron if
432 # 1) It's not checked out and (there's no hold queue OR patron
433 # is at the front of the queue)
435 # 2) It's checked out to the patron and there's no hold queue
437 my ($self, $for_patron) = @_;
439 my $stat = $self->{copy}->status->id;
441 $stat == OILS_COPY_STATUS_AVAILABLE or
442 $stat == OILS_COPY_STATUS_RESHELVING;
449 my $extra_fields = {};
450 my $c = $self->{copy};
451 foreach my $stat_cat_entry (@{$c->stat_cat_entry_copy_maps}) {
452 my $stat_cat = $stat_cat_entry->stat_cat;
453 next unless ($stat_cat->sip_field);
454 my $value = $stat_cat_entry->stat_cat_entry->value;
455 if(defined $stat_cat->sip_format && length($stat_cat->sip_format) > 0) { # Has a format string?
456 if($stat_cat->sip_format =~ /^\|(.*)\|$/) { # Regex match?
457 if($value =~ /($1)/) { # If we have a match
458 if(defined $2) { # Check to see if they embedded a capture group
459 $value = $2; # If so, use it
461 else { # No embedded capture group?
462 $value = $1; # Use our outer one
466 $value = ''; # Empty string. Will be checked for below.
469 else { # Not a regex match - Try sprintf match (looking for a %s, if any)
470 $value = sprintf($stat_cat->sip_format, $value);
473 next unless length($value) > 0; # No value = no export
474 $value =~ s/\|//g; # Remove all lingering pipe chars for sane output purposes
475 $extra_fields->{ $stat_cat->sip_field } = [] unless (defined $extra_fields->{$stat_cat->sip_field});
476 push(@{$extra_fields->{ $stat_cat->sip_field}}, $value);
478 return $extra_fields;
487 OpenILS::SIP::Item - SIP abstraction layer for OpenILS Items.
491 =head2 owning_lib vs. circ_lib
493 In Evergreen, owning_lib is the org unit that purchased the item, the place to which the item
494 should return after it's done rotating/floating to other branches (via staff intervention),
495 or some combination of those. The owning_lib, however, is not necessarily where the item
496 should be going "right now" or where it should return to by default. That would be the copy
497 circ_lib or the transit destination. (In fact, the item may B<never> go to the owning_lib for
498 its entire existence). In the context of SIP, the circ_lib more accurately describes the item's
499 permanent location, i.e. where it needs to be sent if it's not en route to somewhere else.
501 This confusion extends also to the SIP extension field of "owner". It means that the SIP owner does not
502 correspond to EG's asset.volume.owning_lib, mainly because owning_lib is effectively the "ultimate
503 owner" but not necessarily the "current owner". Because we populate SIP fields with circ_lib, the
504 owning_lib is unused by SIP.