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 ) = @_;
747 my $count = new_editor()->json_query({
756 where => {'+circbyyr' => {copy => $copyid}}
768 __PACKAGE__->register_method(
769 method => 'fetch_notes',
771 api_name => 'open-ils.circ.copy_note.retrieve.all',
773 Returns an array of copy note objects.
774 @param args A named hash of parameters including:
775 authtoken : Required if viewing non-public notes
776 itemid : The id of the item whose notes we want to retrieve
777 pub : True if all the caller wants are public notes
778 @return An array of note objects
781 __PACKAGE__->register_method(
782 method => 'fetch_notes',
783 api_name => 'open-ils.circ.call_number_note.retrieve.all',
784 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
786 __PACKAGE__->register_method(
787 method => 'fetch_notes',
788 api_name => 'open-ils.circ.title_note.retrieve.all',
789 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
792 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
794 my( $self, $connection, $args ) = @_;
796 my $id = $$args{itemid};
797 my $authtoken = $$args{authtoken};
800 if( $self->api_name =~ /copy/ ) {
802 return $U->cstorereq(
803 'open-ils.cstore.direct.asset.copy_note.search.atomic',
804 { owning_copy => $id, pub => 't' } );
806 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
808 return $U->cstorereq(
809 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
812 } elsif( $self->api_name =~ /call_number/ ) {
814 return $U->cstorereq(
815 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
816 { call_number => $id, pub => 't' } );
818 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
820 return $U->cstorereq(
821 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
824 } elsif( $self->api_name =~ /title/ ) {
826 return $U->cstorereq(
827 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
828 { record => $id, pub => 't' } );
830 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
832 return $U->cstorereq(
833 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
840 __PACKAGE__->register_method(
841 method => 'has_notes',
842 api_name => 'open-ils.circ.copy.has_notes');
843 __PACKAGE__->register_method(
844 method => 'has_notes',
845 api_name => 'open-ils.circ.call_number.has_notes');
846 __PACKAGE__->register_method(
847 method => 'has_notes',
848 api_name => 'open-ils.circ.title.has_notes');
852 my( $self, $conn, $authtoken, $id ) = @_;
853 my $editor = new_editor(authtoken => $authtoken);
854 return $editor->event unless $editor->checkauth;
856 my $n = $editor->search_asset_copy_note(
857 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
859 $n = $editor->search_asset_call_number_note(
860 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
862 $n = $editor->search_biblio_record_note(
863 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
870 __PACKAGE__->register_method(
871 method => 'create_copy_note',
872 api_name => 'open-ils.circ.copy_note.create',
874 Creates a new copy note
875 @param authtoken The login session key
876 @param note The note object to create
877 @return The id of the new note object
880 sub create_copy_note {
881 my( $self, $connection, $authtoken, $note ) = @_;
883 my $e = new_editor(xact=>1, authtoken=>$authtoken);
884 return $e->event unless $e->checkauth;
885 my $copy = $e->retrieve_asset_copy(
889 flesh_fields => { 'acp' => ['call_number'] }
894 return $e->event unless
895 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
897 $note->create_date('now');
898 $note->creator($e->requestor->id);
899 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
902 $e->create_asset_copy_note($note) or return $e->event;
908 __PACKAGE__->register_method(
909 method => 'delete_copy_note',
910 api_name => 'open-ils.circ.copy_note.delete',
912 Deletes an existing copy note
913 @param authtoken The login session key
914 @param noteid The id of the note to delete
915 @return 1 on success - Event otherwise.
917 sub delete_copy_note {
918 my( $self, $conn, $authtoken, $noteid ) = @_;
920 my $e = new_editor(xact=>1, authtoken=>$authtoken);
921 return $e->die_event unless $e->checkauth;
923 my $note = $e->retrieve_asset_copy_note([
927 'acpn' => [ 'owning_copy' ],
928 'acp' => [ 'call_number' ],
931 ]) or return $e->die_event;
933 if( $note->creator ne $e->requestor->id ) {
934 return $e->die_event unless
935 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
938 $e->delete_asset_copy_note($note) or return $e->die_event;
944 __PACKAGE__->register_method(
945 method => 'age_hold_rules',
946 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
950 my( $self, $conn ) = @_;
951 return new_editor()->retrieve_all_config_rules_age_hold_protect();
956 __PACKAGE__->register_method(
957 method => 'copy_details_barcode',
959 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
960 sub copy_details_barcode {
961 my( $self, $conn, $auth, $barcode ) = @_;
962 my $e = new_editor();
963 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
964 return $e->event unless $cid;
965 return copy_details( $self, $conn, $auth, $cid );
969 __PACKAGE__->register_method(
970 method => 'copy_details',
971 api_name => 'open-ils.circ.copy_details.retrieve');
974 my( $self, $conn, $auth, $copy_id ) = @_;
975 my $e = new_editor(authtoken=>$auth);
976 return $e->event unless $e->checkauth;
978 my $flesh = { flesh => 1 };
980 my $copy = $e->retrieve_asset_copy(
986 acp => ['call_number','parts','peer_record_maps','floating'],
987 acn => ['record','prefix','suffix','label_class']
990 ]) or return $e->event;
993 # De-flesh the copy for backwards compatibility
995 my $vol = $copy->call_number;
997 $copy->call_number($vol->id);
998 my $record = $vol->record;
1000 $vol->record($record->id);
1001 $mvr = $U->record_to_mvr($record);
1006 my $hold = $e->search_action_hold_request(
1008 current_copy => $copy_id,
1009 capture_time => { "!=" => undef },
1010 fulfillment_time => undef,
1011 cancel_time => undef,
1015 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1017 my $transit = $e->search_action_transit_copy(
1018 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1020 # find the latest circ, open or closed
1021 my $circ = $e->search_action_circulation(
1023 { target_copy => $copy_id },
1029 'checkin_workstation',
1032 'recurring_fine_rule'
1035 order_by => { circ => 'xact_start desc' },
1045 transit => $transit,
1055 __PACKAGE__->register_method(
1056 method => 'mark_item',
1057 api_name => 'open-ils.circ.mark_item_damaged',
1059 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1060 @param authtoken The login session key
1061 @param copy_id The ID of the copy to mark as damaged
1062 @return 1 on success - Event otherwise.
1065 __PACKAGE__->register_method(
1066 method => 'mark_item',
1067 api_name => 'open-ils.circ.mark_item_missing',
1069 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1070 @param authtoken The login session key
1071 @param copy_id The ID of the copy to mark as missing
1072 @return 1 on success - Event otherwise.
1075 __PACKAGE__->register_method(
1076 method => 'mark_item',
1077 api_name => 'open-ils.circ.mark_item_bindery',
1079 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1080 @param authtoken The login session key
1081 @param copy_id The ID of the copy to mark as bindery
1082 @return 1 on success - Event otherwise.
1085 __PACKAGE__->register_method(
1086 method => 'mark_item',
1087 api_name => 'open-ils.circ.mark_item_on_order',
1089 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1090 @param authtoken The login session key
1091 @param copy_id The ID of the copy to mark as on order
1092 @return 1 on success - Event otherwise.
1095 __PACKAGE__->register_method(
1096 method => 'mark_item',
1097 api_name => 'open-ils.circ.mark_item_ill',
1099 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1100 @param authtoken The login session key
1101 @param copy_id The ID of the copy to mark as inter-library loan
1102 @return 1 on success - Event otherwise.
1105 __PACKAGE__->register_method(
1106 method => 'mark_item',
1107 api_name => 'open-ils.circ.mark_item_cataloging',
1109 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1110 @param authtoken The login session key
1111 @param copy_id The ID of the copy to mark as cataloging
1112 @return 1 on success - Event otherwise.
1115 __PACKAGE__->register_method(
1116 method => 'mark_item',
1117 api_name => 'open-ils.circ.mark_item_reserves',
1119 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1120 @param authtoken The login session key
1121 @param copy_id The ID of the copy to mark as reserves
1122 @return 1 on success - Event otherwise.
1125 __PACKAGE__->register_method(
1126 method => 'mark_item',
1127 api_name => 'open-ils.circ.mark_item_discard',
1129 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1130 @param authtoken The login session key
1131 @param copy_id The ID of the copy to mark as discard
1132 @return 1 on success - Event otherwise.
1137 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1138 my $e = new_editor(authtoken=>$auth, xact =>1);
1139 return $e->die_event unless $e->checkauth;
1142 my $copy = $e->retrieve_asset_copy([
1144 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1145 or return $e->die_event;
1148 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1149 $copy->circ_lib : $copy->call_number->owning_lib;
1151 my $perm = 'MARK_ITEM_MISSING';
1152 my $stat = OILS_COPY_STATUS_MISSING;
1154 if( $self->api_name =~ /damaged/ ) {
1155 $perm = 'MARK_ITEM_DAMAGED';
1156 $stat = OILS_COPY_STATUS_DAMAGED;
1157 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1158 return $evt if $evt;
1160 } elsif ( $self->api_name =~ /bindery/ ) {
1161 $perm = 'MARK_ITEM_BINDERY';
1162 $stat = OILS_COPY_STATUS_BINDERY;
1163 } elsif ( $self->api_name =~ /on_order/ ) {
1164 $perm = 'MARK_ITEM_ON_ORDER';
1165 $stat = OILS_COPY_STATUS_ON_ORDER;
1166 } elsif ( $self->api_name =~ /ill/ ) {
1167 $perm = 'MARK_ITEM_ILL';
1168 $stat = OILS_COPY_STATUS_ILL;
1169 } elsif ( $self->api_name =~ /cataloging/ ) {
1170 $perm = 'MARK_ITEM_CATALOGING';
1171 $stat = OILS_COPY_STATUS_CATALOGING;
1172 } elsif ( $self->api_name =~ /reserves/ ) {
1173 $perm = 'MARK_ITEM_RESERVES';
1174 $stat = OILS_COPY_STATUS_RESERVES;
1175 } elsif ( $self->api_name =~ /discard/ ) {
1176 $perm = 'MARK_ITEM_DISCARD';
1177 $stat = OILS_COPY_STATUS_DISCARD;
1180 # caller may proceed if either perm is allowed
1181 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1183 $copy->status($stat);
1184 $copy->edit_date('now');
1185 $copy->editor($e->requestor->id);
1187 $e->update_asset_copy($copy) or return $e->die_event;
1189 my $holds = $e->search_action_hold_request(
1191 current_copy => $copy->id,
1192 fulfillment_time => undef,
1193 cancel_time => undef,
1199 if( $self->api_name =~ /damaged/ ) {
1200 # now that we've committed the changes, create related A/T events
1201 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1202 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1205 $logger->debug("resetting holds that target the marked copy");
1206 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1211 sub handle_mark_damaged {
1212 my($e, $copy, $owning_lib, $args) = @_;
1214 my $apply = $args->{apply_fines} || '';
1215 return undef if $apply eq 'noapply';
1217 my $new_amount = $args->{override_amount};
1218 my $new_btype = $args->{override_btype};
1219 my $new_note = $args->{override_note};
1221 # grab the last circulation
1222 my $circ = $e->search_action_circulation([
1223 { target_copy => $copy->id},
1225 order_by => {circ => "xact_start DESC"},
1227 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1231 return undef unless $circ;
1233 my $charge_price = $U->ou_ancestor_setting_value(
1234 $owning_lib, 'circ.charge_on_damaged', $e);
1236 my $proc_fee = $U->ou_ancestor_setting_value(
1237 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1239 my $void_overdue = $U->ou_ancestor_setting_value(
1240 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1242 return undef unless $charge_price or $proc_fee;
1244 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1245 my $total = $copy_price + $proc_fee;
1249 if($new_amount and $new_btype) {
1251 # Allow staff to override the amount to charge for a damaged item
1252 # Consider the case where the item is only partially damaged
1253 # This value is meant to take the place of the item price and
1254 # optional processing fee.
1256 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1257 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1258 return $evt if $evt;
1262 if($charge_price and $copy_price) {
1263 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1264 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1265 return $evt if $evt;
1269 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1270 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1271 return $evt if $evt;
1275 # the assumption is that you would not void the overdues unless you
1276 # were also charging for the item and/or applying a processing fee
1278 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1279 return $evt if $evt;
1282 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1283 return $evt if $evt;
1285 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1286 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1288 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1289 return $evt2 if $evt2;
1294 return OpenILS::Event->new('DAMAGE_CHARGE',
1305 # ----------------------------------------------------------------------
1306 __PACKAGE__->register_method(
1307 method => 'mark_item_missing_pieces',
1308 api_name => 'open-ils.circ.mark_item_missing_pieces',
1310 Changes the status of a copy to "damaged" or to a custom status based on the
1311 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1313 @param authtoken The login session key
1314 @param copy_id The ID of the copy to mark as damaged
1315 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1319 sub mark_item_missing_pieces {
1320 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1321 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1322 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1324 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1325 return $e2->die_event unless $e2->checkauth;
1328 my $copy = $e2->retrieve_asset_copy([
1330 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1331 or return $e2->die_event;
1334 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1335 $copy->circ_lib : $copy->call_number->owning_lib;
1337 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1339 #### grab the last circulation
1340 my $circ = $e2->search_action_circulation([
1341 { target_copy => $copy->id},
1343 order_by => {circ => "xact_start DESC"}
1348 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1350 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1353 my $holds = $e2->search_action_hold_request(
1355 current_copy => $copy->id,
1356 fulfillment_time => undef,
1357 cancel_time => undef,
1361 $logger->debug("resetting holds that target the marked copy");
1362 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1365 if (! $e2->commit) {
1366 return $e2->die_event;
1369 my $e = new_editor(authtoken=>$auth, xact =>1);
1370 return $e->die_event unless $e->checkauth;
1372 if (! $circ->checkin_time) { # if circ active, attempt renew
1373 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1374 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1375 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1376 $circ = $res->[0]->{payload}{'circ'};
1377 $circ->target_copy( $copy->id );
1378 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1380 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1385 'copy_id'=>$circ->target_copy,
1386 'patron_id'=>$circ->usr,
1387 'skip_deposit_fee'=>1,
1388 'skip_rental_fee'=>1
1391 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1393 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1394 $e, $copy, $e->requestor, 1 );
1396 if ($hold) { # needed for hold? then due now
1398 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1399 my $due_date = DateTime->now(time_zone => 'local');
1400 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1402 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1406 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1407 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1408 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1409 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1410 $circ = $res->[0]->{payload}{'circ'};
1412 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1418 ### Update the item status
1420 my $custom_stat = $U->ou_ancestor_setting_value(
1421 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1422 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1424 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1425 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1427 $copy->status($stat);
1428 $copy->edit_date('now');
1429 $copy->editor($e->requestor->id);
1431 $e->update_asset_copy($copy) or return $e->die_event;
1435 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1436 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1438 return OpenILS::Event->new('SUCCESS',
1442 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1443 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1448 return $e->die_event;
1456 # ----------------------------------------------------------------------
1457 __PACKAGE__->register_method(
1458 method => 'magic_fetch',
1459 api_name => 'open-ils.agent.fetch'
1462 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1465 my( $self, $conn, $auth, $args ) = @_;
1466 my $e = new_editor( authtoken => $auth );
1467 return $e->event unless $e->checkauth;
1469 my $hint = $$args{hint};
1470 my $id = $$args{id};
1472 # Is the call allowed to fetch this type of object?
1473 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1475 # Find the class the implements the given hint
1476 my ($class) = grep {
1477 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1479 $class =~ s/Fieldmapper:://og;
1480 $class =~ s/::/_/og;
1481 my $method = "retrieve_$class";
1483 my $obj = $e->$method($id) or return $e->event;
1486 # ----------------------------------------------------------------------
1489 __PACKAGE__->register_method(
1490 method => "fleshed_circ_retrieve",
1492 api_name => "open-ils.circ.fleshed.retrieve",);
1494 sub fleshed_circ_retrieve {
1495 my( $self, $client, $id ) = @_;
1496 my $e = new_editor();
1497 my $circ = $e->retrieve_action_circulation(
1503 circ => [ qw/ target_copy / ],
1504 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1505 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1506 acn => [ qw/ record / ],
1510 ) or return $e->event;
1512 my $copy = $circ->target_copy;
1513 my $vol = $copy->call_number;
1514 my $rec = $circ->target_copy->call_number->record;
1516 $vol->record($rec->id);
1517 $copy->call_number($vol->id);
1518 $circ->target_copy($copy->id);
1522 if( $rec->id == OILS_PRECAT_RECORD ) {
1526 $mvr = $U->record_to_mvr($rec);
1527 $rec->marc(''); # drop the bulky marc data
1541 __PACKAGE__->register_method(
1542 method => "test_batch_circ_events",
1543 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1546 # method for testing the behavior of a given event definition
1547 sub test_batch_circ_events {
1548 my($self, $conn, $auth, $event_def, $barcode) = @_;
1550 my $e = new_editor(authtoken => $auth);
1551 return $e->event unless $e->checkauth;
1552 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1554 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1555 or return $e->event;
1557 my $circ = $e->search_action_circulation(
1558 {target_copy => $copy->id, checkin_time => undef})->[0]
1559 or return $e->event;
1561 return undef unless $circ;
1563 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1567 __PACKAGE__->register_method(
1568 method => "fire_circ_events",
1569 api_name => "open-ils.circ.fire_circ_trigger_events",
1571 General event def runner for circ objects. If no event def ID
1572 is provided, the hook will be used to find the best event_def
1573 match based on the context org unit
1577 __PACKAGE__->register_method(
1578 method => "fire_circ_events",
1579 api_name => "open-ils.circ.fire_hold_trigger_events",
1581 General event def runner for hold objects. If no event def ID
1582 is provided, the hook will be used to find the best event_def
1583 match based on the context org unit
1587 __PACKAGE__->register_method(
1588 method => "fire_circ_events",
1589 api_name => "open-ils.circ.fire_user_trigger_events",
1591 General event def runner for user objects. If no event def ID
1592 is provided, the hook will be used to find the best event_def
1593 match based on the context org unit
1598 sub fire_circ_events {
1599 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1601 my $e = new_editor(authtoken => $auth, xact => 1);
1602 return $e->event unless $e->checkauth;
1606 if($self->api_name =~ /hold/) {
1607 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1608 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1609 } elsif($self->api_name =~ /user/) {
1610 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1611 $targets = $e->batch_retrieve_actor_user($target_ids);
1613 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1614 $targets = $e->batch_retrieve_action_circulation($target_ids);
1616 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1617 # simply making this method authoritative because of weirdness
1618 # with transaction handling in A/T code that causes rollback
1619 # failure down the line if handling many targets
1621 return undef unless @$targets;
1622 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1625 __PACKAGE__->register_method(
1626 method => "user_payments_list",
1627 api_name => "open-ils.circ.user_payments.filtered.batch",
1630 desc => q/Returns a fleshed, date-limited set of all payments a user
1631 has made. By default, ordered by payment date. Optionally
1632 ordered by other columns in the top-level "mp" object/,
1634 {desc => 'Authentication token', type => 'string'},
1635 {desc => 'User ID', type => 'number'},
1636 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1638 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1639 and the related fully-realized payment object (e.g money.cash_payment)/}
1643 sub user_payments_list {
1644 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1646 my $e = new_editor(authtoken => $auth);
1647 return $e->event unless $e->checkauth;
1649 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1650 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1652 $order_by ||= ['payment_ts'];
1654 # all payments by user, between start_date and end_date
1655 my $payments = $e->json_query({
1656 select => {mp => ['id']},
1660 fkey => 'xact', field => 'id'}
1664 '+mbt' => {usr => $user_id},
1665 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1667 order_by => {mp => $order_by}
1670 for my $payment_id (@$payments) {
1671 my $payment = $e->retrieve_money_payment([
1679 'credit_card_payment',
1694 $conn->respond($payment);
1701 __PACKAGE__->register_method(
1702 method => "retrieve_circ_chain",
1703 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1706 desc => q/Given a circulation, this returns all circulation objects
1707 that are part of the same chain of renewals./,
1709 {desc => 'Authentication token', type => 'string'},
1710 {desc => 'Circ ID', type => 'number'},
1712 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1716 __PACKAGE__->register_method(
1717 method => "retrieve_circ_chain",
1718 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1720 desc => q/Given a circulation, this returns a summary of the circulation objects
1721 that are part of the same chain of renewals./,
1723 {desc => 'Authentication token', type => 'string'},
1724 {desc => 'Circ ID', type => 'number'},
1726 return => {desc => q/Circulation Chain Summary/}
1730 sub retrieve_circ_chain {
1731 my($self, $conn, $auth, $circ_id) = @_;
1733 my $e = new_editor(authtoken => $auth);
1734 return $e->event unless $e->checkauth;
1735 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1737 if($self->api_name =~ /summary/) {
1738 return $U->create_circ_chain_summary($e, $circ_id);
1742 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1744 for my $circ_info (@$chain) {
1745 my $circ = Fieldmapper::action::circulation->new;
1746 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1747 $conn->respond($circ);
1754 __PACKAGE__->register_method(
1755 method => "retrieve_prev_circ_chain",
1756 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1759 desc => q/Given a circulation, this returns all circulation objects
1760 that are part of the previous chain of renewals./,
1762 {desc => 'Authentication token', type => 'string'},
1763 {desc => 'Circ ID', type => 'number'},
1765 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1769 __PACKAGE__->register_method(
1770 method => "retrieve_prev_circ_chain",
1771 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1773 desc => q/Given a circulation, this returns a summary of the circulation objects
1774 that are part of the previous chain of renewals./,
1776 {desc => 'Authentication token', type => 'string'},
1777 {desc => 'Circ ID', type => 'number'},
1779 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1783 sub retrieve_prev_circ_chain {
1784 my($self, $conn, $auth, $circ_id) = @_;
1786 my $e = new_editor(authtoken => $auth);
1787 return $e->event unless $e->checkauth;
1788 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1790 if($self->api_name =~ /summary/) {
1791 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1792 my $target_copy = $$first_circ{'target_copy'};
1793 my $usr = $$first_circ{'usr'};
1794 my $last_circ_from_prev_chain = $e->json_query({
1795 'select' => { 'circ' => ['id','usr'] },
1798 target_copy => $target_copy,
1799 xact_start => { '<' => $$first_circ{'xact_start'} }
1801 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1804 return undef unless $last_circ_from_prev_chain;
1805 return undef unless $$last_circ_from_prev_chain{'id'};
1806 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1807 return undef unless $sum;
1808 my $obj = Fieldmapper::action::circ_chain_summary->new;
1809 $obj->$_($sum->{$_}) for keys %$sum;
1810 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1814 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1815 my $target_copy = $$first_circ{'target_copy'};
1816 my $last_circ_from_prev_chain = $e->json_query({
1817 'select' => { 'circ' => ['id'] },
1820 target_copy => $target_copy,
1821 xact_start => { '<' => $$first_circ{'xact_start'} }
1823 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1826 return undef unless $last_circ_from_prev_chain;
1827 return undef unless $$last_circ_from_prev_chain{'id'};
1828 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1830 for my $circ_info (@$chain) {
1831 my $circ = Fieldmapper::action::circulation->new;
1832 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1833 $conn->respond($circ);
1841 __PACKAGE__->register_method(
1842 method => "get_copy_due_date",
1843 api_name => "open-ils.circ.copy.due_date.retrieve",
1846 Given a copy ID, returns the due date for the copy if it's
1847 currently circulating. Otherwise, returns null. Note, this is a public
1848 method requiring no authentication. Only the due date is exposed.
1851 {desc => 'Copy ID', type => 'number'}
1853 return => {desc => q/
1854 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1859 sub get_copy_due_date {
1860 my($self, $conn, $copy_id) = @_;
1861 my $e = new_editor();
1863 my $circ = $e->json_query({
1864 select => {circ => ['due_date']},
1867 target_copy => $copy_id,
1868 checkin_time => undef,
1870 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1871 {stop_fines => undef}
1875 })->[0] or return undef;
1877 return $circ->{due_date};
1884 # {"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}}