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::CStoreEditor q/:funcs/;
30 use OpenILS::Const qw/:const/;
31 use OpenSRF::Utils::SettingsClient;
32 use OpenILS::Application::Cat::AssetCommon;
34 my $apputils = "OpenILS::Application::AppUtils";
37 my $holdcode = "OpenILS::Application::Circ::Holds";
39 # ------------------------------------------------------------------------
40 # Top level Circ package;
41 # ------------------------------------------------------------------------
45 OpenILS::Application::Circ::Circulate->initialize();
49 __PACKAGE__->register_method(
50 method => 'retrieve_circ',
52 api_name => 'open-ils.circ.retrieve',
54 Retrieve a circ object by id
55 @param authtoken Login session key
56 @pararm circid The id of the circ object
60 my( $s, $c, $a, $i ) = @_;
61 my $e = new_editor(authtoken => $a);
62 return $e->event unless $e->checkauth;
63 my $circ = $e->retrieve_action_circulation($i) or return $e->event;
64 if( $e->requestor->id ne $circ->usr ) {
65 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
71 __PACKAGE__->register_method(
72 method => 'fetch_circ_mods',
73 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
75 my($self, $conn, $args) = @_;
76 my $mods = new_editor()->retrieve_all_config_circ_modifier;
77 return [ map {$_->code} @$mods ] unless $$args{full};
81 __PACKAGE__->register_method(
82 method => 'ranged_billing_types',
83 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
85 sub ranged_billing_types {
86 my($self, $conn, $auth, $org_id, $depth) = @_;
87 my $e = new_editor(authtoken => $auth);
88 return $e->event unless $e->checkauth;
89 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
90 return $e->search_config_billing_type(
91 {owner => $U->get_org_full_path($org_id, $depth)});
96 # ------------------------------------------------------------------------
97 # Returns an array of {circ, record} hashes checked out by the user.
98 # ------------------------------------------------------------------------
99 __PACKAGE__->register_method(
100 method => "checkouts_by_user",
101 api_name => "open-ils.circ.actor.user.checked_out",
103 NOTES => <<" NOTES");
104 Returns a list of open circulations as a pile of objects. Each object
105 contains the relevant copy, circ, and record
108 sub checkouts_by_user {
109 my($self, $client, $auth, $user_id) = @_;
111 my $e = new_editor(authtoken=>$auth);
112 return $e->event unless $e->checkauth;
114 my $circ_ids = $e->search_action_circulation(
116 checkin_time => undef,
118 {stop_fines => undef},
119 {stop_fines => ['MAXFINES','LONGOVERDUE']}
125 for my $id (@$circ_ids) {
126 my $circ = $e->retrieve_action_circulation([
130 circ => ['target_copy'],
131 acp => ['call_number'],
137 # un-flesh for consistency
138 my $c = $circ->target_copy;
139 $circ->target_copy($c->id);
141 my $cn = $c->call_number;
142 $c->call_number($cn->id);
150 record => $U->record_to_mvr($t)
160 __PACKAGE__->register_method(
161 method => "checkouts_by_user_slim",
162 api_name => "open-ils.circ.actor.user.checked_out.slim",
163 NOTES => <<" NOTES");
164 Returns a list of open circulation objects
168 sub checkouts_by_user_slim {
169 my( $self, $client, $user_session, $user_id ) = @_;
171 my( $requestor, $target, $copy, $record, $evt );
173 ( $requestor, $target, $evt ) =
174 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
177 $logger->debug( 'User ' . $requestor->id .
178 " retrieving checked out items for user " . $target->id );
180 # XXX Make the call correct..
181 return $apputils->simplereq(
183 "open-ils.cstore.direct.action.open_circulation.search.atomic",
184 { usr => $target->id, checkin_time => undef } );
185 # { usr => $target->id } );
189 __PACKAGE__->register_method(
190 method => "checkouts_by_user_opac",
191 api_name => "open-ils.circ.actor.user.checked_out.opac",);
194 sub checkouts_by_user_opac {
195 my( $self, $client, $auth, $user_id ) = @_;
197 my $e = new_editor( authtoken => $auth );
198 return $e->event unless $e->checkauth;
199 $user_id ||= $e->requestor->id;
200 return $e->event unless
201 my $patron = $e->retrieve_actor_user($user_id);
204 my $search = {usr => $user_id, stop_fines => undef};
206 if( $user_id ne $e->requestor->id ) {
207 $data = $e->search_action_circulation(
208 $search, {checkperm=>1, permorg=>$patron->home_ou})
212 $data = $e->search_action_circulation($search);
219 __PACKAGE__->register_method(
220 method => "title_from_transaction",
221 api_name => "open-ils.circ.circ_transaction.find_title",
222 NOTES => <<" NOTES");
223 Returns a mods object for the title that is linked to from the
224 copy from the hold that created the given transaction
227 sub title_from_transaction {
228 my( $self, $client, $login_session, $transactionid ) = @_;
230 my( $user, $circ, $title, $evt );
232 ( $user, $evt ) = $apputils->checkses( $login_session );
235 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
238 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
241 return $apputils->record_to_mvr($title);
244 __PACKAGE__->register_method(
245 method => "staff_age_to_lost",
246 api_name => "open-ils.circ.circulation.age_to_lost",
249 This fires a circ.staff_age_to_lost Action-Trigger event against all
250 overdue circulations in scope of the specified context library and
251 user profile, which effectively marks the associated items as Lost.
252 This is likely to be done at the end of a semester in an academic
255 @param args : circ_lib, user_profile
259 sub staff_age_to_lost {
260 my( $self, $conn, $auth, $args ) = @_;
261 my $e = new_editor(authtoken=>$auth);
262 return $e->event unless $e->checkauth;
263 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
265 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
266 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
268 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
270 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
271 my $hook = 'circ.staff_age_to_lost';
272 my $context_org = 'circ_lib';
273 my $opt_granularity = undef;
275 "checkin_time" => undef,
276 "due_date" => { "<" => "now" },
278 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
279 { "stop_fines" => undef }
283 "select" => {"au" => ["id"]},
286 "profile" => $profiles,
287 "id" => { "=" => {"+circ" => "usr"} }
291 "select" => {"aou" => ["id"]},
295 {"id" => { "=" => {"+circ" => "circ_lib"} }},
302 my $req_timeout = 10800;
303 my $chunk_size = 100;
306 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
307 my @event_ids; my @chunked_ids;
308 while (my $resp = $req->recv(timeout => $req_timeout)) {
309 push(@event_ids, $resp->content);
310 push(@chunked_ids, $resp->content);
311 if (scalar(@chunked_ids) > $chunk_size) {
312 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
316 if (scalar(@chunked_ids) > 0) {
317 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
321 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
322 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
323 } elsif($req->complete) {
324 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
325 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
327 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
328 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
335 __PACKAGE__->register_method(
336 method => "new_set_circ_lost",
337 api_name => "open-ils.circ.circulation.set_lost",
339 Sets the copy and related open circulation to lost
341 @param args : barcode
346 # ---------------------------------------------------------------------
347 # Sets a circulation to lost. updates copy status to lost
348 # applies copy and/or prcoessing fees depending on org settings
349 # ---------------------------------------------------------------------
350 sub new_set_circ_lost {
351 my( $self, $conn, $auth, $args ) = @_;
353 my $e = new_editor(authtoken=>$auth, xact=>1);
354 return $e->die_event unless $e->checkauth;
356 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
357 or return $e->die_event;
359 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
367 __PACKAGE__->register_method(
368 method => "set_circ_claims_returned",
369 api_name => "open-ils.circ.circulation.set_claims_returned",
371 desc => q/Sets the circ for a given item as claims returned
372 If a backdate is provided, overdue fines will be voided
373 back to the backdate/,
375 {desc => 'Authentication token', type => 'string'},
376 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
378 return => {desc => q/1 on success, failure event on error, and
379 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
380 configured claims return maximum/}
384 __PACKAGE__->register_method(
385 method => "set_circ_claims_returned",
386 api_name => "open-ils.circ.circulation.set_claims_returned.override",
388 desc => q/This adds support for overrideing the configured max
389 claims returned amount.
390 @see open-ils.circ.circulation.set_claims_returned./,
394 sub set_circ_claims_returned {
395 my( $self, $conn, $auth, $args, $oargs ) = @_;
397 my $e = new_editor(authtoken=>$auth, xact=>1);
398 return $e->die_event unless $e->checkauth;
400 $oargs = { all => 1 } unless defined $oargs;
402 my $barcode = $$args{barcode};
403 my $backdate = $$args{backdate};
405 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
406 or return $e->die_event;
408 my $circ = $e->search_action_circulation(
409 {checkin_time => undef, target_copy => $copy->id})->[0]
410 or return $e->die_event;
412 $backdate = $circ->due_date if $$args{use_due_date};
414 $logger->info("marking circ for item $barcode as claims returned".
415 (($backdate) ? " with backdate $backdate" : ''));
417 my $patron = $e->retrieve_actor_user($circ->usr);
418 my $max_count = $U->ou_ancestor_setting_value(
419 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
421 # If the patron has too instances of many claims returned,
422 # require an override to continue. A configured max of
423 # 0 means all attempts require an override
424 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
426 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
428 # see if we're allowed to override
429 return $e->die_event unless
430 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
434 # exit early and return the max claims return event
436 return OpenILS::Event->new(
437 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
439 patron_count => $patron->claims_returned_count,
440 max_count => $max_count
446 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
447 or return $e->die_event;
449 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
450 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
453 $backdate = cleanse_ISO8601($backdate);
455 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
456 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
457 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
459 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
460 $backdate = cleanse_ISO8601($backdate);
462 # make it look like the circ stopped at the cliams returned time
463 $circ->stop_fines_time($backdate);
464 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
468 $e->update_action_circulation($circ) or return $e->die_event;
470 # see if there is a configured post-claims-return copy status
471 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
472 $copy->status($stat);
473 $copy->edit_date('now');
474 $copy->editor($e->requestor->id);
475 $e->update_asset_copy($copy) or return $e->die_event;
478 # Check if the copy circ lib wants lost fees voided on claims
480 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
481 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
486 return $result if ($result);
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 ) = @_;
758 my $count = new_editor()->json_query({
767 where => {'+circbyyr' => {copy => $copyid}}
779 __PACKAGE__->register_method(
780 method => 'fetch_notes',
782 api_name => 'open-ils.circ.copy_note.retrieve.all',
784 Returns an array of copy note objects.
785 @param args A named hash of parameters including:
786 authtoken : Required if viewing non-public notes
787 itemid : The id of the item whose notes we want to retrieve
788 pub : True if all the caller wants are public notes
789 @return An array of note objects
792 __PACKAGE__->register_method(
793 method => 'fetch_notes',
794 api_name => 'open-ils.circ.call_number_note.retrieve.all',
795 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
797 __PACKAGE__->register_method(
798 method => 'fetch_notes',
799 api_name => 'open-ils.circ.title_note.retrieve.all',
800 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
803 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
805 my( $self, $connection, $args ) = @_;
807 my $id = $$args{itemid};
808 my $authtoken = $$args{authtoken};
811 if( $self->api_name =~ /copy/ ) {
813 return $U->cstorereq(
814 'open-ils.cstore.direct.asset.copy_note.search.atomic',
815 { owning_copy => $id, pub => 't' } );
817 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
819 return $U->cstorereq(
820 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
823 } elsif( $self->api_name =~ /call_number/ ) {
825 return $U->cstorereq(
826 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
827 { call_number => $id, pub => 't' } );
829 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
831 return $U->cstorereq(
832 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
835 } elsif( $self->api_name =~ /title/ ) {
837 return $U->cstorereq(
838 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
839 { record => $id, pub => 't' } );
841 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
843 return $U->cstorereq(
844 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
851 __PACKAGE__->register_method(
852 method => 'has_notes',
853 api_name => 'open-ils.circ.copy.has_notes');
854 __PACKAGE__->register_method(
855 method => 'has_notes',
856 api_name => 'open-ils.circ.call_number.has_notes');
857 __PACKAGE__->register_method(
858 method => 'has_notes',
859 api_name => 'open-ils.circ.title.has_notes');
863 my( $self, $conn, $authtoken, $id ) = @_;
864 my $editor = new_editor(authtoken => $authtoken);
865 return $editor->event unless $editor->checkauth;
867 my $n = $editor->search_asset_copy_note(
868 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
870 $n = $editor->search_asset_call_number_note(
871 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
873 $n = $editor->search_biblio_record_note(
874 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
881 __PACKAGE__->register_method(
882 method => 'create_copy_note',
883 api_name => 'open-ils.circ.copy_note.create',
885 Creates a new copy note
886 @param authtoken The login session key
887 @param note The note object to create
888 @return The id of the new note object
891 sub create_copy_note {
892 my( $self, $connection, $authtoken, $note ) = @_;
894 my $e = new_editor(xact=>1, authtoken=>$authtoken);
895 return $e->event unless $e->checkauth;
896 my $copy = $e->retrieve_asset_copy(
900 flesh_fields => { 'acp' => ['call_number'] }
905 return $e->event unless
906 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
908 $note->create_date('now');
909 $note->creator($e->requestor->id);
910 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
913 $e->create_asset_copy_note($note) or return $e->event;
919 __PACKAGE__->register_method(
920 method => 'delete_copy_note',
921 api_name => 'open-ils.circ.copy_note.delete',
923 Deletes an existing copy note
924 @param authtoken The login session key
925 @param noteid The id of the note to delete
926 @return 1 on success - Event otherwise.
928 sub delete_copy_note {
929 my( $self, $conn, $authtoken, $noteid ) = @_;
931 my $e = new_editor(xact=>1, authtoken=>$authtoken);
932 return $e->die_event unless $e->checkauth;
934 my $note = $e->retrieve_asset_copy_note([
938 'acpn' => [ 'owning_copy' ],
939 'acp' => [ 'call_number' ],
942 ]) or return $e->die_event;
944 if( $note->creator ne $e->requestor->id ) {
945 return $e->die_event unless
946 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
949 $e->delete_asset_copy_note($note) or return $e->die_event;
955 __PACKAGE__->register_method(
956 method => 'age_hold_rules',
957 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
961 my( $self, $conn ) = @_;
962 return new_editor()->retrieve_all_config_rules_age_hold_protect();
967 __PACKAGE__->register_method(
968 method => 'copy_details_barcode',
970 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
971 sub copy_details_barcode {
972 my( $self, $conn, $auth, $barcode ) = @_;
973 my $e = new_editor();
974 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
975 return $e->event unless $cid;
976 return copy_details( $self, $conn, $auth, $cid );
980 __PACKAGE__->register_method(
981 method => 'copy_details',
982 api_name => 'open-ils.circ.copy_details.retrieve');
985 my( $self, $conn, $auth, $copy_id ) = @_;
986 my $e = new_editor(authtoken=>$auth);
987 return $e->event unless $e->checkauth;
989 my $flesh = { flesh => 1 };
991 my $copy = $e->retrieve_asset_copy(
997 acp => ['call_number','parts','peer_record_maps','floating'],
998 acn => ['record','prefix','suffix','label_class']
1001 ]) or return $e->event;
1004 # De-flesh the copy for backwards compatibility
1006 my $vol = $copy->call_number;
1008 $copy->call_number($vol->id);
1009 my $record = $vol->record;
1011 $vol->record($record->id);
1012 $mvr = $U->record_to_mvr($record);
1017 my $hold = $e->search_action_hold_request(
1019 current_copy => $copy_id,
1020 capture_time => { "!=" => undef },
1021 fulfillment_time => undef,
1022 cancel_time => undef,
1026 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1028 my $transit = $e->search_action_transit_copy(
1029 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1031 # find the latest circ, open or closed
1032 my $circ = $e->search_action_circulation(
1034 { target_copy => $copy_id },
1040 'checkin_workstation',
1043 'recurring_fine_rule'
1046 order_by => { circ => 'xact_start desc' },
1056 transit => $transit,
1066 __PACKAGE__->register_method(
1067 method => 'mark_item',
1068 api_name => 'open-ils.circ.mark_item_damaged',
1070 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1071 @param authtoken The login session key
1072 @param copy_id The ID of the copy to mark as damaged
1073 @return 1 on success - Event otherwise.
1076 __PACKAGE__->register_method(
1077 method => 'mark_item',
1078 api_name => 'open-ils.circ.mark_item_missing',
1080 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1081 @param authtoken The login session key
1082 @param copy_id The ID of the copy to mark as missing
1083 @return 1 on success - Event otherwise.
1086 __PACKAGE__->register_method(
1087 method => 'mark_item',
1088 api_name => 'open-ils.circ.mark_item_bindery',
1090 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1091 @param authtoken The login session key
1092 @param copy_id The ID of the copy to mark as bindery
1093 @return 1 on success - Event otherwise.
1096 __PACKAGE__->register_method(
1097 method => 'mark_item',
1098 api_name => 'open-ils.circ.mark_item_on_order',
1100 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1101 @param authtoken The login session key
1102 @param copy_id The ID of the copy to mark as on order
1103 @return 1 on success - Event otherwise.
1106 __PACKAGE__->register_method(
1107 method => 'mark_item',
1108 api_name => 'open-ils.circ.mark_item_ill',
1110 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1111 @param authtoken The login session key
1112 @param copy_id The ID of the copy to mark as inter-library loan
1113 @return 1 on success - Event otherwise.
1116 __PACKAGE__->register_method(
1117 method => 'mark_item',
1118 api_name => 'open-ils.circ.mark_item_cataloging',
1120 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1121 @param authtoken The login session key
1122 @param copy_id The ID of the copy to mark as cataloging
1123 @return 1 on success - Event otherwise.
1126 __PACKAGE__->register_method(
1127 method => 'mark_item',
1128 api_name => 'open-ils.circ.mark_item_reserves',
1130 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1131 @param authtoken The login session key
1132 @param copy_id The ID of the copy to mark as reserves
1133 @return 1 on success - Event otherwise.
1136 __PACKAGE__->register_method(
1137 method => 'mark_item',
1138 api_name => 'open-ils.circ.mark_item_discard',
1140 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1141 @param authtoken The login session key
1142 @param copy_id The ID of the copy to mark as discard
1143 @return 1 on success - Event otherwise.
1148 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1149 my $e = new_editor(authtoken=>$auth, xact =>1);
1150 return $e->die_event unless $e->checkauth;
1153 my $copy = $e->retrieve_asset_copy([
1155 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1156 or return $e->die_event;
1159 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1160 $copy->circ_lib : $copy->call_number->owning_lib;
1162 my $perm = 'MARK_ITEM_MISSING';
1163 my $stat = OILS_COPY_STATUS_MISSING;
1165 if( $self->api_name =~ /damaged/ ) {
1166 $perm = 'MARK_ITEM_DAMAGED';
1167 $stat = OILS_COPY_STATUS_DAMAGED;
1168 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1169 return $evt if $evt;
1171 } elsif ( $self->api_name =~ /bindery/ ) {
1172 $perm = 'MARK_ITEM_BINDERY';
1173 $stat = OILS_COPY_STATUS_BINDERY;
1174 } elsif ( $self->api_name =~ /on_order/ ) {
1175 $perm = 'MARK_ITEM_ON_ORDER';
1176 $stat = OILS_COPY_STATUS_ON_ORDER;
1177 } elsif ( $self->api_name =~ /ill/ ) {
1178 $perm = 'MARK_ITEM_ILL';
1179 $stat = OILS_COPY_STATUS_ILL;
1180 } elsif ( $self->api_name =~ /cataloging/ ) {
1181 $perm = 'MARK_ITEM_CATALOGING';
1182 $stat = OILS_COPY_STATUS_CATALOGING;
1183 } elsif ( $self->api_name =~ /reserves/ ) {
1184 $perm = 'MARK_ITEM_RESERVES';
1185 $stat = OILS_COPY_STATUS_RESERVES;
1186 } elsif ( $self->api_name =~ /discard/ ) {
1187 $perm = 'MARK_ITEM_DISCARD';
1188 $stat = OILS_COPY_STATUS_DISCARD;
1191 # caller may proceed if either perm is allowed
1192 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1194 $copy->status($stat);
1195 $copy->edit_date('now');
1196 $copy->editor($e->requestor->id);
1198 $e->update_asset_copy($copy) or return $e->die_event;
1200 my $holds = $e->search_action_hold_request(
1202 current_copy => $copy->id,
1203 fulfillment_time => undef,
1204 cancel_time => undef,
1210 if( $self->api_name =~ /damaged/ ) {
1211 # now that we've committed the changes, create related A/T events
1212 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1213 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1216 $logger->debug("resetting holds that target the marked copy");
1217 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1222 sub handle_mark_damaged {
1223 my($e, $copy, $owning_lib, $args) = @_;
1225 my $apply = $args->{apply_fines} || '';
1226 return undef if $apply eq 'noapply';
1228 my $new_amount = $args->{override_amount};
1229 my $new_btype = $args->{override_btype};
1230 my $new_note = $args->{override_note};
1232 # grab the last circulation
1233 my $circ = $e->search_action_circulation([
1234 { target_copy => $copy->id},
1236 order_by => {circ => "xact_start DESC"},
1238 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1242 return undef unless $circ;
1244 my $charge_price = $U->ou_ancestor_setting_value(
1245 $owning_lib, 'circ.charge_on_damaged', $e);
1247 my $proc_fee = $U->ou_ancestor_setting_value(
1248 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1250 my $void_overdue = $U->ou_ancestor_setting_value(
1251 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1253 return undef unless $charge_price or $proc_fee;
1255 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1256 my $total = $copy_price + $proc_fee;
1260 if($new_amount and $new_btype) {
1262 # Allow staff to override the amount to charge for a damaged item
1263 # Consider the case where the item is only partially damaged
1264 # This value is meant to take the place of the item price and
1265 # optional processing fee.
1267 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1268 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1269 return $evt if $evt;
1273 if($charge_price and $copy_price) {
1274 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1275 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1276 return $evt if $evt;
1280 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1281 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1282 return $evt if $evt;
1286 # the assumption is that you would not void the overdues unless you
1287 # were also charging for the item and/or applying a processing fee
1289 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1290 return $evt if $evt;
1293 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1294 return $evt if $evt;
1296 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1297 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1299 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1300 return $evt2 if $evt2;
1305 return OpenILS::Event->new('DAMAGE_CHARGE',
1316 # ----------------------------------------------------------------------
1317 __PACKAGE__->register_method(
1318 method => 'mark_item_missing_pieces',
1319 api_name => 'open-ils.circ.mark_item_missing_pieces',
1321 Changes the status of a copy to "damaged" or to a custom status based on the
1322 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1324 @param authtoken The login session key
1325 @param copy_id The ID of the copy to mark as damaged
1326 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1330 sub mark_item_missing_pieces {
1331 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1332 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1333 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1335 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1336 return $e2->die_event unless $e2->checkauth;
1339 my $copy = $e2->retrieve_asset_copy([
1341 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1342 or return $e2->die_event;
1345 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1346 $copy->circ_lib : $copy->call_number->owning_lib;
1348 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1350 #### grab the last circulation
1351 my $circ = $e2->search_action_circulation([
1352 { target_copy => $copy->id},
1354 order_by => {circ => "xact_start DESC"}
1359 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1361 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1364 my $holds = $e2->search_action_hold_request(
1366 current_copy => $copy->id,
1367 fulfillment_time => undef,
1368 cancel_time => undef,
1372 $logger->debug("resetting holds that target the marked copy");
1373 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1376 if (! $e2->commit) {
1377 return $e2->die_event;
1380 my $e = new_editor(authtoken=>$auth, xact =>1);
1381 return $e->die_event unless $e->checkauth;
1383 if (! $circ->checkin_time) { # if circ active, attempt renew
1384 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1385 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1386 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1387 $circ = $res->[0]->{payload}{'circ'};
1388 $circ->target_copy( $copy->id );
1389 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1391 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1396 'copy_id'=>$circ->target_copy,
1397 'patron_id'=>$circ->usr,
1398 'skip_deposit_fee'=>1,
1399 'skip_rental_fee'=>1
1402 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1404 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1405 $e, $copy, $e->requestor, 1 );
1407 if ($hold) { # needed for hold? then due now
1409 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1410 my $due_date = DateTime->now(time_zone => 'local');
1411 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1413 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1417 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1418 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1419 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1420 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1421 $circ = $res->[0]->{payload}{'circ'};
1423 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1429 ### Update the item status
1431 my $custom_stat = $U->ou_ancestor_setting_value(
1432 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1433 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1435 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1436 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1438 $copy->status($stat);
1439 $copy->edit_date('now');
1440 $copy->editor($e->requestor->id);
1442 $e->update_asset_copy($copy) or return $e->die_event;
1446 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1447 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1449 return OpenILS::Event->new('SUCCESS',
1453 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1454 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1459 return $e->die_event;
1467 # ----------------------------------------------------------------------
1468 __PACKAGE__->register_method(
1469 method => 'magic_fetch',
1470 api_name => 'open-ils.agent.fetch'
1473 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1476 my( $self, $conn, $auth, $args ) = @_;
1477 my $e = new_editor( authtoken => $auth );
1478 return $e->event unless $e->checkauth;
1480 my $hint = $$args{hint};
1481 my $id = $$args{id};
1483 # Is the call allowed to fetch this type of object?
1484 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1486 # Find the class the implements the given hint
1487 my ($class) = grep {
1488 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1490 $class =~ s/Fieldmapper:://og;
1491 $class =~ s/::/_/og;
1492 my $method = "retrieve_$class";
1494 my $obj = $e->$method($id) or return $e->event;
1497 # ----------------------------------------------------------------------
1500 __PACKAGE__->register_method(
1501 method => "fleshed_circ_retrieve",
1503 api_name => "open-ils.circ.fleshed.retrieve",);
1505 sub fleshed_circ_retrieve {
1506 my( $self, $client, $id ) = @_;
1507 my $e = new_editor();
1508 my $circ = $e->retrieve_action_circulation(
1514 circ => [ qw/ target_copy / ],
1515 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1516 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1517 acn => [ qw/ record / ],
1521 ) or return $e->event;
1523 my $copy = $circ->target_copy;
1524 my $vol = $copy->call_number;
1525 my $rec = $circ->target_copy->call_number->record;
1527 $vol->record($rec->id);
1528 $copy->call_number($vol->id);
1529 $circ->target_copy($copy->id);
1533 if( $rec->id == OILS_PRECAT_RECORD ) {
1537 $mvr = $U->record_to_mvr($rec);
1538 $rec->marc(''); # drop the bulky marc data
1552 __PACKAGE__->register_method(
1553 method => "test_batch_circ_events",
1554 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1557 # method for testing the behavior of a given event definition
1558 sub test_batch_circ_events {
1559 my($self, $conn, $auth, $event_def, $barcode) = @_;
1561 my $e = new_editor(authtoken => $auth);
1562 return $e->event unless $e->checkauth;
1563 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1565 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1566 or return $e->event;
1568 my $circ = $e->search_action_circulation(
1569 {target_copy => $copy->id, checkin_time => undef})->[0]
1570 or return $e->event;
1572 return undef unless $circ;
1574 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1578 __PACKAGE__->register_method(
1579 method => "fire_circ_events",
1580 api_name => "open-ils.circ.fire_circ_trigger_events",
1582 General event def runner for circ objects. If no event def ID
1583 is provided, the hook will be used to find the best event_def
1584 match based on the context org unit
1588 __PACKAGE__->register_method(
1589 method => "fire_circ_events",
1590 api_name => "open-ils.circ.fire_hold_trigger_events",
1592 General event def runner for hold objects. If no event def ID
1593 is provided, the hook will be used to find the best event_def
1594 match based on the context org unit
1598 __PACKAGE__->register_method(
1599 method => "fire_circ_events",
1600 api_name => "open-ils.circ.fire_user_trigger_events",
1602 General event def runner for user objects. If no event def ID
1603 is provided, the hook will be used to find the best event_def
1604 match based on the context org unit
1609 sub fire_circ_events {
1610 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1612 my $e = new_editor(authtoken => $auth, xact => 1);
1613 return $e->event unless $e->checkauth;
1617 if($self->api_name =~ /hold/) {
1618 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1619 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1620 } elsif($self->api_name =~ /user/) {
1621 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1622 $targets = $e->batch_retrieve_actor_user($target_ids);
1624 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1625 $targets = $e->batch_retrieve_action_circulation($target_ids);
1627 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1628 # simply making this method authoritative because of weirdness
1629 # with transaction handling in A/T code that causes rollback
1630 # failure down the line if handling many targets
1632 return undef unless @$targets;
1633 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1636 __PACKAGE__->register_method(
1637 method => "user_payments_list",
1638 api_name => "open-ils.circ.user_payments.filtered.batch",
1641 desc => q/Returns a fleshed, date-limited set of all payments a user
1642 has made. By default, ordered by payment date. Optionally
1643 ordered by other columns in the top-level "mp" object/,
1645 {desc => 'Authentication token', type => 'string'},
1646 {desc => 'User ID', type => 'number'},
1647 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1649 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1650 and the related fully-realized payment object (e.g money.cash_payment)/}
1654 sub user_payments_list {
1655 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1657 my $e = new_editor(authtoken => $auth);
1658 return $e->event unless $e->checkauth;
1660 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1661 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1663 $order_by ||= ['payment_ts'];
1665 # all payments by user, between start_date and end_date
1666 my $payments = $e->json_query({
1667 select => {mp => ['id']},
1671 fkey => 'xact', field => 'id'}
1675 '+mbt' => {usr => $user_id},
1676 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1678 order_by => {mp => $order_by}
1681 for my $payment_id (@$payments) {
1682 my $payment = $e->retrieve_money_payment([
1690 'credit_card_payment',
1705 $conn->respond($payment);
1712 __PACKAGE__->register_method(
1713 method => "retrieve_circ_chain",
1714 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1717 desc => q/Given a circulation, this returns all circulation objects
1718 that are part of the same chain of renewals./,
1720 {desc => 'Authentication token', type => 'string'},
1721 {desc => 'Circ ID', type => 'number'},
1723 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1727 __PACKAGE__->register_method(
1728 method => "retrieve_circ_chain",
1729 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1731 desc => q/Given a circulation, this returns a summary of the circulation objects
1732 that are part of the same chain of renewals./,
1734 {desc => 'Authentication token', type => 'string'},
1735 {desc => 'Circ ID', type => 'number'},
1737 return => {desc => q/Circulation Chain Summary/}
1741 sub retrieve_circ_chain {
1742 my($self, $conn, $auth, $circ_id) = @_;
1744 my $e = new_editor(authtoken => $auth);
1745 return $e->event unless $e->checkauth;
1746 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1748 if($self->api_name =~ /summary/) {
1749 return $U->create_circ_chain_summary($e, $circ_id);
1753 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1755 for my $circ_info (@$chain) {
1756 my $circ = Fieldmapper::action::circulation->new;
1757 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1758 $conn->respond($circ);
1765 __PACKAGE__->register_method(
1766 method => "retrieve_prev_circ_chain",
1767 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1770 desc => q/Given a circulation, this returns all circulation objects
1771 that are part of the previous chain of renewals./,
1773 {desc => 'Authentication token', type => 'string'},
1774 {desc => 'Circ ID', type => 'number'},
1776 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1780 __PACKAGE__->register_method(
1781 method => "retrieve_prev_circ_chain",
1782 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1784 desc => q/Given a circulation, this returns a summary of the circulation objects
1785 that are part of the previous chain of renewals./,
1787 {desc => 'Authentication token', type => 'string'},
1788 {desc => 'Circ ID', type => 'number'},
1790 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1794 sub retrieve_prev_circ_chain {
1795 my($self, $conn, $auth, $circ_id) = @_;
1797 my $e = new_editor(authtoken => $auth);
1798 return $e->event unless $e->checkauth;
1799 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1801 if($self->api_name =~ /summary/) {
1802 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1803 my $target_copy = $$first_circ{'target_copy'};
1804 my $usr = $$first_circ{'usr'};
1805 my $last_circ_from_prev_chain = $e->json_query({
1806 'select' => { 'circ' => ['id','usr'] },
1809 target_copy => $target_copy,
1810 xact_start => { '<' => $$first_circ{'xact_start'} }
1812 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1815 return undef unless $last_circ_from_prev_chain;
1816 return undef unless $$last_circ_from_prev_chain{'id'};
1817 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1818 return undef unless $sum;
1819 my $obj = Fieldmapper::action::circ_chain_summary->new;
1820 $obj->$_($sum->{$_}) for keys %$sum;
1821 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1825 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1826 my $target_copy = $$first_circ{'target_copy'};
1827 my $last_circ_from_prev_chain = $e->json_query({
1828 'select' => { 'circ' => ['id'] },
1831 target_copy => $target_copy,
1832 xact_start => { '<' => $$first_circ{'xact_start'} }
1834 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1837 return undef unless $last_circ_from_prev_chain;
1838 return undef unless $$last_circ_from_prev_chain{'id'};
1839 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1841 for my $circ_info (@$chain) {
1842 my $circ = Fieldmapper::action::circulation->new;
1843 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1844 $conn->respond($circ);
1852 __PACKAGE__->register_method(
1853 method => "get_copy_due_date",
1854 api_name => "open-ils.circ.copy.due_date.retrieve",
1857 Given a copy ID, returns the due date for the copy if it's
1858 currently circulating. Otherwise, returns null. Note, this is a public
1859 method requiring no authentication. Only the due date is exposed.
1862 {desc => 'Copy ID', type => 'number'}
1864 return => {desc => q/
1865 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1870 sub get_copy_due_date {
1871 my($self, $conn, $copy_id) = @_;
1872 my $e = new_editor();
1874 my $circ = $e->json_query({
1875 select => {circ => ['due_date']},
1878 target_copy => $copy_id,
1879 checkin_time => undef,
1881 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1882 {stop_fines => undef}
1886 })->[0] or return undef;
1888 return $circ->{due_date};
1895 # {"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}}