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 OpenSRF::Utils 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_last_copy_inventory",
375 api_name => "open-ils.circ.circulation.update_last_copy_inventory");
377 sub update_last_copy_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_last_copy_inventory({copy => $copyid})->[0];
388 $alci->inventory_date('now');
389 $alci->inventory_workstation($e->requestor->wsid);
390 $e->update_asset_last_copy_inventory($alci) or return $e->die_event;
392 my $alci = Fieldmapper::asset::last_copy_inventory->new;
393 $alci->inventory_date('now');
394 $alci->inventory_workstation($e->requestor->wsid);
395 $alci->copy($copy->id);
396 $e->create_asset_last_copy_inventory($alci) or return $e->die_event;
399 $copy->last_copy_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 = cleanse_ISO8601($backdate);
493 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_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 = cleanse_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(cleanse_ISO8601($backdate));
646 $circ->checkin_time(cleanse_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 = cleanse_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(cleanse_ISO8601($circ->due_date));
687 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
688 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
691 $circ->due_date($date);
692 $e->update_action_circulation($circ) or return $e->die_event;
699 __PACKAGE__->register_method(
700 method => "create_in_house_use",
701 api_name => 'open-ils.circ.in_house_use.create',
703 Creates an in-house use action.
704 @param $authtoken The login session key
705 @param params A hash of params including
706 'location' The org unit id where the in-house use occurs
707 'copyid' The copy in question
708 'count' The number of in-house uses to apply to this copy
709 @return An array of id's representing the id's of the newly created
710 in-house use objects or an event on an error
713 __PACKAGE__->register_method(
714 method => "create_in_house_use",
715 api_name => 'open-ils.circ.non_cat_in_house_use.create',
719 sub create_in_house_use {
720 my( $self, $client, $auth, $params ) = @_;
723 my $org = $params->{location};
724 my $copyid = $params->{copyid};
725 my $count = $params->{count} || 1;
726 my $nc_type = $params->{non_cat_type};
727 my $use_time = $params->{use_time} || 'now';
729 my $e = new_editor(xact=>1,authtoken=>$auth);
730 return $e->event unless $e->checkauth;
731 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
733 my $non_cat = 1 if $self->api_name =~ /non_cat/;
737 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
739 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
745 if( $use_time ne 'now' ) {
746 $use_time = cleanse_ISO8601($use_time);
747 $logger->debug("in_house_use setting use time to $use_time");
758 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
759 $ihu->item_type($nc_type);
760 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
761 $cmeth = "create_action_non_cat_in_house_use";
764 $ihu = Fieldmapper::action::in_house_use->new;
766 $method = 'open-ils.storage.direct.action.in_house_use.create';
767 $cmeth = "create_action_in_house_use";
770 $ihu->staff($e->requestor->id);
771 $ihu->org_unit($org);
772 $ihu->use_time($use_time);
774 $ihu = $e->$cmeth($ihu) or return $e->event;
775 push( @ids, $ihu->id );
786 __PACKAGE__->register_method(
787 method => "view_circs",
788 api_name => "open-ils.circ.copy_checkout_history.retrieve",
790 Retrieves the last X circs for a given copy
791 @param authtoken The login session key
792 @param copyid The copy to check
793 @param count How far to go back in the item history
794 @return An array of circ ids
797 # ----------------------------------------------------------------------
798 # Returns $count most recent circs. If count exceeds the configured
799 # max, use the configured max instead
800 # ----------------------------------------------------------------------
802 my( $self, $client, $authtoken, $copyid, $count ) = @_;
804 my $e = new_editor(authtoken => $authtoken);
805 return $e->event unless $e->checkauth;
807 my $copy = $e->retrieve_asset_copy([
810 flesh_fields => {acp => ['call_number']}
812 ]) or return $e->event;
814 return $e->event unless $e->allowed(
815 'VIEW_COPY_CHECKOUT_HISTORY',
816 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
817 $copy->circ_lib : $copy->call_number->owning_lib);
819 my $max_history = $U->ou_ancestor_setting_value(
820 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
822 if(defined $max_history) {
823 $count = $max_history unless defined $count and $count < $max_history;
825 $count = 4 unless defined $count;
828 return $e->search_action_all_circulation_slim([
829 {target_copy => $copyid},
830 {limit => $count, order_by => { aacs => "xact_start DESC" }}
835 __PACKAGE__->register_method(
836 method => "circ_count",
837 api_name => "open-ils.circ.circulation.count",
839 Returns the number of times the item has circulated
840 @param copyid The copy to check
844 my( $self, $client, $copyid ) = @_;
846 my $count = new_editor()->json_query({
855 where => {'+circbyyr' => {copy => $copyid}}
867 __PACKAGE__->register_method(
868 method => 'fetch_notes',
870 api_name => 'open-ils.circ.copy_note.retrieve.all',
872 Returns an array of copy note objects.
873 @param args A named hash of parameters including:
874 authtoken : Required if viewing non-public notes
875 itemid : The id of the item whose notes we want to retrieve
876 pub : True if all the caller wants are public notes
877 @return An array of note objects
880 __PACKAGE__->register_method(
881 method => 'fetch_notes',
882 api_name => 'open-ils.circ.call_number_note.retrieve.all',
883 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
885 __PACKAGE__->register_method(
886 method => 'fetch_notes',
887 api_name => 'open-ils.circ.title_note.retrieve.all',
888 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
891 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
893 my( $self, $connection, $args ) = @_;
895 my $id = $$args{itemid};
896 my $authtoken = $$args{authtoken};
899 if( $self->api_name =~ /copy/ ) {
901 return $U->cstorereq(
902 'open-ils.cstore.direct.asset.copy_note.search.atomic',
903 { owning_copy => $id, pub => 't' } );
905 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
907 return $U->cstorereq(
908 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
911 } elsif( $self->api_name =~ /call_number/ ) {
913 return $U->cstorereq(
914 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
915 { call_number => $id, pub => 't' } );
917 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
919 return $U->cstorereq(
920 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
923 } elsif( $self->api_name =~ /title/ ) {
925 return $U->cstorereq(
926 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
927 { record => $id, pub => 't' } );
929 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
931 return $U->cstorereq(
932 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
939 __PACKAGE__->register_method(
940 method => 'has_notes',
941 api_name => 'open-ils.circ.copy.has_notes');
942 __PACKAGE__->register_method(
943 method => 'has_notes',
944 api_name => 'open-ils.circ.call_number.has_notes');
945 __PACKAGE__->register_method(
946 method => 'has_notes',
947 api_name => 'open-ils.circ.title.has_notes');
951 my( $self, $conn, $authtoken, $id ) = @_;
952 my $editor = new_editor(authtoken => $authtoken);
953 return $editor->event unless $editor->checkauth;
955 my $n = $editor->search_asset_copy_note(
956 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
958 $n = $editor->search_asset_call_number_note(
959 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
961 $n = $editor->search_biblio_record_note(
962 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
969 __PACKAGE__->register_method(
970 method => 'create_copy_note',
971 api_name => 'open-ils.circ.copy_note.create',
973 Creates a new copy note
974 @param authtoken The login session key
975 @param note The note object to create
976 @return The id of the new note object
979 sub create_copy_note {
980 my( $self, $connection, $authtoken, $note ) = @_;
982 my $e = new_editor(xact=>1, authtoken=>$authtoken);
983 return $e->event unless $e->checkauth;
984 my $copy = $e->retrieve_asset_copy(
988 flesh_fields => { 'acp' => ['call_number'] }
993 return $e->event unless
994 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
996 $note->create_date('now');
997 $note->creator($e->requestor->id);
998 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1001 $e->create_asset_copy_note($note) or return $e->event;
1007 __PACKAGE__->register_method(
1008 method => 'delete_copy_note',
1009 api_name => 'open-ils.circ.copy_note.delete',
1011 Deletes an existing copy note
1012 @param authtoken The login session key
1013 @param noteid The id of the note to delete
1014 @return 1 on success - Event otherwise.
1016 sub delete_copy_note {
1017 my( $self, $conn, $authtoken, $noteid ) = @_;
1019 my $e = new_editor(xact=>1, authtoken=>$authtoken);
1020 return $e->die_event unless $e->checkauth;
1022 my $note = $e->retrieve_asset_copy_note([
1026 'acpn' => [ 'owning_copy' ],
1027 'acp' => [ 'call_number' ],
1030 ]) or return $e->die_event;
1032 if( $note->creator ne $e->requestor->id ) {
1033 return $e->die_event unless
1034 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1037 $e->delete_asset_copy_note($note) or return $e->die_event;
1042 __PACKAGE__->register_method(
1043 method => 'fetch_copy_tags',
1045 api_name => 'open-ils.circ.copy_tags.retrieve',
1047 Returns an array of publicly-visible copy tag objects.
1048 @param args A named hash of parameters including:
1049 copy_id : The id of the item whose notes we want to retrieve
1050 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1051 scope : top of org subtree whose copy tags we want to see
1052 depth : how far down to look for copy tags (optional)
1053 @return An array of copy tag objects
1055 __PACKAGE__->register_method(
1056 method => 'fetch_copy_tags',
1058 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1060 Returns an array of all copy tag objects.
1061 @param args A named hash of parameters including:
1062 authtoken : Required to view non-public notes
1063 copy_id : The id of the item whose notes we want to retrieve (optional)
1064 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1065 scope : top of org subtree whose copy tags we want to see
1066 depth : how far down to look for copy tags (optional)
1067 @return An array of copy tag objects
1070 sub fetch_copy_tags {
1071 my ($self, $conn, $args) = @_;
1073 my $org = $args->{scope};
1074 my $depth = $args->{depth};
1078 if ($self->api_name =~ /\.staff/) {
1079 my $authtoken = $args->{authtoken};
1080 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1081 $e = new_editor(authtoken => $args->{authtoken});
1082 return $e->event unless $e->checkauth;
1083 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1086 $filter->{pub} = 't';
1088 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1089 $filter->{'+acptcm'} = {
1090 copy => $args->{copy_id}
1093 # filter by owner of copy tag and depth
1094 $filter->{owner} = {
1096 select => {aou => [{
1098 transform => 'actor.org_unit_descendants',
1099 result_field => 'id',
1100 (defined($depth) ? ( params => [$depth] ) : ()),
1103 where => {id => $org}
1107 return $e->search_asset_copy_tag([$filter, { join => { acptcm => {} } }]);
1111 __PACKAGE__->register_method(
1112 method => 'age_hold_rules',
1113 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1116 sub age_hold_rules {
1117 my( $self, $conn ) = @_;
1118 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1123 __PACKAGE__->register_method(
1124 method => 'copy_details_barcode',
1126 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1127 sub copy_details_barcode {
1128 my( $self, $conn, $auth, $barcode ) = @_;
1129 my $e = new_editor();
1130 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1131 return $e->event unless $cid;
1132 return copy_details( $self, $conn, $auth, $cid );
1136 __PACKAGE__->register_method(
1137 method => 'copy_details',
1138 api_name => 'open-ils.circ.copy_details.retrieve');
1141 my( $self, $conn, $auth, $copy_id ) = @_;
1142 my $e = new_editor(authtoken=>$auth);
1143 return $e->event unless $e->checkauth;
1145 my $flesh = { flesh => 1 };
1147 my $copy = $e->retrieve_asset_copy(
1153 acp => ['call_number','parts','peer_record_maps','floating'],
1154 acn => ['record','prefix','suffix','label_class']
1157 ]) or return $e->event;
1160 # De-flesh the copy for backwards compatibility
1162 my $vol = $copy->call_number;
1164 $copy->call_number($vol->id);
1165 my $record = $vol->record;
1167 $vol->record($record->id);
1168 $mvr = $U->record_to_mvr($record);
1173 my $hold = $e->search_action_hold_request(
1175 current_copy => $copy_id,
1176 capture_time => { "!=" => undef },
1177 fulfillment_time => undef,
1178 cancel_time => undef,
1182 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1184 my $transit = $e->search_action_transit_copy(
1185 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1187 # find the most recent circulation for the requested copy,
1188 # be it active, completed, or aged.
1189 my $circ = $e->search_action_all_circulation_slim([
1190 { target_copy => $copy_id },
1196 'checkin_workstation',
1199 'recurring_fine_rule'
1202 order_by => { aacs => 'xact_start desc' },
1210 transit => $transit,
1220 __PACKAGE__->register_method(
1221 method => 'mark_item',
1222 api_name => 'open-ils.circ.mark_item_damaged',
1224 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1225 @param authtoken The login session key
1226 @param copy_id The ID of the copy to mark as damaged
1227 @return 1 on success - Event otherwise.
1230 __PACKAGE__->register_method(
1231 method => 'mark_item',
1232 api_name => 'open-ils.circ.mark_item_missing',
1234 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1235 @param authtoken The login session key
1236 @param copy_id The ID of the copy to mark as missing
1237 @return 1 on success - Event otherwise.
1240 __PACKAGE__->register_method(
1241 method => 'mark_item',
1242 api_name => 'open-ils.circ.mark_item_bindery',
1244 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1245 @param authtoken The login session key
1246 @param copy_id The ID of the copy to mark as bindery
1247 @return 1 on success - Event otherwise.
1250 __PACKAGE__->register_method(
1251 method => 'mark_item',
1252 api_name => 'open-ils.circ.mark_item_on_order',
1254 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1255 @param authtoken The login session key
1256 @param copy_id The ID of the copy to mark as on order
1257 @return 1 on success - Event otherwise.
1260 __PACKAGE__->register_method(
1261 method => 'mark_item',
1262 api_name => 'open-ils.circ.mark_item_ill',
1264 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1265 @param authtoken The login session key
1266 @param copy_id The ID of the copy to mark as inter-library loan
1267 @return 1 on success - Event otherwise.
1270 __PACKAGE__->register_method(
1271 method => 'mark_item',
1272 api_name => 'open-ils.circ.mark_item_cataloging',
1274 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1275 @param authtoken The login session key
1276 @param copy_id The ID of the copy to mark as cataloging
1277 @return 1 on success - Event otherwise.
1280 __PACKAGE__->register_method(
1281 method => 'mark_item',
1282 api_name => 'open-ils.circ.mark_item_reserves',
1284 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1285 @param authtoken The login session key
1286 @param copy_id The ID of the copy to mark as reserves
1287 @return 1 on success - Event otherwise.
1290 __PACKAGE__->register_method(
1291 method => 'mark_item',
1292 api_name => 'open-ils.circ.mark_item_discard',
1294 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1295 @param authtoken The login session key
1296 @param copy_id The ID of the copy to mark as discard
1297 @return 1 on success - Event otherwise.
1302 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1305 # Items must be checked in before any attempt is made to mark damaged
1306 my $evt = try_checkin($auth, $copy_id) if
1307 ($self->api_name=~ /damaged/ && $args->{handle_checkin});
1308 return $evt if $evt;
1310 my $e = new_editor(authtoken=>$auth, xact =>1);
1311 return $e->die_event unless $e->checkauth;
1312 my $copy = $e->retrieve_asset_copy([
1314 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1315 or return $e->die_event;
1318 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1319 $copy->circ_lib : $copy->call_number->owning_lib;
1321 my $perm = 'MARK_ITEM_MISSING';
1322 my $stat = OILS_COPY_STATUS_MISSING;
1324 if( $self->api_name =~ /damaged/ ) {
1325 $perm = 'MARK_ITEM_DAMAGED';
1326 $stat = OILS_COPY_STATUS_DAMAGED;
1327 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1328 return $evt if $evt;
1330 } elsif ( $self->api_name =~ /bindery/ ) {
1331 $perm = 'MARK_ITEM_BINDERY';
1332 $stat = OILS_COPY_STATUS_BINDERY;
1333 } elsif ( $self->api_name =~ /on_order/ ) {
1334 $perm = 'MARK_ITEM_ON_ORDER';
1335 $stat = OILS_COPY_STATUS_ON_ORDER;
1336 } elsif ( $self->api_name =~ /ill/ ) {
1337 $perm = 'MARK_ITEM_ILL';
1338 $stat = OILS_COPY_STATUS_ILL;
1339 } elsif ( $self->api_name =~ /cataloging/ ) {
1340 $perm = 'MARK_ITEM_CATALOGING';
1341 $stat = OILS_COPY_STATUS_CATALOGING;
1342 } elsif ( $self->api_name =~ /reserves/ ) {
1343 $perm = 'MARK_ITEM_RESERVES';
1344 $stat = OILS_COPY_STATUS_RESERVES;
1345 } elsif ( $self->api_name =~ /discard/ ) {
1346 $perm = 'MARK_ITEM_DISCARD';
1347 $stat = OILS_COPY_STATUS_DISCARD;
1350 # caller may proceed if either perm is allowed
1351 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1353 $copy->status($stat);
1354 $copy->edit_date('now');
1355 $copy->editor($e->requestor->id);
1357 $e->update_asset_copy($copy) or return $e->die_event;
1359 my $holds = $e->search_action_hold_request(
1361 current_copy => $copy->id,
1362 fulfillment_time => undef,
1363 cancel_time => undef,
1369 if( $self->api_name =~ /damaged/ ) {
1370 # now that we've committed the changes, create related A/T events
1371 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1372 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1375 $logger->debug("resetting holds that target the marked copy");
1376 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1382 my($auth, $copy_id) = @_;
1384 my $checkin = $U->simplereq(
1386 'open-ils.circ.checkin.override',
1388 copy_id => $copy_id,
1392 if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1394 my $evt_code = $checkin->[0]->{textcode};
1395 $logger->info("try_checkin() received event: $evt_code");
1397 if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1398 $logger->info('try_checkin() successful checkin');
1401 $logger->warn('try_checkin() un-successful checkin');
1406 sub handle_mark_damaged {
1407 my($e, $copy, $owning_lib, $args) = @_;
1409 my $apply = $args->{apply_fines} || '';
1410 return undef if $apply eq 'noapply';
1412 my $new_amount = $args->{override_amount};
1413 my $new_btype = $args->{override_btype};
1414 my $new_note = $args->{override_note};
1416 # grab the last circulation
1417 my $circ = $e->search_action_circulation([
1418 { target_copy => $copy->id},
1420 order_by => {circ => "xact_start DESC"},
1422 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1426 return undef unless $circ;
1428 my $charge_price = $U->ou_ancestor_setting_value(
1429 $owning_lib, 'circ.charge_on_damaged', $e);
1431 my $proc_fee = $U->ou_ancestor_setting_value(
1432 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1434 my $void_overdue = $U->ou_ancestor_setting_value(
1435 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1437 return undef unless $charge_price or $proc_fee;
1439 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1440 my $total = $copy_price + $proc_fee;
1444 if($new_amount and $new_btype) {
1446 # Allow staff to override the amount to charge for a damaged item
1447 # Consider the case where the item is only partially damaged
1448 # This value is meant to take the place of the item price and
1449 # optional processing fee.
1451 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1452 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1453 return $evt if $evt;
1457 if($charge_price and $copy_price) {
1458 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1459 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1460 return $evt if $evt;
1464 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1465 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1466 return $evt if $evt;
1470 # the assumption is that you would not void the overdues unless you
1471 # were also charging for the item and/or applying a processing fee
1473 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1474 return $evt if $evt;
1477 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1478 return $evt if $evt;
1480 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1481 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1483 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1484 return $evt2 if $evt2;
1487 return OpenILS::Event->new('DAMAGE_CHARGE',
1498 # ----------------------------------------------------------------------
1499 __PACKAGE__->register_method(
1500 method => 'mark_item_missing_pieces',
1501 api_name => 'open-ils.circ.mark_item_missing_pieces',
1503 Changes the status of a copy to "damaged" or to a custom status based on the
1504 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1506 @param authtoken The login session key
1507 @param copy_id The ID of the copy to mark as damaged
1508 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1512 sub mark_item_missing_pieces {
1513 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1514 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1515 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1517 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1518 return $e2->die_event unless $e2->checkauth;
1521 my $copy = $e2->retrieve_asset_copy([
1523 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1524 or return $e2->die_event;
1527 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1528 $copy->circ_lib : $copy->call_number->owning_lib;
1530 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1532 #### grab the last circulation
1533 my $circ = $e2->search_action_circulation([
1534 { target_copy => $copy->id},
1536 order_by => {circ => "xact_start DESC"}
1541 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1543 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1546 my $holds = $e2->search_action_hold_request(
1548 current_copy => $copy->id,
1549 fulfillment_time => undef,
1550 cancel_time => undef,
1554 $logger->debug("resetting holds that target the marked copy");
1555 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1558 if (! $e2->commit) {
1559 return $e2->die_event;
1562 my $e = new_editor(authtoken=>$auth, xact =>1);
1563 return $e->die_event unless $e->checkauth;
1565 if (! $circ->checkin_time) { # if circ active, attempt renew
1566 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1567 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1568 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1569 $circ = $res->[0]->{payload}{'circ'};
1570 $circ->target_copy( $copy->id );
1571 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1573 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1578 'copy_id'=>$circ->target_copy,
1579 'patron_id'=>$circ->usr,
1580 'skip_deposit_fee'=>1,
1581 'skip_rental_fee'=>1
1584 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1586 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1587 $e, $copy, $e->requestor, 1 );
1589 if ($hold) { # needed for hold? then due now
1591 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1592 my $due_date = DateTime->now(time_zone => 'local');
1593 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1595 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1599 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1600 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1601 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1602 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1603 $circ = $res->[0]->{payload}{'circ'};
1605 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1611 ### Update the item status
1613 my $custom_stat = $U->ou_ancestor_setting_value(
1614 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1615 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1617 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1618 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1620 $copy->status($stat);
1621 $copy->edit_date('now');
1622 $copy->editor($e->requestor->id);
1624 $e->update_asset_copy($copy) or return $e->die_event;
1628 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1629 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1631 return OpenILS::Event->new('SUCCESS',
1635 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1636 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1641 return $e->die_event;
1649 # ----------------------------------------------------------------------
1650 __PACKAGE__->register_method(
1651 method => 'magic_fetch',
1652 api_name => 'open-ils.agent.fetch'
1655 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1658 my( $self, $conn, $auth, $args ) = @_;
1659 my $e = new_editor( authtoken => $auth );
1660 return $e->event unless $e->checkauth;
1662 my $hint = $$args{hint};
1663 my $id = $$args{id};
1665 # Is the call allowed to fetch this type of object?
1666 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1668 # Find the class the implements the given hint
1669 my ($class) = grep {
1670 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1672 $class =~ s/Fieldmapper:://og;
1673 $class =~ s/::/_/og;
1674 my $method = "retrieve_$class";
1676 my $obj = $e->$method($id) or return $e->event;
1679 # ----------------------------------------------------------------------
1682 __PACKAGE__->register_method(
1683 method => "fleshed_circ_retrieve",
1685 api_name => "open-ils.circ.fleshed.retrieve",);
1687 sub fleshed_circ_retrieve {
1688 my( $self, $client, $id ) = @_;
1689 my $e = new_editor();
1690 my $circ = $e->retrieve_action_circulation(
1696 circ => [ qw/ target_copy / ],
1697 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1698 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1699 acn => [ qw/ record / ],
1703 ) or return $e->event;
1705 my $copy = $circ->target_copy;
1706 my $vol = $copy->call_number;
1707 my $rec = $circ->target_copy->call_number->record;
1709 $vol->record($rec->id);
1710 $copy->call_number($vol->id);
1711 $circ->target_copy($copy->id);
1715 if( $rec->id == OILS_PRECAT_RECORD ) {
1719 $mvr = $U->record_to_mvr($rec);
1720 $rec->marc(''); # drop the bulky marc data
1734 __PACKAGE__->register_method(
1735 method => "test_batch_circ_events",
1736 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1739 # method for testing the behavior of a given event definition
1740 sub test_batch_circ_events {
1741 my($self, $conn, $auth, $event_def, $barcode) = @_;
1743 my $e = new_editor(authtoken => $auth);
1744 return $e->event unless $e->checkauth;
1745 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1747 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1748 or return $e->event;
1750 my $circ = $e->search_action_circulation(
1751 {target_copy => $copy->id, checkin_time => undef})->[0]
1752 or return $e->event;
1754 return undef unless $circ;
1756 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1760 __PACKAGE__->register_method(
1761 method => "fire_circ_events",
1762 api_name => "open-ils.circ.fire_circ_trigger_events",
1764 General event def runner for circ objects. If no event def ID
1765 is provided, the hook will be used to find the best event_def
1766 match based on the context org unit
1770 __PACKAGE__->register_method(
1771 method => "fire_circ_events",
1772 api_name => "open-ils.circ.fire_hold_trigger_events",
1774 General event def runner for hold objects. If no event def ID
1775 is provided, the hook will be used to find the best event_def
1776 match based on the context org unit
1780 __PACKAGE__->register_method(
1781 method => "fire_circ_events",
1782 api_name => "open-ils.circ.fire_user_trigger_events",
1784 General event def runner for user objects. If no event def ID
1785 is provided, the hook will be used to find the best event_def
1786 match based on the context org unit
1791 sub fire_circ_events {
1792 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1794 my $e = new_editor(authtoken => $auth, xact => 1);
1795 return $e->event unless $e->checkauth;
1799 if($self->api_name =~ /hold/) {
1800 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1801 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1802 } elsif($self->api_name =~ /user/) {
1803 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1804 $targets = $e->batch_retrieve_actor_user($target_ids);
1806 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1807 $targets = $e->batch_retrieve_action_circulation($target_ids);
1809 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1810 # simply making this method authoritative because of weirdness
1811 # with transaction handling in A/T code that causes rollback
1812 # failure down the line if handling many targets
1814 return undef unless @$targets;
1815 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1818 __PACKAGE__->register_method(
1819 method => "user_payments_list",
1820 api_name => "open-ils.circ.user_payments.filtered.batch",
1823 desc => q/Returns a fleshed, date-limited set of all payments a user
1824 has made. By default, ordered by payment date. Optionally
1825 ordered by other columns in the top-level "mp" object/,
1827 {desc => 'Authentication token', type => 'string'},
1828 {desc => 'User ID', type => 'number'},
1829 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1831 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1832 and the related fully-realized payment object (e.g money.cash_payment)/}
1836 sub user_payments_list {
1837 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1839 my $e = new_editor(authtoken => $auth);
1840 return $e->event unless $e->checkauth;
1842 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1843 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1845 $order_by ||= ['payment_ts'];
1847 # all payments by user, between start_date and end_date
1848 my $payments = $e->json_query({
1849 select => {mp => ['id']},
1853 fkey => 'xact', field => 'id'}
1857 '+mbt' => {usr => $user_id},
1858 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1860 order_by => {mp => $order_by}
1863 for my $payment_id (@$payments) {
1864 my $payment = $e->retrieve_money_payment([
1872 'credit_card_payment',
1887 $conn->respond($payment);
1894 __PACKAGE__->register_method(
1895 method => "retrieve_circ_chain",
1896 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1899 desc => q/Given a circulation, this returns all circulation objects
1900 that are part of the same chain of renewals./,
1902 {desc => 'Authentication token', type => 'string'},
1903 {desc => 'Circ ID', type => 'number'},
1905 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1909 __PACKAGE__->register_method(
1910 method => "retrieve_circ_chain",
1911 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1913 desc => q/Given a circulation, this returns a summary of the circulation objects
1914 that are part of the same chain of renewals./,
1916 {desc => 'Authentication token', type => 'string'},
1917 {desc => 'Circ ID', type => 'number'},
1919 return => {desc => q/Circulation Chain Summary/}
1923 sub retrieve_circ_chain {
1924 my($self, $conn, $auth, $circ_id) = @_;
1926 my $e = new_editor(authtoken => $auth);
1927 return $e->event unless $e->checkauth;
1928 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1930 if($self->api_name =~ /summary/) {
1931 return $U->create_circ_chain_summary($e, $circ_id);
1935 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
1937 for my $circ_info (@$chain) {
1938 my $circ = Fieldmapper::action::all_circulation_slim->new;
1939 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1940 $conn->respond($circ);
1947 __PACKAGE__->register_method(
1948 method => "retrieve_prev_circ_chain",
1949 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1952 desc => q/Given a circulation, this returns all circulation objects
1953 that are part of the previous chain of renewals./,
1955 {desc => 'Authentication token', type => 'string'},
1956 {desc => 'Circ ID', type => 'number'},
1958 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1962 __PACKAGE__->register_method(
1963 method => "retrieve_prev_circ_chain",
1964 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1966 desc => q/Given a circulation, this returns a summary of the circulation objects
1967 that are part of the previous chain of renewals./,
1969 {desc => 'Authentication token', type => 'string'},
1970 {desc => 'Circ ID', type => 'number'},
1972 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1976 sub retrieve_prev_circ_chain {
1977 my($self, $conn, $auth, $circ_id) = @_;
1979 my $e = new_editor(authtoken => $auth);
1980 return $e->event unless $e->checkauth;
1981 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1984 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
1986 my $prev_circ = $e->search_action_all_circulation_slim([
1987 { target_copy => $first_circ->{target_copy},
1988 xact_start => {'<' => $first_circ->{xact_start}}
1997 order_by => { aacs => 'xact_start desc' },
2002 return undef unless $prev_circ;
2004 my $chain_usr = $prev_circ->usr; # note: may be undef
2006 if ($self->api_name =~ /summary/) {
2007 my $sum = $e->json_query({
2009 'action.summarize_all_circ_chain',
2014 my $summary = Fieldmapper::action::circ_chain_summary->new;
2015 $summary->$_($sum->{$_}) for keys %$sum;
2017 return {summary => $summary, usr => $chain_usr};
2021 my $chain = $e->json_query(
2022 {from => ['action.all_circ_chain', $prev_circ->id]});
2024 for my $circ_info (@$chain) {
2025 my $circ = Fieldmapper::action::all_circulation_slim->new;
2026 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2027 $conn->respond($circ);
2034 __PACKAGE__->register_method(
2035 method => "get_copy_due_date",
2036 api_name => "open-ils.circ.copy.due_date.retrieve",
2039 Given a copy ID, returns the due date for the copy if it's
2040 currently circulating. Otherwise, returns null. Note, this is a public
2041 method requiring no authentication. Only the due date is exposed.
2044 {desc => 'Copy ID', type => 'number'}
2046 return => {desc => q/
2047 Due date (ISO date stamp) if the copy is circulating, null otherwise.
2052 sub get_copy_due_date {
2053 my($self, $conn, $copy_id) = @_;
2054 my $e = new_editor();
2056 my $circ = $e->json_query({
2057 select => {circ => ['due_date']},
2060 target_copy => $copy_id,
2061 checkin_time => undef,
2063 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2064 {stop_fines => undef}
2068 })->[0] or return undef;
2070 return $circ->{due_date};
2077 # {"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}}