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;
483 __PACKAGE__->register_method(
484 method => "post_checkin_backdate_circ",
485 api_name => "open-ils.circ.post_checkin_backdate",
487 desc => q/Back-date an already checked in circulation/,
489 {desc => 'Authentication token', type => 'string'},
490 {desc => 'Circ ID', type => 'number'},
491 {desc => 'ISO8601 backdate', type => 'string'},
493 return => {desc => q/1 on success, failure event on error/}
497 __PACKAGE__->register_method(
498 method => "post_checkin_backdate_circ",
499 api_name => "open-ils.circ.post_checkin_backdate.batch",
502 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
504 {desc => 'Authentication token', type => 'string'},
505 {desc => 'List of Circ ID', type => 'array'},
506 {desc => 'ISO8601 backdate', type => 'string'},
508 return => {desc => q/Set of: 1 on success, failure event on error/}
513 sub post_checkin_backdate_circ {
514 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
515 my $e = new_editor(authtoken=>$auth);
516 return $e->die_event unless $e->checkauth;
517 if($self->api_name =~ /batch/) {
518 foreach my $c (@$circ_id) {
519 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
522 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
530 sub post_checkin_backdate_circ_impl {
531 my($e, $circ_id, $backdate) = @_;
535 my $circ = $e->retrieve_action_circulation($circ_id)
536 or return $e->die_event;
538 # anyone with checkin perms can backdate (more restrictive?)
539 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
541 # don't allow back-dating an open circulation
542 return OpenILS::Event->new('BAD_PARAMS') unless
543 $backdate and $circ->checkin_time;
545 # update the checkin and stop_fines times to reflect the new backdate
546 $circ->stop_fines_time(cleanse_ISO8601($backdate));
547 $circ->checkin_time(cleanse_ISO8601($backdate));
548 $e->update_action_circulation($circ) or return $e->die_event;
550 # now void the overdues "erased" by the back-dating
551 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
554 # If the circ was closed before and the balance owned !=0, re-open the transaction
555 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
564 __PACKAGE__->register_method (
565 method => 'set_circ_due_date',
566 api_name => 'open-ils.circ.circulation.due_date.update',
568 Updates the due_date on the given circ
570 @param circid The id of the circ to update
571 @param date The timestamp of the new due date
575 sub set_circ_due_date {
576 my( $self, $conn, $auth, $circ_id, $date ) = @_;
578 my $e = new_editor(xact=>1, authtoken=>$auth);
579 return $e->die_event unless $e->checkauth;
580 my $circ = $e->retrieve_action_circulation($circ_id)
581 or return $e->die_event;
583 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
584 $date = cleanse_ISO8601($date);
586 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
587 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
588 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
589 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
592 $circ->due_date($date);
593 $e->update_action_circulation($circ) or return $e->die_event;
600 __PACKAGE__->register_method(
601 method => "create_in_house_use",
602 api_name => 'open-ils.circ.in_house_use.create',
604 Creates an in-house use action.
605 @param $authtoken The login session key
606 @param params A hash of params including
607 'location' The org unit id where the in-house use occurs
608 'copyid' The copy in question
609 'count' The number of in-house uses to apply to this copy
610 @return An array of id's representing the id's of the newly created
611 in-house use objects or an event on an error
614 __PACKAGE__->register_method(
615 method => "create_in_house_use",
616 api_name => 'open-ils.circ.non_cat_in_house_use.create',
620 sub create_in_house_use {
621 my( $self, $client, $auth, $params ) = @_;
624 my $org = $params->{location};
625 my $copyid = $params->{copyid};
626 my $count = $params->{count} || 1;
627 my $nc_type = $params->{non_cat_type};
628 my $use_time = $params->{use_time} || 'now';
630 my $e = new_editor(xact=>1,authtoken=>$auth);
631 return $e->event unless $e->checkauth;
632 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
634 my $non_cat = 1 if $self->api_name =~ /non_cat/;
638 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
640 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
646 if( $use_time ne 'now' ) {
647 $use_time = cleanse_ISO8601($use_time);
648 $logger->debug("in_house_use setting use time to $use_time");
659 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
660 $ihu->item_type($nc_type);
661 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
662 $cmeth = "create_action_non_cat_in_house_use";
665 $ihu = Fieldmapper::action::in_house_use->new;
667 $method = 'open-ils.storage.direct.action.in_house_use.create';
668 $cmeth = "create_action_in_house_use";
671 $ihu->staff($e->requestor->id);
672 $ihu->org_unit($org);
673 $ihu->use_time($use_time);
675 $ihu = $e->$cmeth($ihu) or return $e->event;
676 push( @ids, $ihu->id );
687 __PACKAGE__->register_method(
688 method => "view_circs",
689 api_name => "open-ils.circ.copy_checkout_history.retrieve",
691 Retrieves the last X circs for a given copy
692 @param authtoken The login session key
693 @param copyid The copy to check
694 @param count How far to go back in the item history
695 @return An array of circ ids
698 # ----------------------------------------------------------------------
699 # Returns $count most recent circs. If count exceeds the configured
700 # max, use the configured max instead
701 # ----------------------------------------------------------------------
703 my( $self, $client, $authtoken, $copyid, $count ) = @_;
705 my $e = new_editor(authtoken => $authtoken);
706 return $e->event unless $e->checkauth;
708 my $copy = $e->retrieve_asset_copy([
711 flesh_fields => {acp => ['call_number']}
713 ]) or return $e->event;
715 return $e->event unless $e->allowed(
716 'VIEW_COPY_CHECKOUT_HISTORY',
717 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
718 $copy->circ_lib : $copy->call_number->owning_lib);
720 my $max_history = $U->ou_ancestor_setting_value(
721 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
723 if(defined $max_history) {
724 $count = $max_history unless defined $count and $count < $max_history;
726 $count = 4 unless defined $count;
729 return $e->search_action_circulation([
730 {target_copy => $copyid},
731 {limit => $count, order_by => { circ => "xact_start DESC" }}
736 __PACKAGE__->register_method(
737 method => "circ_count",
738 api_name => "open-ils.circ.circulation.count",
740 Returns the number of times the item has circulated
741 @param copyid The copy to check
745 my( $self, $client, $copyid, $range ) = @_;
747 return $U->simplereq(
749 'open-ils.storage.asset.copy.circ_count',
756 __PACKAGE__->register_method(
757 method => 'fetch_notes',
759 api_name => 'open-ils.circ.copy_note.retrieve.all',
761 Returns an array of copy note objects.
762 @param args A named hash of parameters including:
763 authtoken : Required if viewing non-public notes
764 itemid : The id of the item whose notes we want to retrieve
765 pub : True if all the caller wants are public notes
766 @return An array of note objects
769 __PACKAGE__->register_method(
770 method => 'fetch_notes',
771 api_name => 'open-ils.circ.call_number_note.retrieve.all',
772 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
774 __PACKAGE__->register_method(
775 method => 'fetch_notes',
776 api_name => 'open-ils.circ.title_note.retrieve.all',
777 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
780 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
782 my( $self, $connection, $args ) = @_;
784 my $id = $$args{itemid};
785 my $authtoken = $$args{authtoken};
788 if( $self->api_name =~ /copy/ ) {
790 return $U->cstorereq(
791 'open-ils.cstore.direct.asset.copy_note.search.atomic',
792 { owning_copy => $id, pub => 't' } );
794 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
796 return $U->cstorereq(
797 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
800 } elsif( $self->api_name =~ /call_number/ ) {
802 return $U->cstorereq(
803 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
804 { call_number => $id, pub => 't' } );
806 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
808 return $U->cstorereq(
809 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
812 } elsif( $self->api_name =~ /title/ ) {
814 return $U->cstorereq(
815 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
816 { record => $id, pub => 't' } );
818 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
820 return $U->cstorereq(
821 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
828 __PACKAGE__->register_method(
829 method => 'has_notes',
830 api_name => 'open-ils.circ.copy.has_notes');
831 __PACKAGE__->register_method(
832 method => 'has_notes',
833 api_name => 'open-ils.circ.call_number.has_notes');
834 __PACKAGE__->register_method(
835 method => 'has_notes',
836 api_name => 'open-ils.circ.title.has_notes');
840 my( $self, $conn, $authtoken, $id ) = @_;
841 my $editor = new_editor(authtoken => $authtoken);
842 return $editor->event unless $editor->checkauth;
844 my $n = $editor->search_asset_copy_note(
845 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
847 $n = $editor->search_asset_call_number_note(
848 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
850 $n = $editor->search_biblio_record_note(
851 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
858 __PACKAGE__->register_method(
859 method => 'create_copy_note',
860 api_name => 'open-ils.circ.copy_note.create',
862 Creates a new copy note
863 @param authtoken The login session key
864 @param note The note object to create
865 @return The id of the new note object
868 sub create_copy_note {
869 my( $self, $connection, $authtoken, $note ) = @_;
871 my $e = new_editor(xact=>1, authtoken=>$authtoken);
872 return $e->event unless $e->checkauth;
873 my $copy = $e->retrieve_asset_copy(
877 flesh_fields => { 'acp' => ['call_number'] }
882 return $e->event unless
883 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
885 $note->create_date('now');
886 $note->creator($e->requestor->id);
887 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
890 $e->create_asset_copy_note($note) or return $e->event;
896 __PACKAGE__->register_method(
897 method => 'delete_copy_note',
898 api_name => 'open-ils.circ.copy_note.delete',
900 Deletes an existing copy note
901 @param authtoken The login session key
902 @param noteid The id of the note to delete
903 @return 1 on success - Event otherwise.
905 sub delete_copy_note {
906 my( $self, $conn, $authtoken, $noteid ) = @_;
908 my $e = new_editor(xact=>1, authtoken=>$authtoken);
909 return $e->die_event unless $e->checkauth;
911 my $note = $e->retrieve_asset_copy_note([
915 'acpn' => [ 'owning_copy' ],
916 'acp' => [ 'call_number' ],
919 ]) or return $e->die_event;
921 if( $note->creator ne $e->requestor->id ) {
922 return $e->die_event unless
923 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
926 $e->delete_asset_copy_note($note) or return $e->die_event;
932 __PACKAGE__->register_method(
933 method => 'age_hold_rules',
934 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
938 my( $self, $conn ) = @_;
939 return new_editor()->retrieve_all_config_rules_age_hold_protect();
944 __PACKAGE__->register_method(
945 method => 'copy_details_barcode',
947 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
948 sub copy_details_barcode {
949 my( $self, $conn, $auth, $barcode ) = @_;
950 my $e = new_editor();
951 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
952 return $e->event unless $cid;
953 return copy_details( $self, $conn, $auth, $cid );
957 __PACKAGE__->register_method(
958 method => 'copy_details',
959 api_name => 'open-ils.circ.copy_details.retrieve');
962 my( $self, $conn, $auth, $copy_id ) = @_;
963 my $e = new_editor(authtoken=>$auth);
964 return $e->event unless $e->checkauth;
966 my $flesh = { flesh => 1 };
968 my $copy = $e->retrieve_asset_copy(
974 acp => ['call_number','parts','peer_record_maps','floating'],
975 acn => ['record','prefix','suffix','label_class']
978 ]) or return $e->event;
981 # De-flesh the copy for backwards compatibility
983 my $vol = $copy->call_number;
985 $copy->call_number($vol->id);
986 my $record = $vol->record;
988 $vol->record($record->id);
989 $mvr = $U->record_to_mvr($record);
994 my $hold = $e->search_action_hold_request(
996 current_copy => $copy_id,
997 capture_time => { "!=" => undef },
998 fulfillment_time => undef,
999 cancel_time => undef,
1003 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1005 my $transit = $e->search_action_transit_copy(
1006 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1008 # find the latest circ, open or closed
1009 my $circ = $e->search_action_circulation(
1011 { target_copy => $copy_id },
1017 'checkin_workstation',
1020 'recurring_fine_rule'
1023 order_by => { circ => 'xact_start desc' },
1033 transit => $transit,
1043 __PACKAGE__->register_method(
1044 method => 'mark_item',
1045 api_name => 'open-ils.circ.mark_item_damaged',
1047 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1048 @param authtoken The login session key
1049 @param copy_id The ID of the copy to mark as damaged
1050 @return 1 on success - Event otherwise.
1053 __PACKAGE__->register_method(
1054 method => 'mark_item',
1055 api_name => 'open-ils.circ.mark_item_missing',
1057 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1058 @param authtoken The login session key
1059 @param copy_id The ID of the copy to mark as missing
1060 @return 1 on success - Event otherwise.
1063 __PACKAGE__->register_method(
1064 method => 'mark_item',
1065 api_name => 'open-ils.circ.mark_item_bindery',
1067 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1068 @param authtoken The login session key
1069 @param copy_id The ID of the copy to mark as bindery
1070 @return 1 on success - Event otherwise.
1073 __PACKAGE__->register_method(
1074 method => 'mark_item',
1075 api_name => 'open-ils.circ.mark_item_on_order',
1077 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1078 @param authtoken The login session key
1079 @param copy_id The ID of the copy to mark as on order
1080 @return 1 on success - Event otherwise.
1083 __PACKAGE__->register_method(
1084 method => 'mark_item',
1085 api_name => 'open-ils.circ.mark_item_ill',
1087 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1088 @param authtoken The login session key
1089 @param copy_id The ID of the copy to mark as inter-library loan
1090 @return 1 on success - Event otherwise.
1093 __PACKAGE__->register_method(
1094 method => 'mark_item',
1095 api_name => 'open-ils.circ.mark_item_cataloging',
1097 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1098 @param authtoken The login session key
1099 @param copy_id The ID of the copy to mark as cataloging
1100 @return 1 on success - Event otherwise.
1103 __PACKAGE__->register_method(
1104 method => 'mark_item',
1105 api_name => 'open-ils.circ.mark_item_reserves',
1107 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1108 @param authtoken The login session key
1109 @param copy_id The ID of the copy to mark as reserves
1110 @return 1 on success - Event otherwise.
1113 __PACKAGE__->register_method(
1114 method => 'mark_item',
1115 api_name => 'open-ils.circ.mark_item_discard',
1117 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1118 @param authtoken The login session key
1119 @param copy_id The ID of the copy to mark as discard
1120 @return 1 on success - Event otherwise.
1125 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1126 my $e = new_editor(authtoken=>$auth, xact =>1);
1127 return $e->die_event unless $e->checkauth;
1130 my $copy = $e->retrieve_asset_copy([
1132 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1133 or return $e->die_event;
1136 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1137 $copy->circ_lib : $copy->call_number->owning_lib;
1139 my $perm = 'MARK_ITEM_MISSING';
1140 my $stat = OILS_COPY_STATUS_MISSING;
1142 if( $self->api_name =~ /damaged/ ) {
1143 $perm = 'MARK_ITEM_DAMAGED';
1144 $stat = OILS_COPY_STATUS_DAMAGED;
1145 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1146 return $evt if $evt;
1148 } elsif ( $self->api_name =~ /bindery/ ) {
1149 $perm = 'MARK_ITEM_BINDERY';
1150 $stat = OILS_COPY_STATUS_BINDERY;
1151 } elsif ( $self->api_name =~ /on_order/ ) {
1152 $perm = 'MARK_ITEM_ON_ORDER';
1153 $stat = OILS_COPY_STATUS_ON_ORDER;
1154 } elsif ( $self->api_name =~ /ill/ ) {
1155 $perm = 'MARK_ITEM_ILL';
1156 $stat = OILS_COPY_STATUS_ILL;
1157 } elsif ( $self->api_name =~ /cataloging/ ) {
1158 $perm = 'MARK_ITEM_CATALOGING';
1159 $stat = OILS_COPY_STATUS_CATALOGING;
1160 } elsif ( $self->api_name =~ /reserves/ ) {
1161 $perm = 'MARK_ITEM_RESERVES';
1162 $stat = OILS_COPY_STATUS_RESERVES;
1163 } elsif ( $self->api_name =~ /discard/ ) {
1164 $perm = 'MARK_ITEM_DISCARD';
1165 $stat = OILS_COPY_STATUS_DISCARD;
1168 # caller may proceed if either perm is allowed
1169 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1171 $copy->status($stat);
1172 $copy->edit_date('now');
1173 $copy->editor($e->requestor->id);
1175 $e->update_asset_copy($copy) or return $e->die_event;
1177 my $holds = $e->search_action_hold_request(
1179 current_copy => $copy->id,
1180 fulfillment_time => undef,
1181 cancel_time => undef,
1187 if( $self->api_name =~ /damaged/ ) {
1188 # now that we've committed the changes, create related A/T events
1189 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1190 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1193 $logger->debug("resetting holds that target the marked copy");
1194 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1199 sub handle_mark_damaged {
1200 my($e, $copy, $owning_lib, $args) = @_;
1202 my $apply = $args->{apply_fines} || '';
1203 return undef if $apply eq 'noapply';
1205 my $new_amount = $args->{override_amount};
1206 my $new_btype = $args->{override_btype};
1207 my $new_note = $args->{override_note};
1209 # grab the last circulation
1210 my $circ = $e->search_action_circulation([
1211 { target_copy => $copy->id},
1213 order_by => {circ => "xact_start DESC"},
1215 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1219 return undef unless $circ;
1221 my $charge_price = $U->ou_ancestor_setting_value(
1222 $owning_lib, 'circ.charge_on_damaged', $e);
1224 my $proc_fee = $U->ou_ancestor_setting_value(
1225 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1227 my $void_overdue = $U->ou_ancestor_setting_value(
1228 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1230 return undef unless $charge_price or $proc_fee;
1232 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1233 my $total = $copy_price + $proc_fee;
1237 if($new_amount and $new_btype) {
1239 # Allow staff to override the amount to charge for a damaged item
1240 # Consider the case where the item is only partially damaged
1241 # This value is meant to take the place of the item price and
1242 # optional processing fee.
1244 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1245 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1246 return $evt if $evt;
1250 if($charge_price and $copy_price) {
1251 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1252 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1253 return $evt if $evt;
1257 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1258 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1259 return $evt if $evt;
1263 # the assumption is that you would not void the overdues unless you
1264 # were also charging for the item and/or applying a processing fee
1266 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1267 return $evt if $evt;
1270 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1271 return $evt if $evt;
1273 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1274 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1276 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1277 return $evt2 if $evt2;
1282 return OpenILS::Event->new('DAMAGE_CHARGE',
1293 # ----------------------------------------------------------------------
1294 __PACKAGE__->register_method(
1295 method => 'mark_item_missing_pieces',
1296 api_name => 'open-ils.circ.mark_item_missing_pieces',
1298 Changes the status of a copy to "damaged" or to a custom status based on the
1299 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1301 @param authtoken The login session key
1302 @param copy_id The ID of the copy to mark as damaged
1303 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1307 sub mark_item_missing_pieces {
1308 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1309 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1310 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1312 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1313 return $e2->die_event unless $e2->checkauth;
1316 my $copy = $e2->retrieve_asset_copy([
1318 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1319 or return $e2->die_event;
1322 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1323 $copy->circ_lib : $copy->call_number->owning_lib;
1325 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1327 #### grab the last circulation
1328 my $circ = $e2->search_action_circulation([
1329 { target_copy => $copy->id},
1331 order_by => {circ => "xact_start DESC"}
1336 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1338 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1341 my $holds = $e2->search_action_hold_request(
1343 current_copy => $copy->id,
1344 fulfillment_time => undef,
1345 cancel_time => undef,
1349 $logger->debug("resetting holds that target the marked copy");
1350 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1353 if (! $e2->commit) {
1354 return $e2->die_event;
1357 my $e = new_editor(authtoken=>$auth, xact =>1);
1358 return $e->die_event unless $e->checkauth;
1360 if (! $circ->checkin_time) { # if circ active, attempt renew
1361 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1362 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1363 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1364 $circ = $res->[0]->{payload}{'circ'};
1365 $circ->target_copy( $copy->id );
1366 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1368 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1373 'copy_id'=>$circ->target_copy,
1374 'patron_id'=>$circ->usr,
1375 'skip_deposit_fee'=>1,
1376 'skip_rental_fee'=>1
1379 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1381 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1382 $e, $copy, $e->requestor, 1 );
1384 if ($hold) { # needed for hold? then due now
1386 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1387 my $due_date = DateTime->now(time_zone => 'local');
1388 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1390 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1394 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1395 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1396 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1397 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1398 $circ = $res->[0]->{payload}{'circ'};
1400 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1406 ### Update the item status
1408 my $custom_stat = $U->ou_ancestor_setting_value(
1409 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1410 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1412 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1413 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1415 $copy->status($stat);
1416 $copy->edit_date('now');
1417 $copy->editor($e->requestor->id);
1419 $e->update_asset_copy($copy) or return $e->die_event;
1423 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1424 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1426 return OpenILS::Event->new('SUCCESS',
1430 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1431 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1436 return $e->die_event;
1444 # ----------------------------------------------------------------------
1445 __PACKAGE__->register_method(
1446 method => 'magic_fetch',
1447 api_name => 'open-ils.agent.fetch'
1450 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1453 my( $self, $conn, $auth, $args ) = @_;
1454 my $e = new_editor( authtoken => $auth );
1455 return $e->event unless $e->checkauth;
1457 my $hint = $$args{hint};
1458 my $id = $$args{id};
1460 # Is the call allowed to fetch this type of object?
1461 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1463 # Find the class the implements the given hint
1464 my ($class) = grep {
1465 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1467 $class =~ s/Fieldmapper:://og;
1468 $class =~ s/::/_/og;
1469 my $method = "retrieve_$class";
1471 my $obj = $e->$method($id) or return $e->event;
1474 # ----------------------------------------------------------------------
1477 __PACKAGE__->register_method(
1478 method => "fleshed_circ_retrieve",
1480 api_name => "open-ils.circ.fleshed.retrieve",);
1482 sub fleshed_circ_retrieve {
1483 my( $self, $client, $id ) = @_;
1484 my $e = new_editor();
1485 my $circ = $e->retrieve_action_circulation(
1491 circ => [ qw/ target_copy / ],
1492 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1493 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1494 acn => [ qw/ record / ],
1498 ) or return $e->event;
1500 my $copy = $circ->target_copy;
1501 my $vol = $copy->call_number;
1502 my $rec = $circ->target_copy->call_number->record;
1504 $vol->record($rec->id);
1505 $copy->call_number($vol->id);
1506 $circ->target_copy($copy->id);
1510 if( $rec->id == OILS_PRECAT_RECORD ) {
1514 $mvr = $U->record_to_mvr($rec);
1515 $rec->marc(''); # drop the bulky marc data
1529 __PACKAGE__->register_method(
1530 method => "test_batch_circ_events",
1531 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1534 # method for testing the behavior of a given event definition
1535 sub test_batch_circ_events {
1536 my($self, $conn, $auth, $event_def, $barcode) = @_;
1538 my $e = new_editor(authtoken => $auth);
1539 return $e->event unless $e->checkauth;
1540 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1542 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1543 or return $e->event;
1545 my $circ = $e->search_action_circulation(
1546 {target_copy => $copy->id, checkin_time => undef})->[0]
1547 or return $e->event;
1549 return undef unless $circ;
1551 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1555 __PACKAGE__->register_method(
1556 method => "fire_circ_events",
1557 api_name => "open-ils.circ.fire_circ_trigger_events",
1559 General event def runner for circ objects. If no event def ID
1560 is provided, the hook will be used to find the best event_def
1561 match based on the context org unit
1565 __PACKAGE__->register_method(
1566 method => "fire_circ_events",
1567 api_name => "open-ils.circ.fire_hold_trigger_events",
1569 General event def runner for hold objects. If no event def ID
1570 is provided, the hook will be used to find the best event_def
1571 match based on the context org unit
1575 __PACKAGE__->register_method(
1576 method => "fire_circ_events",
1577 api_name => "open-ils.circ.fire_user_trigger_events",
1579 General event def runner for user objects. If no event def ID
1580 is provided, the hook will be used to find the best event_def
1581 match based on the context org unit
1586 sub fire_circ_events {
1587 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1589 my $e = new_editor(authtoken => $auth, xact => 1);
1590 return $e->event unless $e->checkauth;
1594 if($self->api_name =~ /hold/) {
1595 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1596 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1597 } elsif($self->api_name =~ /user/) {
1598 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1599 $targets = $e->batch_retrieve_actor_user($target_ids);
1601 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1602 $targets = $e->batch_retrieve_action_circulation($target_ids);
1604 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1605 # simply making this method authoritative because of weirdness
1606 # with transaction handling in A/T code that causes rollback
1607 # failure down the line if handling many targets
1609 return undef unless @$targets;
1610 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1613 __PACKAGE__->register_method(
1614 method => "user_payments_list",
1615 api_name => "open-ils.circ.user_payments.filtered.batch",
1618 desc => q/Returns a fleshed, date-limited set of all payments a user
1619 has made. By default, ordered by payment date. Optionally
1620 ordered by other columns in the top-level "mp" object/,
1622 {desc => 'Authentication token', type => 'string'},
1623 {desc => 'User ID', type => 'number'},
1624 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1626 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1627 and the related fully-realized payment object (e.g money.cash_payment)/}
1631 sub user_payments_list {
1632 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1634 my $e = new_editor(authtoken => $auth);
1635 return $e->event unless $e->checkauth;
1637 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1638 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1640 $order_by ||= ['payment_ts'];
1642 # all payments by user, between start_date and end_date
1643 my $payments = $e->json_query({
1644 select => {mp => ['id']},
1648 fkey => 'xact', field => 'id'}
1652 '+mbt' => {usr => $user_id},
1653 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1655 order_by => {mp => $order_by}
1658 for my $payment_id (@$payments) {
1659 my $payment = $e->retrieve_money_payment([
1667 'credit_card_payment',
1682 $conn->respond($payment);
1689 __PACKAGE__->register_method(
1690 method => "retrieve_circ_chain",
1691 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1694 desc => q/Given a circulation, this returns all circulation objects
1695 that are part of the same chain of renewals./,
1697 {desc => 'Authentication token', type => 'string'},
1698 {desc => 'Circ ID', type => 'number'},
1700 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1704 __PACKAGE__->register_method(
1705 method => "retrieve_circ_chain",
1706 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1708 desc => q/Given a circulation, this returns a summary of the circulation objects
1709 that are part of the same chain of renewals./,
1711 {desc => 'Authentication token', type => 'string'},
1712 {desc => 'Circ ID', type => 'number'},
1714 return => {desc => q/Circulation Chain Summary/}
1718 sub retrieve_circ_chain {
1719 my($self, $conn, $auth, $circ_id) = @_;
1721 my $e = new_editor(authtoken => $auth);
1722 return $e->event unless $e->checkauth;
1723 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1725 if($self->api_name =~ /summary/) {
1726 return $U->create_circ_chain_summary($e, $circ_id);
1730 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1732 for my $circ_info (@$chain) {
1733 my $circ = Fieldmapper::action::circulation->new;
1734 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1735 $conn->respond($circ);
1742 __PACKAGE__->register_method(
1743 method => "retrieve_prev_circ_chain",
1744 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1747 desc => q/Given a circulation, this returns all circulation objects
1748 that are part of the previous chain of renewals./,
1750 {desc => 'Authentication token', type => 'string'},
1751 {desc => 'Circ ID', type => 'number'},
1753 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1757 __PACKAGE__->register_method(
1758 method => "retrieve_prev_circ_chain",
1759 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1761 desc => q/Given a circulation, this returns a summary of the circulation objects
1762 that are part of the previous chain of renewals./,
1764 {desc => 'Authentication token', type => 'string'},
1765 {desc => 'Circ ID', type => 'number'},
1767 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1771 sub retrieve_prev_circ_chain {
1772 my($self, $conn, $auth, $circ_id) = @_;
1774 my $e = new_editor(authtoken => $auth);
1775 return $e->event unless $e->checkauth;
1776 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1778 if($self->api_name =~ /summary/) {
1779 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1780 my $target_copy = $$first_circ{'target_copy'};
1781 my $usr = $$first_circ{'usr'};
1782 my $last_circ_from_prev_chain = $e->json_query({
1783 'select' => { 'circ' => ['id','usr'] },
1786 target_copy => $target_copy,
1787 xact_start => { '<' => $$first_circ{'xact_start'} }
1789 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1792 return undef unless $last_circ_from_prev_chain;
1793 return undef unless $$last_circ_from_prev_chain{'id'};
1794 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1795 return undef unless $sum;
1796 my $obj = Fieldmapper::action::circ_chain_summary->new;
1797 $obj->$_($sum->{$_}) for keys %$sum;
1798 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1802 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1803 my $target_copy = $$first_circ{'target_copy'};
1804 my $last_circ_from_prev_chain = $e->json_query({
1805 'select' => { 'circ' => ['id'] },
1808 target_copy => $target_copy,
1809 xact_start => { '<' => $$first_circ{'xact_start'} }
1811 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1814 return undef unless $last_circ_from_prev_chain;
1815 return undef unless $$last_circ_from_prev_chain{'id'};
1816 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1818 for my $circ_info (@$chain) {
1819 my $circ = Fieldmapper::action::circulation->new;
1820 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1821 $conn->respond($circ);
1829 __PACKAGE__->register_method(
1830 method => "get_copy_due_date",
1831 api_name => "open-ils.circ.copy.due_date.retrieve",
1834 Given a copy ID, returns the due date for the copy if it's
1835 currently circulating. Otherwise, returns null. Note, this is a public
1836 method requiring no authentication. Only the due date is exposed.
1839 {desc => 'Copy ID', type => 'number'}
1841 return => {desc => q/
1842 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1847 sub get_copy_due_date {
1848 my($self, $conn, $copy_id) = @_;
1849 my $e = new_editor();
1851 my $circ = $e->json_query({
1852 select => {circ => ['due_date']},
1855 target_copy => $copy_id,
1856 checkin_time => undef,
1858 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1859 {stop_fines => undef}
1863 })->[0] or return undef;
1865 return $circ->{due_date};
1872 # {"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}}