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::CreditCard;
12 use OpenILS::Application::Circ::Money;
13 use OpenILS::Application::Circ::NonCat;
14 use OpenILS::Application::Circ::CopyLocations;
15 use OpenILS::Application::Circ::CircCommon;
18 use DateTime::Format::ISO8601;
20 use OpenILS::Application::AppUtils;
22 use OpenSRF::Utils qw/:datetime/;
23 use OpenSRF::AppSession;
24 use OpenILS::Utils::ModsParser;
26 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::Fieldmapper;
29 use OpenILS::Utils::Editor;
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
61 my( $s, $c, $a, $i ) = @_;
62 my $e = new_editor(authtoken => $a);
63 return $e->event unless $e->checkauth;
64 my $circ = $e->retrieve_action_circulation($i) or return $e->event;
65 if( $e->requestor->id ne $circ->usr ) {
66 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
72 __PACKAGE__->register_method(
73 method => 'fetch_circ_mods',
74 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
76 my($self, $conn, $args) = @_;
77 my $mods = new_editor()->retrieve_all_config_circ_modifier;
78 return [ map {$_->code} @$mods ] unless $$args{full};
82 __PACKAGE__->register_method(
83 method => 'fetch_bill_types',
84 api_name => 'open-ils.circ.billing_type.retrieve.all');
85 sub fetch_bill_types {
86 my $conf = OpenSRF::Utils::SettingsClient->new;
87 return $conf->config_value(
88 'apps', 'open-ils.circ', 'app_settings', 'billing_types', 'type' );
92 __PACKAGE__->register_method(
93 method => 'ranged_billing_types',
94 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
96 sub ranged_billing_types {
97 my($self, $conn, $auth, $org_id, $depth) = @_;
98 my $e = new_editor(authtoken => $auth);
99 return $e->event unless $e->checkauth;
100 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
101 return $e->search_config_billing_type(
102 {owner => $U->get_org_full_path($org_id, $depth)});
107 # ------------------------------------------------------------------------
108 # Returns an array of {circ, record} hashes checked out by the user.
109 # ------------------------------------------------------------------------
110 __PACKAGE__->register_method(
111 method => "checkouts_by_user",
112 api_name => "open-ils.circ.actor.user.checked_out",
114 NOTES => <<" NOTES");
115 Returns a list of open circulations as a pile of objects. Each object
116 contains the relevant copy, circ, and record
119 sub checkouts_by_user {
120 my($self, $client, $auth, $user_id) = @_;
122 my $e = new_editor(authtoken=>$auth);
123 return $e->event unless $e->checkauth;
125 my $circ_ids = $e->search_action_circulation(
127 checkin_time => undef,
129 {stop_fines => undef},
130 {stop_fines => ['MAXFINES','LONGOVERDUE']}
136 for my $id (@$circ_ids) {
137 my $circ = $e->retrieve_action_circulation([
141 circ => ['target_copy'],
142 acp => ['call_number'],
148 # un-flesh for consistency
149 my $c = $circ->target_copy;
150 $circ->target_copy($c->id);
152 my $cn = $c->call_number;
153 $c->call_number($cn->id);
161 record => $U->record_to_mvr($t)
171 __PACKAGE__->register_method(
172 method => "checkouts_by_user_slim",
173 api_name => "open-ils.circ.actor.user.checked_out.slim",
174 NOTES => <<" NOTES");
175 Returns a list of open circulation objects
179 sub checkouts_by_user_slim {
180 my( $self, $client, $user_session, $user_id ) = @_;
182 my( $requestor, $target, $copy, $record, $evt );
184 ( $requestor, $target, $evt ) =
185 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
188 $logger->debug( 'User ' . $requestor->id .
189 " retrieving checked out items for user " . $target->id );
191 # XXX Make the call correct..
192 return $apputils->simplereq(
194 "open-ils.cstore.direct.action.open_circulation.search.atomic",
195 { usr => $target->id, checkin_time => undef } );
196 # { usr => $target->id } );
200 __PACKAGE__->register_method(
201 method => "checkouts_by_user_opac",
202 api_name => "open-ils.circ.actor.user.checked_out.opac",);
205 sub checkouts_by_user_opac {
206 my( $self, $client, $auth, $user_id ) = @_;
208 my $e = OpenILS::Utils::Editor->new( authtoken => $auth );
209 return $e->event unless $e->checkauth;
210 $user_id ||= $e->requestor->id;
211 return $e->event unless
212 my $patron = $e->retrieve_actor_user($user_id);
215 my $search = {usr => $user_id, stop_fines => undef};
217 if( $user_id ne $e->requestor->id ) {
218 $data = $e->search_action_circulation(
219 $search, {checkperm=>1, permorg=>$patron->home_ou})
223 $data = $e->search_action_circulation($search);
230 __PACKAGE__->register_method(
231 method => "title_from_transaction",
232 api_name => "open-ils.circ.circ_transaction.find_title",
233 NOTES => <<" NOTES");
234 Returns a mods object for the title that is linked to from the
235 copy from the hold that created the given transaction
238 sub title_from_transaction {
239 my( $self, $client, $login_session, $transactionid ) = @_;
241 my( $user, $circ, $title, $evt );
243 ( $user, $evt ) = $apputils->checkses( $login_session );
246 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
249 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
252 return $apputils->record_to_mvr($title);
255 __PACKAGE__->register_method(
256 method => "staff_age_to_lost",
257 api_name => "open-ils.circ.circulation.age_to_lost",
260 This fires a circ.staff_age_to_lost Action-Trigger event against all
261 overdue circulations in scope of the specified context library and
262 user profile, which effectively marks the associated items as Lost.
263 This is likely to be done at the end of a semester in an academic
266 @param args : circ_lib, user_profile
270 sub staff_age_to_lost {
271 my( $self, $conn, $auth, $args ) = @_;
272 my $e = new_editor(authtoken=>$auth);
273 return $e->event unless $e->checkauth;
274 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
276 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
277 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
279 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
281 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
282 my $hook = 'circ.staff_age_to_lost';
283 my $context_org = 'circ_lib';
284 my $opt_granularity = undef;
286 "checkin_time" => undef,
287 "due_date" => { "<" => "now" },
289 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
290 { "stop_fines" => undef }
294 "select" => {"au" => ["id"]},
297 "profile" => $profiles,
298 "id" => { "=" => {"+circ" => "usr"} }
302 "select" => {"aou" => ["id"]},
306 {"id" => { "=" => {"+circ" => "circ_lib"} }},
313 my $req_timeout = 10800;
314 my $chunk_size = 100;
317 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
318 my @event_ids; my @chunked_ids;
319 while (my $resp = $req->recv(timeout => $req_timeout)) {
320 push(@event_ids, $resp->content);
321 push(@chunked_ids, $resp->content);
322 if (scalar(@chunked_ids) > $chunk_size) {
323 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
327 if (scalar(@chunked_ids) > 0) {
328 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
332 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
333 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
334 } elsif($req->complete) {
335 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
336 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
338 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
339 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
346 __PACKAGE__->register_method(
347 method => "new_set_circ_lost",
348 api_name => "open-ils.circ.circulation.set_lost",
350 Sets the copy and related open circulation to lost
352 @param args : barcode
357 # ---------------------------------------------------------------------
358 # Sets a circulation to lost. updates copy status to lost
359 # applies copy and/or prcoessing fees depending on org settings
360 # ---------------------------------------------------------------------
361 sub new_set_circ_lost {
362 my( $self, $conn, $auth, $args ) = @_;
364 my $e = new_editor(authtoken=>$auth, xact=>1);
365 return $e->die_event unless $e->checkauth;
367 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
368 or return $e->die_event;
370 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
378 __PACKAGE__->register_method(
379 method => "set_circ_claims_returned",
380 api_name => "open-ils.circ.circulation.set_claims_returned",
382 desc => q/Sets the circ for a given item as claims returned
383 If a backdate is provided, overdue fines will be voided
384 back to the backdate/,
386 {desc => 'Authentication token', type => 'string'},
387 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
389 return => {desc => q/1 on success, failure event on error, and
390 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
391 configured claims return maximum/}
395 __PACKAGE__->register_method(
396 method => "set_circ_claims_returned",
397 api_name => "open-ils.circ.circulation.set_claims_returned.override",
399 desc => q/This adds support for overrideing the configured max
400 claims returned amount.
401 @see open-ils.circ.circulation.set_claims_returned./,
405 sub set_circ_claims_returned {
406 my( $self, $conn, $auth, $args, $oargs ) = @_;
408 my $e = new_editor(authtoken=>$auth, xact=>1);
409 return $e->die_event unless $e->checkauth;
411 $oargs = { all => 1 } unless defined $oargs;
413 my $barcode = $$args{barcode};
414 my $backdate = $$args{backdate};
416 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
417 or return $e->die_event;
419 my $circ = $e->search_action_circulation(
420 {checkin_time => undef, target_copy => $copy->id})->[0]
421 or return $e->die_event;
423 $backdate = $circ->due_date if $$args{use_due_date};
425 $logger->info("marking circ for item $barcode as claims returned".
426 (($backdate) ? " with backdate $backdate" : ''));
428 my $patron = $e->retrieve_actor_user($circ->usr);
429 my $max_count = $U->ou_ancestor_setting_value(
430 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
432 # If the patron has too instances of many claims returned,
433 # require an override to continue. A configured max of
434 # 0 means all attempts require an override
435 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
437 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
439 # see if we're allowed to override
440 return $e->die_event unless
441 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
445 # exit early and return the max claims return event
447 return OpenILS::Event->new(
448 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
450 patron_count => $patron->claims_returned_count,
451 max_count => $max_count
457 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
458 or return $e->die_event;
460 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
461 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
464 $backdate = cleanse_ISO8601($backdate);
466 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
467 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
468 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
470 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
471 $backdate = cleanse_ISO8601($backdate);
473 # make it look like the circ stopped at the cliams returned time
474 $circ->stop_fines_time($backdate);
475 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
479 $e->update_action_circulation($circ) or return $e->die_event;
481 # see if there is a configured post-claims-return copy status
482 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
483 $copy->status($stat);
484 $copy->edit_date('now');
485 $copy->editor($e->requestor->id);
486 $e->update_asset_copy($copy) or return $e->die_event;
494 __PACKAGE__->register_method(
495 method => "post_checkin_backdate_circ",
496 api_name => "open-ils.circ.post_checkin_backdate",
498 desc => q/Back-date an already checked in circulation/,
500 {desc => 'Authentication token', type => 'string'},
501 {desc => 'Circ ID', type => 'number'},
502 {desc => 'ISO8601 backdate', type => 'string'},
504 return => {desc => q/1 on success, failure event on error/}
508 __PACKAGE__->register_method(
509 method => "post_checkin_backdate_circ",
510 api_name => "open-ils.circ.post_checkin_backdate.batch",
513 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
515 {desc => 'Authentication token', type => 'string'},
516 {desc => 'List of Circ ID', type => 'array'},
517 {desc => 'ISO8601 backdate', type => 'string'},
519 return => {desc => q/Set of: 1 on success, failure event on error/}
524 sub post_checkin_backdate_circ {
525 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
526 my $e = new_editor(authtoken=>$auth);
527 return $e->die_event unless $e->checkauth;
528 if($self->api_name =~ /batch/) {
529 foreach my $c (@$circ_id) {
530 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
533 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
541 sub post_checkin_backdate_circ_impl {
542 my($e, $circ_id, $backdate) = @_;
546 my $circ = $e->retrieve_action_circulation($circ_id)
547 or return $e->die_event;
549 # anyone with checkin perms can backdate (more restrictive?)
550 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
552 # don't allow back-dating an open circulation
553 return OpenILS::Event->new('BAD_PARAMS') unless
554 $backdate and $circ->checkin_time;
556 # update the checkin and stop_fines times to reflect the new backdate
557 $circ->stop_fines_time(cleanse_ISO8601($backdate));
558 $circ->checkin_time(cleanse_ISO8601($backdate));
559 $e->update_action_circulation($circ) or return $e->die_event;
561 # now void the overdues "erased" by the back-dating
562 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
565 # If the circ was closed before and the balance owned !=0, re-open the transaction
566 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
575 __PACKAGE__->register_method (
576 method => 'set_circ_due_date',
577 api_name => 'open-ils.circ.circulation.due_date.update',
579 Updates the due_date on the given circ
581 @param circid The id of the circ to update
582 @param date The timestamp of the new due date
586 sub set_circ_due_date {
587 my( $self, $conn, $auth, $circ_id, $date ) = @_;
589 my $e = new_editor(xact=>1, authtoken=>$auth);
590 return $e->die_event unless $e->checkauth;
591 my $circ = $e->retrieve_action_circulation($circ_id)
592 or return $e->die_event;
594 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
595 $date = cleanse_ISO8601($date);
597 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
598 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
599 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
600 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
603 $circ->due_date($date);
604 $e->update_action_circulation($circ) or return $e->die_event;
611 __PACKAGE__->register_method(
612 method => "create_in_house_use",
613 api_name => 'open-ils.circ.in_house_use.create',
615 Creates an in-house use action.
616 @param $authtoken The login session key
617 @param params A hash of params including
618 'location' The org unit id where the in-house use occurs
619 'copyid' The copy in question
620 'count' The number of in-house uses to apply to this copy
621 @return An array of id's representing the id's of the newly created
622 in-house use objects or an event on an error
625 __PACKAGE__->register_method(
626 method => "create_in_house_use",
627 api_name => 'open-ils.circ.non_cat_in_house_use.create',
631 sub create_in_house_use {
632 my( $self, $client, $auth, $params ) = @_;
635 my $org = $params->{location};
636 my $copyid = $params->{copyid};
637 my $count = $params->{count} || 1;
638 my $nc_type = $params->{non_cat_type};
639 my $use_time = $params->{use_time} || 'now';
641 my $e = new_editor(xact=>1,authtoken=>$auth);
642 return $e->event unless $e->checkauth;
643 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
645 my $non_cat = 1 if $self->api_name =~ /non_cat/;
649 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
651 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
657 if( $use_time ne 'now' ) {
658 $use_time = cleanse_ISO8601($use_time);
659 $logger->debug("in_house_use setting use time to $use_time");
670 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
671 $ihu->item_type($nc_type);
672 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
673 $cmeth = "create_action_non_cat_in_house_use";
676 $ihu = Fieldmapper::action::in_house_use->new;
678 $method = 'open-ils.storage.direct.action.in_house_use.create';
679 $cmeth = "create_action_in_house_use";
682 $ihu->staff($e->requestor->id);
683 $ihu->org_unit($org);
684 $ihu->use_time($use_time);
686 $ihu = $e->$cmeth($ihu) or return $e->event;
687 push( @ids, $ihu->id );
698 __PACKAGE__->register_method(
699 method => "view_circs",
700 api_name => "open-ils.circ.copy_checkout_history.retrieve",
702 Retrieves the last X circs for a given copy
703 @param authtoken The login session key
704 @param copyid The copy to check
705 @param count How far to go back in the item history
706 @return An array of circ ids
709 # ----------------------------------------------------------------------
710 # Returns $count most recent circs. If count exceeds the configured
711 # max, use the configured max instead
712 # ----------------------------------------------------------------------
714 my( $self, $client, $authtoken, $copyid, $count ) = @_;
716 my $e = new_editor(authtoken => $authtoken);
717 return $e->event unless $e->checkauth;
719 my $copy = $e->retrieve_asset_copy([
722 flesh_fields => {acp => ['call_number']}
724 ]) or return $e->event;
726 return $e->event unless $e->allowed(
727 'VIEW_COPY_CHECKOUT_HISTORY',
728 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
729 $copy->circ_lib : $copy->call_number->owning_lib);
731 my $max_history = $U->ou_ancestor_setting_value(
732 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
734 if(defined $max_history) {
735 $count = $max_history unless defined $count and $count < $max_history;
737 $count = 4 unless defined $count;
740 return $e->search_action_circulation([
741 {target_copy => $copyid},
742 {limit => $count, order_by => { circ => "xact_start DESC" }}
747 __PACKAGE__->register_method(
748 method => "circ_count",
749 api_name => "open-ils.circ.circulation.count",
751 Returns the number of times the item has circulated
752 @param copyid The copy to check
756 my( $self, $client, $copyid, $range ) = @_;
757 my $e = OpenILS::Utils::Editor->new;
758 return $e->request('open-ils.storage.asset.copy.circ_count', $copyid, $range);
763 __PACKAGE__->register_method(
764 method => 'fetch_notes',
766 api_name => 'open-ils.circ.copy_note.retrieve.all',
768 Returns an array of copy note objects.
769 @param args A named hash of parameters including:
770 authtoken : Required if viewing non-public notes
771 itemid : The id of the item whose notes we want to retrieve
772 pub : True if all the caller wants are public notes
773 @return An array of note objects
776 __PACKAGE__->register_method(
777 method => 'fetch_notes',
778 api_name => 'open-ils.circ.call_number_note.retrieve.all',
779 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
781 __PACKAGE__->register_method(
782 method => 'fetch_notes',
783 api_name => 'open-ils.circ.title_note.retrieve.all',
784 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
787 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
789 my( $self, $connection, $args ) = @_;
791 my $id = $$args{itemid};
792 my $authtoken = $$args{authtoken};
795 if( $self->api_name =~ /copy/ ) {
797 return $U->cstorereq(
798 'open-ils.cstore.direct.asset.copy_note.search.atomic',
799 { owning_copy => $id, pub => 't' } );
801 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
803 return $U->cstorereq(
804 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
807 } elsif( $self->api_name =~ /call_number/ ) {
809 return $U->cstorereq(
810 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
811 { call_number => $id, pub => 't' } );
813 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
815 return $U->cstorereq(
816 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
819 } elsif( $self->api_name =~ /title/ ) {
821 return $U->cstorereq(
822 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
823 { record => $id, pub => 't' } );
825 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
827 return $U->cstorereq(
828 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
835 __PACKAGE__->register_method(
836 method => 'has_notes',
837 api_name => 'open-ils.circ.copy.has_notes');
838 __PACKAGE__->register_method(
839 method => 'has_notes',
840 api_name => 'open-ils.circ.call_number.has_notes');
841 __PACKAGE__->register_method(
842 method => 'has_notes',
843 api_name => 'open-ils.circ.title.has_notes');
847 my( $self, $conn, $authtoken, $id ) = @_;
848 my $editor = OpenILS::Utils::Editor->new(authtoken => $authtoken);
849 return $editor->event unless $editor->checkauth;
851 my $n = $editor->search_asset_copy_note(
852 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
854 $n = $editor->search_asset_call_number_note(
855 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
857 $n = $editor->search_biblio_record_note(
858 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
865 __PACKAGE__->register_method(
866 method => 'create_copy_note',
867 api_name => 'open-ils.circ.copy_note.create',
869 Creates a new copy note
870 @param authtoken The login session key
871 @param note The note object to create
872 @return The id of the new note object
875 sub create_copy_note {
876 my( $self, $connection, $authtoken, $note ) = @_;
878 my $e = new_editor(xact=>1, authtoken=>$authtoken);
879 return $e->event unless $e->checkauth;
880 my $copy = $e->retrieve_asset_copy(
884 flesh_fields => { 'acp' => ['call_number'] }
889 return $e->event unless
890 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
892 $note->create_date('now');
893 $note->creator($e->requestor->id);
894 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
897 $e->create_asset_copy_note($note) or return $e->event;
903 __PACKAGE__->register_method(
904 method => 'delete_copy_note',
905 api_name => 'open-ils.circ.copy_note.delete',
907 Deletes an existing copy note
908 @param authtoken The login session key
909 @param noteid The id of the note to delete
910 @return 1 on success - Event otherwise.
912 sub delete_copy_note {
913 my( $self, $conn, $authtoken, $noteid ) = @_;
915 my $e = new_editor(xact=>1, authtoken=>$authtoken);
916 return $e->die_event unless $e->checkauth;
918 my $note = $e->retrieve_asset_copy_note([
922 'acpn' => [ 'owning_copy' ],
923 'acp' => [ 'call_number' ],
926 ]) or return $e->die_event;
928 if( $note->creator ne $e->requestor->id ) {
929 return $e->die_event unless
930 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
933 $e->delete_asset_copy_note($note) or return $e->die_event;
939 __PACKAGE__->register_method(
940 method => 'age_hold_rules',
941 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
945 my( $self, $conn ) = @_;
946 return new_editor()->retrieve_all_config_rules_age_hold_protect();
951 __PACKAGE__->register_method(
952 method => 'copy_details_barcode',
954 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
955 sub copy_details_barcode {
956 my( $self, $conn, $auth, $barcode ) = @_;
957 my $e = new_editor();
958 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
959 return $e->event unless $cid;
960 return copy_details( $self, $conn, $auth, $cid );
964 __PACKAGE__->register_method(
965 method => 'copy_details',
966 api_name => 'open-ils.circ.copy_details.retrieve');
969 my( $self, $conn, $auth, $copy_id ) = @_;
970 my $e = new_editor(authtoken=>$auth);
971 return $e->event unless $e->checkauth;
973 my $flesh = { flesh => 1 };
975 my $copy = $e->retrieve_asset_copy(
981 acp => ['call_number','parts','peer_record_maps'],
982 acn => ['record','prefix','suffix','label_class']
985 ]) or return $e->event;
988 # De-flesh the copy for backwards compatibility
990 my $vol = $copy->call_number;
992 $copy->call_number($vol->id);
993 my $record = $vol->record;
995 $vol->record($record->id);
996 $mvr = $U->record_to_mvr($record);
1001 my $hold = $e->search_action_hold_request(
1003 current_copy => $copy_id,
1004 capture_time => { "!=" => undef },
1005 fulfillment_time => undef,
1006 cancel_time => undef,
1010 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1012 my $transit = $e->search_action_transit_copy(
1013 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1015 # find the latest circ, open or closed
1016 my $circ = $e->search_action_circulation(
1018 { target_copy => $copy_id },
1024 'checkin_workstation',
1027 'recurring_fine_rule'
1030 order_by => { circ => 'xact_start desc' },
1040 transit => $transit,
1050 __PACKAGE__->register_method(
1051 method => 'mark_item',
1052 api_name => 'open-ils.circ.mark_item_damaged',
1054 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1055 @param authtoken The login session key
1056 @param copy_id The ID of the copy to mark as damaged
1057 @return 1 on success - Event otherwise.
1060 __PACKAGE__->register_method(
1061 method => 'mark_item',
1062 api_name => 'open-ils.circ.mark_item_missing',
1064 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1065 @param authtoken The login session key
1066 @param copy_id The ID of the copy to mark as missing
1067 @return 1 on success - Event otherwise.
1070 __PACKAGE__->register_method(
1071 method => 'mark_item',
1072 api_name => 'open-ils.circ.mark_item_bindery',
1074 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1075 @param authtoken The login session key
1076 @param copy_id The ID of the copy to mark as bindery
1077 @return 1 on success - Event otherwise.
1080 __PACKAGE__->register_method(
1081 method => 'mark_item',
1082 api_name => 'open-ils.circ.mark_item_on_order',
1084 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1085 @param authtoken The login session key
1086 @param copy_id The ID of the copy to mark as on order
1087 @return 1 on success - Event otherwise.
1090 __PACKAGE__->register_method(
1091 method => 'mark_item',
1092 api_name => 'open-ils.circ.mark_item_ill',
1094 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1095 @param authtoken The login session key
1096 @param copy_id The ID of the copy to mark as inter-library loan
1097 @return 1 on success - Event otherwise.
1100 __PACKAGE__->register_method(
1101 method => 'mark_item',
1102 api_name => 'open-ils.circ.mark_item_cataloging',
1104 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1105 @param authtoken The login session key
1106 @param copy_id The ID of the copy to mark as cataloging
1107 @return 1 on success - Event otherwise.
1110 __PACKAGE__->register_method(
1111 method => 'mark_item',
1112 api_name => 'open-ils.circ.mark_item_reserves',
1114 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1115 @param authtoken The login session key
1116 @param copy_id The ID of the copy to mark as reserves
1117 @return 1 on success - Event otherwise.
1120 __PACKAGE__->register_method(
1121 method => 'mark_item',
1122 api_name => 'open-ils.circ.mark_item_discard',
1124 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1125 @param authtoken The login session key
1126 @param copy_id The ID of the copy to mark as discard
1127 @return 1 on success - Event otherwise.
1132 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1133 my $e = new_editor(authtoken=>$auth, xact =>1);
1134 return $e->die_event unless $e->checkauth;
1137 my $copy = $e->retrieve_asset_copy([
1139 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1140 or return $e->die_event;
1143 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1144 $copy->circ_lib : $copy->call_number->owning_lib;
1146 my $perm = 'MARK_ITEM_MISSING';
1147 my $stat = OILS_COPY_STATUS_MISSING;
1149 if( $self->api_name =~ /damaged/ ) {
1150 $perm = 'MARK_ITEM_DAMAGED';
1151 $stat = OILS_COPY_STATUS_DAMAGED;
1152 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1153 return $evt if $evt;
1155 } elsif ( $self->api_name =~ /bindery/ ) {
1156 $perm = 'MARK_ITEM_BINDERY';
1157 $stat = OILS_COPY_STATUS_BINDERY;
1158 } elsif ( $self->api_name =~ /on_order/ ) {
1159 $perm = 'MARK_ITEM_ON_ORDER';
1160 $stat = OILS_COPY_STATUS_ON_ORDER;
1161 } elsif ( $self->api_name =~ /ill/ ) {
1162 $perm = 'MARK_ITEM_ILL';
1163 $stat = OILS_COPY_STATUS_ILL;
1164 } elsif ( $self->api_name =~ /cataloging/ ) {
1165 $perm = 'MARK_ITEM_CATALOGING';
1166 $stat = OILS_COPY_STATUS_CATALOGING;
1167 } elsif ( $self->api_name =~ /reserves/ ) {
1168 $perm = 'MARK_ITEM_RESERVES';
1169 $stat = OILS_COPY_STATUS_RESERVES;
1170 } elsif ( $self->api_name =~ /discard/ ) {
1171 $perm = 'MARK_ITEM_DISCARD';
1172 $stat = OILS_COPY_STATUS_DISCARD;
1175 # caller may proceed if either perm is allowed
1176 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1178 $copy->status($stat);
1179 $copy->edit_date('now');
1180 $copy->editor($e->requestor->id);
1182 $e->update_asset_copy($copy) or return $e->die_event;
1184 my $holds = $e->search_action_hold_request(
1186 current_copy => $copy->id,
1187 fulfillment_time => undef,
1188 cancel_time => undef,
1194 if( $self->api_name =~ /damaged/ ) {
1195 # now that we've committed the changes, create related A/T events
1196 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1197 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1200 $logger->debug("resetting holds that target the marked copy");
1201 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1206 sub handle_mark_damaged {
1207 my($e, $copy, $owning_lib, $args) = @_;
1209 my $apply = $args->{apply_fines} || '';
1210 return undef if $apply eq 'noapply';
1212 my $new_amount = $args->{override_amount};
1213 my $new_btype = $args->{override_btype};
1214 my $new_note = $args->{override_note};
1216 # grab the last circulation
1217 my $circ = $e->search_action_circulation([
1218 { target_copy => $copy->id},
1220 order_by => {circ => "xact_start DESC"},
1222 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1226 return undef unless $circ;
1228 my $charge_price = $U->ou_ancestor_setting_value(
1229 $owning_lib, 'circ.charge_on_damaged', $e);
1231 my $proc_fee = $U->ou_ancestor_setting_value(
1232 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1234 my $void_overdue = $U->ou_ancestor_setting_value(
1235 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1237 return undef unless $charge_price or $proc_fee;
1239 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1240 my $total = $copy_price + $proc_fee;
1244 if($new_amount and $new_btype) {
1246 # Allow staff to override the amount to charge for a damaged item
1247 # Consider the case where the item is only partially damaged
1248 # This value is meant to take the place of the item price and
1249 # optional processing fee.
1251 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1252 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1253 return $evt if $evt;
1257 if($charge_price and $copy_price) {
1258 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1259 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1260 return $evt if $evt;
1264 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1265 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1266 return $evt if $evt;
1270 # the assumption is that you would not void the overdues unless you
1271 # were also charging for the item and/or applying a processing fee
1273 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1274 return $evt if $evt;
1277 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1278 return $evt if $evt;
1280 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1281 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1283 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1284 return $evt2 if $evt2;
1289 return OpenILS::Event->new('DAMAGE_CHARGE',
1300 # ----------------------------------------------------------------------
1301 __PACKAGE__->register_method(
1302 method => 'mark_item_missing_pieces',
1303 api_name => 'open-ils.circ.mark_item_missing_pieces',
1305 Changes the status of a copy to "damaged" or to a custom status based on the
1306 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1308 @param authtoken The login session key
1309 @param copy_id The ID of the copy to mark as damaged
1310 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1314 sub mark_item_missing_pieces {
1315 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1316 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1317 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1319 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1320 return $e2->die_event unless $e2->checkauth;
1323 my $copy = $e2->retrieve_asset_copy([
1325 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1326 or return $e2->die_event;
1329 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1330 $copy->circ_lib : $copy->call_number->owning_lib;
1332 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1334 #### grab the last circulation
1335 my $circ = $e2->search_action_circulation([
1336 { target_copy => $copy->id},
1338 order_by => {circ => "xact_start DESC"}
1343 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1345 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1348 my $holds = $e2->search_action_hold_request(
1350 current_copy => $copy->id,
1351 fulfillment_time => undef,
1352 cancel_time => undef,
1356 $logger->debug("resetting holds that target the marked copy");
1357 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1360 if (! $e2->commit) {
1361 return $e2->die_event;
1364 my $e = new_editor(authtoken=>$auth, xact =>1);
1365 return $e->die_event unless $e->checkauth;
1367 if (! $circ->checkin_time) { # if circ active, attempt renew
1368 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1369 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1370 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1371 $circ = $res->[0]->{payload}{'circ'};
1372 $circ->target_copy( $copy->id );
1373 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1375 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1380 'copy_id'=>$circ->target_copy,
1381 'patron_id'=>$circ->usr,
1382 'skip_deposit_fee'=>1,
1383 'skip_rental_fee'=>1
1386 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1388 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1389 $e, $copy, $e->requestor, 1 );
1391 if ($hold) { # needed for hold? then due now
1393 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1394 my $due_date = DateTime->now(time_zone => 'local');
1395 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1397 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1401 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1402 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1403 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1404 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1405 $circ = $res->[0]->{payload}{'circ'};
1407 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1413 ### Update the item status
1415 my $custom_stat = $U->ou_ancestor_setting_value(
1416 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1417 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1419 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1420 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1422 $copy->status($stat);
1423 $copy->edit_date('now');
1424 $copy->editor($e->requestor->id);
1426 $e->update_asset_copy($copy) or return $e->die_event;
1430 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1431 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1433 return OpenILS::Event->new('SUCCESS',
1437 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1438 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1443 return $e->die_event;
1451 # ----------------------------------------------------------------------
1452 __PACKAGE__->register_method(
1453 method => 'magic_fetch',
1454 api_name => 'open-ils.agent.fetch'
1457 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1460 my( $self, $conn, $auth, $args ) = @_;
1461 my $e = new_editor( authtoken => $auth );
1462 return $e->event unless $e->checkauth;
1464 my $hint = $$args{hint};
1465 my $id = $$args{id};
1467 # Is the call allowed to fetch this type of object?
1468 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1470 # Find the class the implements the given hint
1471 my ($class) = grep {
1472 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1474 $class =~ s/Fieldmapper:://og;
1475 $class =~ s/::/_/og;
1476 my $method = "retrieve_$class";
1478 my $obj = $e->$method($id) or return $e->event;
1481 # ----------------------------------------------------------------------
1484 __PACKAGE__->register_method(
1485 method => "fleshed_circ_retrieve",
1487 api_name => "open-ils.circ.fleshed.retrieve",);
1489 sub fleshed_circ_retrieve {
1490 my( $self, $client, $id ) = @_;
1491 my $e = new_editor();
1492 my $circ = $e->retrieve_action_circulation(
1498 circ => [ qw/ target_copy / ],
1499 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1500 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1501 acn => [ qw/ record / ],
1505 ) or return $e->event;
1507 my $copy = $circ->target_copy;
1508 my $vol = $copy->call_number;
1509 my $rec = $circ->target_copy->call_number->record;
1511 $vol->record($rec->id);
1512 $copy->call_number($vol->id);
1513 $circ->target_copy($copy->id);
1517 if( $rec->id == OILS_PRECAT_RECORD ) {
1521 $mvr = $U->record_to_mvr($rec);
1522 $rec->marc(''); # drop the bulky marc data
1536 __PACKAGE__->register_method(
1537 method => "test_batch_circ_events",
1538 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1541 # method for testing the behavior of a given event definition
1542 sub test_batch_circ_events {
1543 my($self, $conn, $auth, $event_def, $barcode) = @_;
1545 my $e = new_editor(authtoken => $auth);
1546 return $e->event unless $e->checkauth;
1547 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1549 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1550 or return $e->event;
1552 my $circ = $e->search_action_circulation(
1553 {target_copy => $copy->id, checkin_time => undef})->[0]
1554 or return $e->event;
1556 return undef unless $circ;
1558 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1562 __PACKAGE__->register_method(
1563 method => "fire_circ_events",
1564 api_name => "open-ils.circ.fire_circ_trigger_events",
1566 General event def runner for circ objects. If no event def ID
1567 is provided, the hook will be used to find the best event_def
1568 match based on the context org unit
1572 __PACKAGE__->register_method(
1573 method => "fire_circ_events",
1574 api_name => "open-ils.circ.fire_hold_trigger_events",
1576 General event def runner for hold objects. If no event def ID
1577 is provided, the hook will be used to find the best event_def
1578 match based on the context org unit
1582 __PACKAGE__->register_method(
1583 method => "fire_circ_events",
1584 api_name => "open-ils.circ.fire_user_trigger_events",
1586 General event def runner for user objects. If no event def ID
1587 is provided, the hook will be used to find the best event_def
1588 match based on the context org unit
1593 sub fire_circ_events {
1594 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1596 my $e = new_editor(authtoken => $auth, xact => 1);
1597 return $e->event unless $e->checkauth;
1601 if($self->api_name =~ /hold/) {
1602 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1603 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1604 } elsif($self->api_name =~ /user/) {
1605 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1606 $targets = $e->batch_retrieve_actor_user($target_ids);
1608 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1609 $targets = $e->batch_retrieve_action_circulation($target_ids);
1611 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1612 # simply making this method authoritative because of weirdness
1613 # with transaction handling in A/T code that causes rollback
1614 # failure down the line if handling many targets
1616 return undef unless @$targets;
1617 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1620 __PACKAGE__->register_method(
1621 method => "user_payments_list",
1622 api_name => "open-ils.circ.user_payments.filtered.batch",
1625 desc => q/Returns a fleshed, date-limited set of all payments a user
1626 has made. By default, ordered by payment date. Optionally
1627 ordered by other columns in the top-level "mp" object/,
1629 {desc => 'Authentication token', type => 'string'},
1630 {desc => 'User ID', type => 'number'},
1631 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1633 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1634 and the related fully-realized payment object (e.g money.cash_payment)/}
1638 sub user_payments_list {
1639 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1641 my $e = new_editor(authtoken => $auth);
1642 return $e->event unless $e->checkauth;
1644 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1645 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1647 $order_by ||= ['payment_ts'];
1649 # all payments by user, between start_date and end_date
1650 my $payments = $e->json_query({
1651 select => {mp => ['id']},
1655 fkey => 'xact', field => 'id'}
1659 '+mbt' => {usr => $user_id},
1660 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1662 order_by => {mp => $order_by}
1665 for my $payment_id (@$payments) {
1666 my $payment = $e->retrieve_money_payment([
1674 'credit_card_payment',
1689 $conn->respond($payment);
1696 __PACKAGE__->register_method(
1697 method => "retrieve_circ_chain",
1698 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1701 desc => q/Given a circulation, this returns all circulation objects
1702 that are part of the same chain of renewals./,
1704 {desc => 'Authentication token', type => 'string'},
1705 {desc => 'Circ ID', type => 'number'},
1707 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1711 __PACKAGE__->register_method(
1712 method => "retrieve_circ_chain",
1713 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1715 desc => q/Given a circulation, this returns a summary of the circulation objects
1716 that are part of the same chain of renewals./,
1718 {desc => 'Authentication token', type => 'string'},
1719 {desc => 'Circ ID', type => 'number'},
1721 return => {desc => q/Circulation Chain Summary/}
1725 sub retrieve_circ_chain {
1726 my($self, $conn, $auth, $circ_id) = @_;
1728 my $e = new_editor(authtoken => $auth);
1729 return $e->event unless $e->checkauth;
1730 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1732 if($self->api_name =~ /summary/) {
1733 return $U->create_circ_chain_summary($e, $circ_id);
1737 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1739 for my $circ_info (@$chain) {
1740 my $circ = Fieldmapper::action::circulation->new;
1741 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1742 $conn->respond($circ);
1749 __PACKAGE__->register_method(
1750 method => "retrieve_prev_circ_chain",
1751 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1754 desc => q/Given a circulation, this returns all circulation objects
1755 that are part of the previous chain of renewals./,
1757 {desc => 'Authentication token', type => 'string'},
1758 {desc => 'Circ ID', type => 'number'},
1760 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1764 __PACKAGE__->register_method(
1765 method => "retrieve_prev_circ_chain",
1766 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1768 desc => q/Given a circulation, this returns a summary of the circulation objects
1769 that are part of the previous chain of renewals./,
1771 {desc => 'Authentication token', type => 'string'},
1772 {desc => 'Circ ID', type => 'number'},
1774 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1778 sub retrieve_prev_circ_chain {
1779 my($self, $conn, $auth, $circ_id) = @_;
1781 my $e = new_editor(authtoken => $auth);
1782 return $e->event unless $e->checkauth;
1783 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1785 if($self->api_name =~ /summary/) {
1786 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1787 my $target_copy = $$first_circ{'target_copy'};
1788 my $usr = $$first_circ{'usr'};
1789 my $last_circ_from_prev_chain = $e->json_query({
1790 'select' => { 'circ' => ['id','usr'] },
1793 target_copy => $target_copy,
1794 xact_start => { '<' => $$first_circ{'xact_start'} }
1796 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1799 return undef unless $last_circ_from_prev_chain;
1800 return undef unless $$last_circ_from_prev_chain{'id'};
1801 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1802 return undef unless $sum;
1803 my $obj = Fieldmapper::action::circ_chain_summary->new;
1804 $obj->$_($sum->{$_}) for keys %$sum;
1805 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1809 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1810 my $target_copy = $$first_circ{'target_copy'};
1811 my $last_circ_from_prev_chain = $e->json_query({
1812 'select' => { 'circ' => ['id'] },
1815 target_copy => $target_copy,
1816 xact_start => { '<' => $$first_circ{'xact_start'} }
1818 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1821 return undef unless $last_circ_from_prev_chain;
1822 return undef unless $$last_circ_from_prev_chain{'id'};
1823 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1825 for my $circ_info (@$chain) {
1826 my $circ = Fieldmapper::action::circulation->new;
1827 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1828 $conn->respond($circ);
1836 __PACKAGE__->register_method(
1837 method => "get_copy_due_date",
1838 api_name => "open-ils.circ.copy.due_date.retrieve",
1841 Given a copy ID, returns the due date for the copy if it's
1842 currently circulating. Otherwise, returns null. Note, this is a public
1843 method requiring no authentication. Only the due date is exposed.
1846 {desc => 'Copy ID', type => 'number'}
1848 return => {desc => q/
1849 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1854 sub get_copy_due_date {
1855 my($self, $conn, $copy_id) = @_;
1856 my $e = new_editor();
1858 my $circ = $e->json_query({
1859 select => {circ => ['due_date']},
1862 target_copy => $copy_id,
1863 checkin_time => undef,
1865 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1866 {stop_fines => undef}
1870 })->[0] or return undef;
1872 return $circ->{due_date};
1879 # {"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}}