1 package OpenILS::Application::Circ;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
6 use OpenILS::Application::Circ::Circulate;
7 use OpenILS::Application::Circ::Survey;
8 use OpenILS::Application::Circ::StatCat;
9 use OpenILS::Application::Circ::Holds;
10 use OpenILS::Application::Circ::HoldNotify;
11 use OpenILS::Application::Circ::CircNotify;
12 use OpenILS::Application::Circ::CreditCard;
13 use OpenILS::Application::Circ::Money;
14 use OpenILS::Application::Circ::NonCat;
15 use OpenILS::Application::Circ::CopyLocations;
16 use OpenILS::Application::Circ::CircCommon;
19 use DateTime::Format::ISO8601;
21 use OpenILS::Application::AppUtils;
23 use OpenILS::Utils::DateTime qw/:datetime/;
24 use OpenSRF::AppSession;
25 use OpenILS::Utils::ModsParser;
27 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::Fieldmapper;
30 use OpenILS::Utils::CStoreEditor q/:funcs/;
31 use OpenILS::Const qw/:const/;
32 use OpenSRF::Utils::SettingsClient;
33 use OpenILS::Application::Cat::AssetCommon;
35 my $apputils = "OpenILS::Application::AppUtils";
38 my $holdcode = "OpenILS::Application::Circ::Holds";
40 # ------------------------------------------------------------------------
41 # Top level Circ package;
42 # ------------------------------------------------------------------------
46 OpenILS::Application::Circ::Circulate->initialize();
50 __PACKAGE__->register_method(
51 method => 'retrieve_circ',
53 api_name => 'open-ils.circ.retrieve',
55 Retrieve a circ object by id
56 @param authtoken Login session key
57 @pararm circid The id of the circ object
58 @param all_circ Returns an action.all_circulation_slim object instead
59 of an action.circulation object to pick up aged circs.
64 my( $s, $c, $a, $i, $all_circ ) = @_;
65 my $e = new_editor(authtoken => $a);
66 return $e->event unless $e->checkauth;
67 my $method = $all_circ ?
68 'retrieve_action_all_circulation_slim' :
69 'retrieve_action_circulation';
70 my $circ = $e->$method($i) or return $e->event;
71 if( $e->requestor->id ne ($circ->usr || '') ) {
72 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
78 __PACKAGE__->register_method(
79 method => 'fetch_circ_mods',
80 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
82 my($self, $conn, $args) = @_;
83 my $mods = new_editor()->retrieve_all_config_circ_modifier;
84 return [ map {$_->code} @$mods ] unless $$args{full};
88 __PACKAGE__->register_method(
89 method => 'ranged_billing_types',
90 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
92 sub ranged_billing_types {
93 my($self, $conn, $auth, $org_id, $depth) = @_;
94 my $e = new_editor(authtoken => $auth);
95 return $e->event unless $e->checkauth;
96 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
97 return $e->search_config_billing_type(
98 {owner => $U->get_org_full_path($org_id, $depth)});
103 # ------------------------------------------------------------------------
104 # Returns an array of {circ, record} hashes checked out by the user.
105 # ------------------------------------------------------------------------
106 __PACKAGE__->register_method(
107 method => "checkouts_by_user",
108 api_name => "open-ils.circ.actor.user.checked_out",
110 NOTES => <<" NOTES");
111 Returns a list of open circulations as a pile of objects. Each object
112 contains the relevant copy, circ, and record
115 sub checkouts_by_user {
116 my($self, $client, $auth, $user_id) = @_;
118 my $e = new_editor(authtoken=>$auth);
119 return $e->event unless $e->checkauth;
121 my $circ_ids = $e->search_action_circulation(
123 checkin_time => undef,
125 {stop_fines => undef},
126 {stop_fines => ['MAXFINES','LONGOVERDUE']}
132 for my $id (@$circ_ids) {
133 my $circ = $e->retrieve_action_circulation([
137 circ => ['target_copy'],
138 acp => ['call_number'],
144 # un-flesh for consistency
145 my $c = $circ->target_copy;
146 $circ->target_copy($c->id);
148 my $cn = $c->call_number;
149 $c->call_number($cn->id);
157 record => $U->record_to_mvr($t)
167 __PACKAGE__->register_method(
168 method => "checkouts_by_user_slim",
169 api_name => "open-ils.circ.actor.user.checked_out.slim",
170 NOTES => <<" NOTES");
171 Returns a list of open circulation objects
175 sub checkouts_by_user_slim {
176 my( $self, $client, $user_session, $user_id ) = @_;
178 my( $requestor, $target, $copy, $record, $evt );
180 ( $requestor, $target, $evt ) =
181 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
184 $logger->debug( 'User ' . $requestor->id .
185 " retrieving checked out items for user " . $target->id );
187 # XXX Make the call correct..
188 return $apputils->simplereq(
190 "open-ils.cstore.direct.action.open_circulation.search.atomic",
191 { usr => $target->id, checkin_time => undef } );
192 # { usr => $target->id } );
196 __PACKAGE__->register_method(
197 method => "checkouts_by_user_opac",
198 api_name => "open-ils.circ.actor.user.checked_out.opac",);
201 sub checkouts_by_user_opac {
202 my( $self, $client, $auth, $user_id ) = @_;
204 my $e = new_editor( authtoken => $auth );
205 return $e->event unless $e->checkauth;
206 $user_id ||= $e->requestor->id;
207 return $e->event unless
208 my $patron = $e->retrieve_actor_user($user_id);
211 my $search = {usr => $user_id, stop_fines => undef};
213 if( $user_id ne $e->requestor->id ) {
214 $data = $e->search_action_circulation(
215 $search, {checkperm=>1, permorg=>$patron->home_ou})
219 $data = $e->search_action_circulation($search);
226 __PACKAGE__->register_method(
227 method => "title_from_transaction",
228 api_name => "open-ils.circ.circ_transaction.find_title",
229 NOTES => <<" NOTES");
230 Returns a mods object for the title that is linked to from the
231 copy from the hold that created the given transaction
234 sub title_from_transaction {
235 my( $self, $client, $login_session, $transactionid ) = @_;
237 my( $user, $circ, $title, $evt );
239 ( $user, $evt ) = $apputils->checkses( $login_session );
242 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
245 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
248 return $apputils->record_to_mvr($title);
251 __PACKAGE__->register_method(
252 method => "staff_age_to_lost",
253 api_name => "open-ils.circ.circulation.age_to_lost",
256 This fires a circ.staff_age_to_lost Action-Trigger event against all
257 overdue circulations in scope of the specified context library and
258 user profile, which effectively marks the associated items as Lost.
259 This is likely to be done at the end of a semester in an academic
262 @param args : circ_lib, user_profile
266 sub staff_age_to_lost {
267 my( $self, $conn, $auth, $args ) = @_;
268 my $e = new_editor(authtoken=>$auth);
269 return $e->event unless $e->checkauth;
270 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
272 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
273 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
275 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
277 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
278 my $hook = 'circ.staff_age_to_lost';
279 my $context_org = 'circ_lib';
280 my $opt_granularity = undef;
282 "checkin_time" => undef,
283 "due_date" => { "<" => "now" },
285 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
286 { "stop_fines" => undef }
290 "select" => {"au" => ["id"]},
293 "profile" => $profiles,
294 "id" => { "=" => {"+circ" => "usr"} }
298 "select" => {"aou" => ["id"]},
302 {"id" => { "=" => {"+circ" => "circ_lib"} }},
309 my $req_timeout = 10800;
310 my $chunk_size = 100;
313 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
314 my @event_ids; my @chunked_ids;
315 while (my $resp = $req->recv(timeout => $req_timeout)) {
316 push(@event_ids, $resp->content);
317 push(@chunked_ids, $resp->content);
318 if (scalar(@chunked_ids) > $chunk_size) {
319 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
323 if (scalar(@chunked_ids) > 0) {
324 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
328 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
329 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
330 } elsif($req->complete) {
331 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
332 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
334 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
335 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
342 __PACKAGE__->register_method(
343 method => "new_set_circ_lost",
344 api_name => "open-ils.circ.circulation.set_lost",
346 Sets the copy and related open circulation to lost
348 @param args : barcode
353 # ---------------------------------------------------------------------
354 # Sets a circulation to lost. updates copy status to lost
355 # applies copy and/or prcoessing fees depending on org settings
356 # ---------------------------------------------------------------------
357 sub new_set_circ_lost {
358 my( $self, $conn, $auth, $args ) = @_;
360 my $e = new_editor(authtoken=>$auth, xact=>1);
361 return $e->die_event unless $e->checkauth;
363 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
364 or return $e->die_event;
366 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
373 __PACKAGE__->register_method(
374 method => "update_latest_inventory",
375 api_name => "open-ils.circ.circulation.update_latest_inventory");
377 sub update_latest_inventory {
378 my( $self, $conn, $auth, $args ) = @_;
379 my $e = new_editor(authtoken=>$auth, xact=>1);
380 return $e->die_event unless $e->checkauth;
382 my $copies = $$args{copy_list};
383 foreach my $copyid (@$copies) {
384 my $copy = $e->retrieve_asset_copy($copyid);
385 my $alci = $e->search_asset_latest_inventory({copy => $copyid})->[0];
388 $alci->inventory_date('now');
389 $alci->inventory_workstation($e->requestor->wsid);
390 $e->update_asset_latest_inventory($alci) or return $e->die_event;
392 my $alci = Fieldmapper::asset::latest_inventory->new;
393 $alci->inventory_date('now');
394 $alci->inventory_workstation($e->requestor->wsid);
395 $alci->copy($copy->id);
396 $e->create_asset_latest_inventory($alci) or return $e->die_event;
399 $copy->latest_inventory($alci);
405 __PACKAGE__->register_method(
406 method => "set_circ_claims_returned",
407 api_name => "open-ils.circ.circulation.set_claims_returned",
409 desc => q/Sets the circ for a given item as claims returned
410 If a backdate is provided, overdue fines will be voided
411 back to the backdate/,
413 {desc => 'Authentication token', type => 'string'},
414 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
416 return => {desc => q/1 on success, failure event on error, and
417 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
418 configured claims return maximum/}
422 __PACKAGE__->register_method(
423 method => "set_circ_claims_returned",
424 api_name => "open-ils.circ.circulation.set_claims_returned.override",
426 desc => q/This adds support for overrideing the configured max
427 claims returned amount.
428 @see open-ils.circ.circulation.set_claims_returned./,
432 sub set_circ_claims_returned {
433 my( $self, $conn, $auth, $args, $oargs ) = @_;
435 my $e = new_editor(authtoken=>$auth, xact=>1);
436 return $e->die_event unless $e->checkauth;
438 $oargs = { all => 1 } unless defined $oargs;
440 my $barcode = $$args{barcode};
441 my $backdate = $$args{backdate};
443 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
444 or return $e->die_event;
446 my $circ = $e->search_action_circulation(
447 {checkin_time => undef, target_copy => $copy->id})->[0]
448 or return $e->die_event;
450 $backdate = $circ->due_date if $$args{use_due_date};
452 $logger->info("marking circ for item $barcode as claims returned".
453 (($backdate) ? " with backdate $backdate" : ''));
455 my $patron = $e->retrieve_actor_user($circ->usr);
456 my $max_count = $U->ou_ancestor_setting_value(
457 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
459 # If the patron has too instances of many claims returned,
460 # require an override to continue. A configured max of
461 # 0 means all attempts require an override
462 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
464 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
466 # see if we're allowed to override
467 return $e->die_event unless
468 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
472 # exit early and return the max claims return event
474 return OpenILS::Event->new(
475 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
477 patron_count => $patron->claims_returned_count,
478 max_count => $max_count
484 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
485 or return $e->die_event;
487 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
488 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
491 $backdate = clean_ISO8601($backdate);
493 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
494 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
495 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
497 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
498 $backdate = clean_ISO8601($backdate);
500 # make it look like the circ stopped at the cliams returned time
501 $circ->stop_fines_time($backdate);
502 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
506 $e->update_action_circulation($circ) or return $e->die_event;
508 # see if there is a configured post-claims-return copy status
509 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
510 $copy->status($stat);
511 $copy->edit_date('now');
512 $copy->editor($e->requestor->id);
513 $e->update_asset_copy($copy) or return $e->die_event;
516 # Check if the copy circ lib wants lost fees voided on claims
518 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
519 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
530 # Check if the copy circ lib wants lost processing fees voided on
532 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
533 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
544 # Check if the copy circ lib wants longoverdue fees voided on claims
546 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
547 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
558 # Check if the copy circ lib wants longoverdue processing fees voided on
560 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
561 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
572 # Now that all data has been munged, do a no-op update of
573 # the patron to force a change of the last_xact_id value.
574 $e->update_actor_user($e->retrieve_actor_user($circ->usr))
575 or return $e->die_event;
582 __PACKAGE__->register_method(
583 method => "post_checkin_backdate_circ",
584 api_name => "open-ils.circ.post_checkin_backdate",
586 desc => q/Back-date an already checked in circulation/,
588 {desc => 'Authentication token', type => 'string'},
589 {desc => 'Circ ID', type => 'number'},
590 {desc => 'ISO8601 backdate', type => 'string'},
592 return => {desc => q/1 on success, failure event on error/}
596 __PACKAGE__->register_method(
597 method => "post_checkin_backdate_circ",
598 api_name => "open-ils.circ.post_checkin_backdate.batch",
601 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
603 {desc => 'Authentication token', type => 'string'},
604 {desc => 'List of Circ ID', type => 'array'},
605 {desc => 'ISO8601 backdate', type => 'string'},
607 return => {desc => q/Set of: 1 on success, failure event on error/}
612 sub post_checkin_backdate_circ {
613 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
614 my $e = new_editor(authtoken=>$auth);
615 return $e->die_event unless $e->checkauth;
616 if($self->api_name =~ /batch/) {
617 foreach my $c (@$circ_id) {
618 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
621 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
629 sub post_checkin_backdate_circ_impl {
630 my($e, $circ_id, $backdate) = @_;
634 my $circ = $e->retrieve_action_circulation($circ_id)
635 or return $e->die_event;
637 # anyone with checkin perms can backdate (more restrictive?)
638 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
640 # don't allow back-dating an open circulation
641 return OpenILS::Event->new('BAD_PARAMS') unless
642 $backdate and $circ->checkin_time;
644 # update the checkin and stop_fines times to reflect the new backdate
645 $circ->stop_fines_time(clean_ISO8601($backdate));
646 $circ->checkin_time(clean_ISO8601($backdate));
647 $e->update_action_circulation($circ) or return $e->die_event;
649 # now void the overdues "erased" by the back-dating
650 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
653 # If the circ was closed before and the balance owned !=0, re-open the transaction
654 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
663 __PACKAGE__->register_method (
664 method => 'set_circ_due_date',
665 api_name => 'open-ils.circ.circulation.due_date.update',
667 Updates the due_date on the given circ
669 @param circid The id of the circ to update
670 @param date The timestamp of the new due date
674 sub set_circ_due_date {
675 my( $self, $conn, $auth, $circ_id, $date ) = @_;
677 my $e = new_editor(xact=>1, authtoken=>$auth);
678 return $e->die_event unless $e->checkauth;
679 my $circ = $e->retrieve_action_circulation($circ_id)
680 or return $e->die_event;
682 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
683 $date = clean_ISO8601($date);
685 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
686 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
687 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
689 # since the new date may be coming in as UTC, convert it
690 # to the same time zone as the original due date so that
691 # ->ymd is more likely to yield the expected results
692 $new_date->set_time_zone($original_date->time_zone());
693 $date = clean_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
696 $circ->due_date($date);
697 $e->update_action_circulation($circ) or return $e->die_event;
704 __PACKAGE__->register_method(
705 method => "create_in_house_use",
706 api_name => 'open-ils.circ.in_house_use.create',
708 Creates an in-house use action.
709 @param $authtoken The login session key
710 @param params A hash of params including
711 'location' The org unit id where the in-house use occurs
712 'copyid' The copy in question
713 'count' The number of in-house uses to apply to this copy
714 @return An array of id's representing the id's of the newly created
715 in-house use objects or an event on an error
718 __PACKAGE__->register_method(
719 method => "create_in_house_use",
720 api_name => 'open-ils.circ.non_cat_in_house_use.create',
724 sub create_in_house_use {
725 my( $self, $client, $auth, $params ) = @_;
728 my $org = $params->{location};
729 my $copyid = $params->{copyid};
730 my $count = $params->{count} || 1;
731 my $nc_type = $params->{non_cat_type};
732 my $use_time = $params->{use_time} || 'now';
734 my $e = new_editor(xact=>1,authtoken=>$auth);
735 return $e->event unless $e->checkauth;
736 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
738 my $non_cat = 1 if $self->api_name =~ /non_cat/;
742 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
744 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
750 if( $use_time ne 'now' ) {
751 $use_time = clean_ISO8601($use_time);
752 $logger->debug("in_house_use setting use time to $use_time");
763 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
764 $ihu->item_type($nc_type);
765 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
766 $cmeth = "create_action_non_cat_in_house_use";
769 $ihu = Fieldmapper::action::in_house_use->new;
771 $method = 'open-ils.storage.direct.action.in_house_use.create';
772 $cmeth = "create_action_in_house_use";
775 $ihu->staff($e->requestor->id);
776 $ihu->workstation($e->requestor->wsid);
777 $ihu->org_unit($org);
778 $ihu->use_time($use_time);
780 $ihu = $e->$cmeth($ihu) or return $e->event;
781 push( @ids, $ihu->id );
792 __PACKAGE__->register_method(
793 method => "view_circs",
794 api_name => "open-ils.circ.copy_checkout_history.retrieve",
796 Retrieves the last X circs for a given copy
797 @param authtoken The login session key
798 @param copyid The copy to check
799 @param count How far to go back in the item history
800 @return An array of circ ids
803 # ----------------------------------------------------------------------
804 # Returns $count most recent circs. If count exceeds the configured
805 # max, use the configured max instead
806 # ----------------------------------------------------------------------
808 my( $self, $client, $authtoken, $copyid, $count ) = @_;
810 my $e = new_editor(authtoken => $authtoken);
811 return $e->event unless $e->checkauth;
813 my $copy = $e->retrieve_asset_copy([
816 flesh_fields => {acp => ['call_number']}
818 ]) or return $e->event;
820 return $e->event unless $e->allowed(
821 'VIEW_COPY_CHECKOUT_HISTORY',
822 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
823 $copy->circ_lib : $copy->call_number->owning_lib);
825 my $max_history = $U->ou_ancestor_setting_value(
826 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
828 if(defined $max_history) {
829 $count = $max_history unless defined $count and $count < $max_history;
831 $count = 4 unless defined $count;
834 return $e->search_action_all_circulation_slim([
835 {target_copy => $copyid},
836 {limit => $count, order_by => { aacs => "xact_start DESC" }}
841 __PACKAGE__->register_method(
842 method => "circ_count",
843 api_name => "open-ils.circ.circulation.count",
845 Returns the number of times the item has circulated
846 @param copyid The copy to check
850 my( $self, $client, $copyid ) = @_;
852 my $count = new_editor()->json_query({
861 where => {'+circbyyr' => {copy => $copyid}}
873 __PACKAGE__->register_method(
874 method => 'fetch_notes',
876 api_name => 'open-ils.circ.copy_note.retrieve.all',
878 Returns an array of copy note objects.
879 @param args A named hash of parameters including:
880 authtoken : Required if viewing non-public notes
881 itemid : The id of the item whose notes we want to retrieve
882 pub : True if all the caller wants are public notes
883 @return An array of note objects
886 __PACKAGE__->register_method(
887 method => 'fetch_notes',
888 api_name => 'open-ils.circ.call_number_note.retrieve.all',
889 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
891 __PACKAGE__->register_method(
892 method => 'fetch_notes',
893 api_name => 'open-ils.circ.title_note.retrieve.all',
894 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
897 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
899 my( $self, $connection, $args ) = @_;
901 my $id = $$args{itemid};
902 my $authtoken = $$args{authtoken};
905 if( $self->api_name =~ /copy/ ) {
907 return $U->cstorereq(
908 'open-ils.cstore.direct.asset.copy_note.search.atomic',
909 { owning_copy => $id, pub => 't' } );
911 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
913 return $U->cstorereq(
914 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
917 } elsif( $self->api_name =~ /call_number/ ) {
919 return $U->cstorereq(
920 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
921 { call_number => $id, pub => 't' } );
923 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
925 return $U->cstorereq(
926 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
929 } elsif( $self->api_name =~ /title/ ) {
931 return $U->cstorereq(
932 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
933 { record => $id, pub => 't' } );
935 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
937 return $U->cstorereq(
938 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
945 __PACKAGE__->register_method(
946 method => 'has_notes',
947 api_name => 'open-ils.circ.copy.has_notes');
948 __PACKAGE__->register_method(
949 method => 'has_notes',
950 api_name => 'open-ils.circ.call_number.has_notes');
951 __PACKAGE__->register_method(
952 method => 'has_notes',
953 api_name => 'open-ils.circ.title.has_notes');
957 my( $self, $conn, $authtoken, $id ) = @_;
958 my $editor = new_editor(authtoken => $authtoken);
959 return $editor->event unless $editor->checkauth;
961 my $n = $editor->search_asset_copy_note(
962 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
964 $n = $editor->search_asset_call_number_note(
965 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
967 $n = $editor->search_biblio_record_note(
968 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
975 __PACKAGE__->register_method(
976 method => 'create_copy_note',
977 api_name => 'open-ils.circ.copy_note.create',
979 Creates a new copy note
980 @param authtoken The login session key
981 @param note The note object to create
982 @return The id of the new note object
985 sub create_copy_note {
986 my( $self, $connection, $authtoken, $note ) = @_;
988 my $e = new_editor(xact=>1, authtoken=>$authtoken);
989 return $e->event unless $e->checkauth;
990 my $copy = $e->retrieve_asset_copy(
994 flesh_fields => { 'acp' => ['call_number'] }
999 return $e->event unless
1000 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
1002 $note->create_date('now');
1003 $note->creator($e->requestor->id);
1004 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1007 $e->create_asset_copy_note($note) or return $e->event;
1013 __PACKAGE__->register_method(
1014 method => 'delete_copy_note',
1015 api_name => 'open-ils.circ.copy_note.delete',
1017 Deletes an existing copy note
1018 @param authtoken The login session key
1019 @param noteid The id of the note to delete
1020 @return 1 on success - Event otherwise.
1022 sub delete_copy_note {
1023 my( $self, $conn, $authtoken, $noteid ) = @_;
1025 my $e = new_editor(xact=>1, authtoken=>$authtoken);
1026 return $e->die_event unless $e->checkauth;
1028 my $note = $e->retrieve_asset_copy_note([
1032 'acpn' => [ 'owning_copy' ],
1033 'acp' => [ 'call_number' ],
1036 ]) or return $e->die_event;
1038 if( $note->creator ne $e->requestor->id ) {
1039 return $e->die_event unless
1040 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1043 $e->delete_asset_copy_note($note) or return $e->die_event;
1048 __PACKAGE__->register_method(
1049 method => 'fetch_copy_tags',
1051 api_name => 'open-ils.circ.copy_tags.retrieve',
1053 Returns an array of publicly-visible copy tag objects.
1054 @param args A named hash of parameters including:
1055 copy_id : The id of the item whose notes we want to retrieve
1056 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1057 scope : top of org subtree whose copy tags we want to see
1058 depth : how far down to look for copy tags (optional)
1059 @return An array of copy tag objects
1061 __PACKAGE__->register_method(
1062 method => 'fetch_copy_tags',
1064 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1066 Returns an array of all copy tag objects.
1067 @param args A named hash of parameters including:
1068 authtoken : Required to view non-public notes
1069 copy_id : The id of the item whose notes we want to retrieve (optional)
1070 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1071 scope : top of org subtree whose copy tags we want to see
1072 depth : how far down to look for copy tags (optional)
1073 @return An array of copy tag objects
1076 sub fetch_copy_tags {
1077 my ($self, $conn, $args) = @_;
1079 my $org = $args->{scope};
1080 my $depth = $args->{depth};
1084 if ($self->api_name =~ /\.staff/) {
1085 my $authtoken = $args->{authtoken};
1086 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1087 $e = new_editor(authtoken => $args->{authtoken});
1088 return $e->event unless $e->checkauth;
1089 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1092 $filter->{pub} = 't';
1094 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1095 $filter->{'+acptcm'} = {
1096 copy => $args->{copy_id}
1099 # filter by owner of copy tag and depth
1100 $filter->{owner} = {
1102 select => {aou => [{
1104 transform => 'actor.org_unit_descendants',
1105 result_field => 'id',
1106 (defined($depth) ? ( params => [$depth] ) : ()),
1109 where => {id => $org}
1113 return $e->search_asset_copy_tag([$filter, {
1114 join => { acptcm => {} },
1116 flesh_fields => { acpt => ['tag_type'] }
1121 __PACKAGE__->register_method(
1122 method => 'age_hold_rules',
1123 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1126 sub age_hold_rules {
1127 my( $self, $conn ) = @_;
1128 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1133 __PACKAGE__->register_method(
1134 method => 'copy_details_barcode',
1136 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1137 sub copy_details_barcode {
1138 my( $self, $conn, $auth, $barcode ) = @_;
1139 my $e = new_editor();
1140 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1141 return $e->event unless $cid;
1142 return copy_details( $self, $conn, $auth, $cid );
1146 __PACKAGE__->register_method(
1147 method => 'copy_details',
1148 api_name => 'open-ils.circ.copy_details.retrieve');
1151 my( $self, $conn, $auth, $copy_id ) = @_;
1152 my $e = new_editor(authtoken=>$auth);
1153 return $e->event unless $e->checkauth;
1155 my $flesh = { flesh => 1 };
1157 my $copy = $e->retrieve_asset_copy(
1163 acp => ['call_number','parts','peer_record_maps','floating'],
1164 acn => ['record','prefix','suffix','label_class']
1167 ]) or return $e->event;
1170 # De-flesh the copy for backwards compatibility
1172 my $vol = $copy->call_number;
1174 $copy->call_number($vol->id);
1175 my $record = $vol->record;
1177 $vol->record($record->id);
1178 $mvr = $U->record_to_mvr($record);
1183 my $hold = $e->search_action_hold_request(
1185 current_copy => $copy_id,
1186 capture_time => { "!=" => undef },
1187 fulfillment_time => undef,
1188 cancel_time => undef,
1192 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1194 my $transit = $e->search_action_transit_copy(
1195 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1197 # find the most recent circulation for the requested copy,
1198 # be it active, completed, or aged.
1199 my $circ = $e->search_action_all_circulation_slim([
1200 { target_copy => $copy_id },
1206 'checkin_workstation',
1209 'recurring_fine_rule'
1212 order_by => { aacs => 'xact_start desc' },
1220 transit => $transit,
1230 __PACKAGE__->register_method(
1231 method => 'mark_item',
1232 api_name => 'open-ils.circ.mark_item_damaged',
1234 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1235 @param authtoken The login session key
1236 @param copy_id The ID of the copy to mark as damaged
1237 @return 1 on success - Event otherwise.
1240 __PACKAGE__->register_method(
1241 method => 'mark_item',
1242 api_name => 'open-ils.circ.mark_item_missing',
1244 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1245 @param authtoken The login session key
1246 @param copy_id The ID of the copy to mark as missing
1247 @return 1 on success - Event otherwise.
1250 __PACKAGE__->register_method(
1251 method => 'mark_item',
1252 api_name => 'open-ils.circ.mark_item_bindery',
1254 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1255 @param authtoken The login session key
1256 @param copy_id The ID of the copy to mark as bindery
1257 @return 1 on success - Event otherwise.
1260 __PACKAGE__->register_method(
1261 method => 'mark_item',
1262 api_name => 'open-ils.circ.mark_item_on_order',
1264 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1265 @param authtoken The login session key
1266 @param copy_id The ID of the copy to mark as on order
1267 @return 1 on success - Event otherwise.
1270 __PACKAGE__->register_method(
1271 method => 'mark_item',
1272 api_name => 'open-ils.circ.mark_item_ill',
1274 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1275 @param authtoken The login session key
1276 @param copy_id The ID of the copy to mark as inter-library loan
1277 @return 1 on success - Event otherwise.
1280 __PACKAGE__->register_method(
1281 method => 'mark_item',
1282 api_name => 'open-ils.circ.mark_item_cataloging',
1284 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1285 @param authtoken The login session key
1286 @param copy_id The ID of the copy to mark as cataloging
1287 @return 1 on success - Event otherwise.
1290 __PACKAGE__->register_method(
1291 method => 'mark_item',
1292 api_name => 'open-ils.circ.mark_item_reserves',
1294 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1295 @param authtoken The login session key
1296 @param copy_id The ID of the copy to mark as reserves
1297 @return 1 on success - Event otherwise.
1300 __PACKAGE__->register_method(
1301 method => 'mark_item',
1302 api_name => 'open-ils.circ.mark_item_discard',
1304 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1305 @param authtoken The login session key
1306 @param copy_id The ID of the copy to mark as discard
1307 @return 1 on success - Event otherwise.
1312 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1315 my $e = new_editor(authtoken=>$auth);
1316 return $e->die_event unless $e->checkauth;
1317 my $copy = $e->retrieve_asset_copy([
1319 {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
1320 or return $e->die_event;
1323 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1324 $copy->circ_lib : $copy->call_number->owning_lib;
1326 my $evt; # For later.
1327 my $perm = 'MARK_ITEM_MISSING';
1328 my $stat = OILS_COPY_STATUS_MISSING;
1330 if( $self->api_name =~ /damaged/ ) {
1331 $perm = 'MARK_ITEM_DAMAGED';
1332 $stat = OILS_COPY_STATUS_DAMAGED;
1333 } elsif ( $self->api_name =~ /bindery/ ) {
1334 $perm = 'MARK_ITEM_BINDERY';
1335 $stat = OILS_COPY_STATUS_BINDERY;
1336 } elsif ( $self->api_name =~ /on_order/ ) {
1337 $perm = 'MARK_ITEM_ON_ORDER';
1338 $stat = OILS_COPY_STATUS_ON_ORDER;
1339 } elsif ( $self->api_name =~ /ill/ ) {
1340 $perm = 'MARK_ITEM_ILL';
1341 $stat = OILS_COPY_STATUS_ILL;
1342 } elsif ( $self->api_name =~ /cataloging/ ) {
1343 $perm = 'MARK_ITEM_CATALOGING';
1344 $stat = OILS_COPY_STATUS_CATALOGING;
1345 } elsif ( $self->api_name =~ /reserves/ ) {
1346 $perm = 'MARK_ITEM_RESERVES';
1347 $stat = OILS_COPY_STATUS_RESERVES;
1348 } elsif ( $self->api_name =~ /discard/ ) {
1349 $perm = 'MARK_ITEM_DISCARD';
1350 $stat = OILS_COPY_STATUS_DISCARD;
1353 # caller may proceed if either perm is allowed
1354 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1356 # Copy status checks.
1357 if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
1358 # Items must be checked in before any attempt is made to change its status.
1359 if ($args->{handle_checkin}) {
1360 $evt = try_checkin($auth, $copy_id);
1362 $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
1364 } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
1365 # Items in transit need to have the transit aborted before being marked.
1366 if ($args->{handle_transit}) {
1367 $evt = try_abort_transit($auth, $copy_id);
1369 $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
1371 } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
1372 # Items with restrict_copy_delete status require the
1373 # COPY_DELETE_WARNING.override permission to be marked for
1375 if ($args->{handle_copy_delete_warning}) {
1376 $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
1378 $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
1381 return $evt if $evt;
1383 # Retrieving holds for later use.
1384 my $holds = $e->search_action_hold_request([
1386 current_copy => $copy->id,
1387 fulfillment_time => undef,
1388 cancel_time => undef,
1390 {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
1393 # Throw event if attempting to mark discard the only copy to fill a hold.
1394 if ($self->api_name =~ /discard/) {
1395 if (!$args->{handle_last_hold_copy}) {
1396 for my $hold (@$holds) {
1397 my $eligible = $hold->eligible_copies();
1398 if (!defined($eligible) || scalar(@{$eligible}) < 2) {
1399 $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
1405 return $evt if $evt;
1407 # Things below here require a transaction and there is nothing left to interfere with it.
1410 # Handle extra mark damaged charges, etc.
1411 if ($self->api_name =~ /damaged/) {
1412 $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1413 return $evt if $evt;
1417 $copy->status($stat);
1418 $copy->edit_date('now');
1419 $copy->editor($e->requestor->id);
1421 $e->update_asset_copy($copy) or return $e->die_event;
1425 if( $self->api_name =~ /damaged/ ) {
1426 # now that we've committed the changes, create related A/T events
1427 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1428 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1431 $logger->debug("resetting holds that target the marked copy");
1432 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1438 my($auth, $copy_id) = @_;
1440 my $checkin = $U->simplereq(
1442 'open-ils.circ.checkin.override',
1444 copy_id => $copy_id,
1448 if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1450 my $evt_code = $checkin->[0]->{textcode};
1451 $logger->info("try_checkin() received event: $evt_code");
1453 if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1454 $logger->info('try_checkin() successful checkin');
1457 $logger->warn('try_checkin() un-successful checkin');
1462 sub try_abort_transit {
1463 my ($auth, $copy_id) = @_;
1465 my $abort = $U->simplereq(
1467 'open-ils.circ.transit.abort',
1468 $auth, {copyid => $copy_id}
1470 # Above returns 1 or an event.
1471 return $abort if (ref $abort);
1475 sub handle_mark_damaged {
1476 my($e, $copy, $owning_lib, $args) = @_;
1478 my $apply = $args->{apply_fines} || '';
1479 return undef if $apply eq 'noapply';
1481 my $new_amount = $args->{override_amount};
1482 my $new_btype = $args->{override_btype};
1483 my $new_note = $args->{override_note};
1485 # grab the last circulation
1486 my $circ = $e->search_action_circulation([
1487 { target_copy => $copy->id},
1489 order_by => {circ => "xact_start DESC"},
1491 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1495 return undef unless $circ;
1497 my $charge_price = $U->ou_ancestor_setting_value(
1498 $owning_lib, 'circ.charge_on_damaged', $e);
1500 my $proc_fee = $U->ou_ancestor_setting_value(
1501 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1503 my $void_overdue = $U->ou_ancestor_setting_value(
1504 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1506 return undef unless $charge_price or $proc_fee;
1508 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1509 my $total = $copy_price + $proc_fee;
1513 if($new_amount and $new_btype) {
1515 # Allow staff to override the amount to charge for a damaged item
1516 # Consider the case where the item is only partially damaged
1517 # This value is meant to take the place of the item price and
1518 # optional processing fee.
1520 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1521 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1522 return $evt if $evt;
1526 if($charge_price and $copy_price) {
1527 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1528 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1529 return $evt if $evt;
1533 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1534 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1535 return $evt if $evt;
1539 # the assumption is that you would not void the overdues unless you
1540 # were also charging for the item and/or applying a processing fee
1542 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1543 return $evt if $evt;
1546 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1547 return $evt if $evt;
1549 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1550 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1552 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1553 return $evt2 if $evt2;
1556 return OpenILS::Event->new('DAMAGE_CHARGE',
1567 # ----------------------------------------------------------------------
1568 __PACKAGE__->register_method(
1569 method => 'mark_item_missing_pieces',
1570 api_name => 'open-ils.circ.mark_item_missing_pieces',
1572 Changes the status of a copy to "damaged" or to a custom status based on the
1573 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1575 @param authtoken The login session key
1576 @param copy_id The ID of the copy to mark as damaged
1577 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1581 sub mark_item_missing_pieces {
1582 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1583 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1584 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1586 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1587 return $e2->die_event unless $e2->checkauth;
1590 my $copy = $e2->retrieve_asset_copy([
1592 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1593 or return $e2->die_event;
1596 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1597 $copy->circ_lib : $copy->call_number->owning_lib;
1599 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1601 #### grab the last circulation
1602 my $circ = $e2->search_action_circulation([
1603 { target_copy => $copy->id},
1605 order_by => {circ => "xact_start DESC"}
1610 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1612 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1615 my $holds = $e2->search_action_hold_request(
1617 current_copy => $copy->id,
1618 fulfillment_time => undef,
1619 cancel_time => undef,
1623 $logger->debug("resetting holds that target the marked copy");
1624 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1627 if (! $e2->commit) {
1628 return $e2->die_event;
1631 my $e = new_editor(authtoken=>$auth, xact =>1);
1632 return $e->die_event unless $e->checkauth;
1634 if (! $circ->checkin_time) { # if circ active, attempt renew
1635 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1636 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1637 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1638 $circ = $res->[0]->{payload}{'circ'};
1639 $circ->target_copy( $copy->id );
1640 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1642 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1647 'copy_id'=>$circ->target_copy,
1648 'patron_id'=>$circ->usr,
1649 'skip_deposit_fee'=>1,
1650 'skip_rental_fee'=>1
1653 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1655 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1656 $e, $copy, $e->requestor, 1 );
1658 if ($hold) { # needed for hold? then due now
1660 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1661 my $due_date = DateTime->now(time_zone => 'local');
1662 $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
1664 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1668 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1669 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1670 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1671 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1672 $circ = $res->[0]->{payload}{'circ'};
1674 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1680 ### Update the item status
1682 my $custom_stat = $U->ou_ancestor_setting_value(
1683 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1684 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1686 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1687 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1689 $copy->status($stat);
1690 $copy->edit_date('now');
1691 $copy->editor($e->requestor->id);
1693 $e->update_asset_copy($copy) or return $e->die_event;
1697 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1698 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1700 return OpenILS::Event->new('SUCCESS',
1704 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1705 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1710 return $e->die_event;
1718 # ----------------------------------------------------------------------
1719 __PACKAGE__->register_method(
1720 method => 'magic_fetch',
1721 api_name => 'open-ils.agent.fetch'
1724 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1727 my( $self, $conn, $auth, $args ) = @_;
1728 my $e = new_editor( authtoken => $auth );
1729 return $e->event unless $e->checkauth;
1731 my $hint = $$args{hint};
1732 my $id = $$args{id};
1734 # Is the call allowed to fetch this type of object?
1735 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1737 # Find the class the implements the given hint
1738 my ($class) = grep {
1739 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1741 $class =~ s/Fieldmapper:://og;
1742 $class =~ s/::/_/og;
1743 my $method = "retrieve_$class";
1745 my $obj = $e->$method($id) or return $e->event;
1748 # ----------------------------------------------------------------------
1751 __PACKAGE__->register_method(
1752 method => "fleshed_circ_retrieve",
1754 api_name => "open-ils.circ.fleshed.retrieve",);
1756 sub fleshed_circ_retrieve {
1757 my( $self, $client, $id ) = @_;
1758 my $e = new_editor();
1759 my $circ = $e->retrieve_action_circulation(
1765 circ => [ qw/ target_copy / ],
1766 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1767 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1768 acn => [ qw/ record / ],
1772 ) or return $e->event;
1774 my $copy = $circ->target_copy;
1775 my $vol = $copy->call_number;
1776 my $rec = $circ->target_copy->call_number->record;
1778 $vol->record($rec->id);
1779 $copy->call_number($vol->id);
1780 $circ->target_copy($copy->id);
1784 if( $rec->id == OILS_PRECAT_RECORD ) {
1788 $mvr = $U->record_to_mvr($rec);
1789 $rec->marc(''); # drop the bulky marc data
1803 __PACKAGE__->register_method(
1804 method => "test_batch_circ_events",
1805 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1808 # method for testing the behavior of a given event definition
1809 sub test_batch_circ_events {
1810 my($self, $conn, $auth, $event_def, $barcode) = @_;
1812 my $e = new_editor(authtoken => $auth);
1813 return $e->event unless $e->checkauth;
1814 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1816 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1817 or return $e->event;
1819 my $circ = $e->search_action_circulation(
1820 {target_copy => $copy->id, checkin_time => undef})->[0]
1821 or return $e->event;
1823 return undef unless $circ;
1825 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1829 __PACKAGE__->register_method(
1830 method => "fire_circ_events",
1831 api_name => "open-ils.circ.fire_circ_trigger_events",
1833 General event def runner for circ objects. If no event def ID
1834 is provided, the hook will be used to find the best event_def
1835 match based on the context org unit
1839 __PACKAGE__->register_method(
1840 method => "fire_circ_events",
1841 api_name => "open-ils.circ.fire_hold_trigger_events",
1843 General event def runner for hold objects. If no event def ID
1844 is provided, the hook will be used to find the best event_def
1845 match based on the context org unit
1849 __PACKAGE__->register_method(
1850 method => "fire_circ_events",
1851 api_name => "open-ils.circ.fire_user_trigger_events",
1853 General event def runner for user objects. If no event def ID
1854 is provided, the hook will be used to find the best event_def
1855 match based on the context org unit
1860 sub fire_circ_events {
1861 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1863 my $e = new_editor(authtoken => $auth, xact => 1);
1864 return $e->event unless $e->checkauth;
1868 if($self->api_name =~ /hold/) {
1869 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1870 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1871 } elsif($self->api_name =~ /user/) {
1872 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1873 $targets = $e->batch_retrieve_actor_user($target_ids);
1875 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1876 $targets = $e->batch_retrieve_action_circulation($target_ids);
1878 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1879 # simply making this method authoritative because of weirdness
1880 # with transaction handling in A/T code that causes rollback
1881 # failure down the line if handling many targets
1883 return undef unless @$targets;
1884 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1887 __PACKAGE__->register_method(
1888 method => "user_payments_list",
1889 api_name => "open-ils.circ.user_payments.filtered.batch",
1892 desc => q/Returns a fleshed, date-limited set of all payments a user
1893 has made. By default, ordered by payment date. Optionally
1894 ordered by other columns in the top-level "mp" object/,
1896 {desc => 'Authentication token', type => 'string'},
1897 {desc => 'User ID', type => 'number'},
1898 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1900 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1901 and the related fully-realized payment object (e.g money.cash_payment)/}
1905 sub user_payments_list {
1906 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1908 my $e = new_editor(authtoken => $auth);
1909 return $e->event unless $e->checkauth;
1911 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1912 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1914 $order_by ||= ['payment_ts'];
1916 # all payments by user, between start_date and end_date
1917 my $payments = $e->json_query({
1918 select => {mp => ['id']},
1922 fkey => 'xact', field => 'id'}
1926 '+mbt' => {usr => $user_id},
1927 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1929 order_by => {mp => $order_by}
1932 for my $payment_id (@$payments) {
1933 my $payment = $e->retrieve_money_payment([
1941 'credit_card_payment',
1942 'debit_card_payment',
1957 $conn->respond($payment);
1964 __PACKAGE__->register_method(
1965 method => "retrieve_circ_chain",
1966 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1969 desc => q/Given a circulation, this returns all circulation objects
1970 that are part of the same chain of renewals./,
1972 {desc => 'Authentication token', type => 'string'},
1973 {desc => 'Circ ID', type => 'number'},
1975 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1979 __PACKAGE__->register_method(
1980 method => "retrieve_circ_chain",
1981 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1983 desc => q/Given a circulation, this returns a summary of the circulation objects
1984 that are part of the same chain of renewals./,
1986 {desc => 'Authentication token', type => 'string'},
1987 {desc => 'Circ ID', type => 'number'},
1989 return => {desc => q/Circulation Chain Summary/}
1993 sub retrieve_circ_chain {
1994 my($self, $conn, $auth, $circ_id) = @_;
1996 my $e = new_editor(authtoken => $auth);
1997 return $e->event unless $e->checkauth;
1998 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2000 if($self->api_name =~ /summary/) {
2001 return $U->create_circ_chain_summary($e, $circ_id);
2005 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
2007 for my $circ_info (@$chain) {
2008 my $circ = Fieldmapper::action::all_circulation_slim->new;
2009 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2010 $conn->respond($circ);
2017 __PACKAGE__->register_method(
2018 method => "retrieve_prev_circ_chain",
2019 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
2022 desc => q/Given a circulation, this returns all circulation objects
2023 that are part of the previous chain of renewals./,
2025 {desc => 'Authentication token', type => 'string'},
2026 {desc => 'Circ ID', type => 'number'},
2028 return => {desc => q/List of circ objects, orderd by oldest circ first/}
2032 __PACKAGE__->register_method(
2033 method => "retrieve_prev_circ_chain",
2034 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
2036 desc => q/Given a circulation, this returns a summary of the circulation objects
2037 that are part of the previous chain of renewals./,
2039 {desc => 'Authentication token', type => 'string'},
2040 {desc => 'Circ ID', type => 'number'},
2042 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
2046 sub retrieve_prev_circ_chain {
2047 my($self, $conn, $auth, $circ_id) = @_;
2049 my $e = new_editor(authtoken => $auth);
2050 return $e->event unless $e->checkauth;
2051 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2054 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
2056 my $prev_circ = $e->search_action_all_circulation_slim([
2057 { target_copy => $first_circ->{target_copy},
2058 xact_start => {'<' => $first_circ->{xact_start}}
2067 order_by => { aacs => 'xact_start desc' },
2072 return undef unless $prev_circ;
2074 my $chain_usr = $prev_circ->usr; # note: may be undef
2076 if ($self->api_name =~ /summary/) {
2077 my $sum = $e->json_query({
2079 'action.summarize_all_circ_chain',
2084 my $summary = Fieldmapper::action::circ_chain_summary->new;
2085 $summary->$_($sum->{$_}) for keys %$sum;
2087 return {summary => $summary, usr => $chain_usr};
2091 my $chain = $e->json_query(
2092 {from => ['action.all_circ_chain', $prev_circ->id]});
2094 for my $circ_info (@$chain) {
2095 my $circ = Fieldmapper::action::all_circulation_slim->new;
2096 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2097 $conn->respond($circ);
2104 __PACKAGE__->register_method(
2105 method => "get_copy_due_date",
2106 api_name => "open-ils.circ.copy.due_date.retrieve",
2109 Given a copy ID, returns the due date for the copy if it's
2110 currently circulating. Otherwise, returns null. Note, this is a public
2111 method requiring no authentication. Only the due date is exposed.
2114 {desc => 'Copy ID', type => 'number'}
2116 return => {desc => q/
2117 Due date (ISO date stamp) if the copy is circulating, null otherwise.
2122 sub get_copy_due_date {
2123 my($self, $conn, $copy_id) = @_;
2124 my $e = new_editor();
2126 my $circ = $e->json_query({
2127 select => {circ => ['due_date']},
2130 target_copy => $copy_id,
2131 checkin_time => undef,
2133 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2134 {stop_fines => undef}
2138 })->[0] or return undef;
2140 return $circ->{due_date};
2147 # {"select":{"acp":["id"],"circ":[{"aggregate":true,"transform":"count","alias":"count","column":"id"}]},"from":{"acp":{"circ":{"field":"target_copy","fkey":"id","type":"left"},"acn"{"field":"id","fkey":"call_number"}}},"where":{"+acn":{"record":200057}}