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 ) = @_;
746 my $e = new_editor();
747 return $e->request('open-ils.storage.asset.copy.circ_count', $copyid, $range);
752 __PACKAGE__->register_method(
753 method => 'fetch_notes',
755 api_name => 'open-ils.circ.copy_note.retrieve.all',
757 Returns an array of copy note objects.
758 @param args A named hash of parameters including:
759 authtoken : Required if viewing non-public notes
760 itemid : The id of the item whose notes we want to retrieve
761 pub : True if all the caller wants are public notes
762 @return An array of note objects
765 __PACKAGE__->register_method(
766 method => 'fetch_notes',
767 api_name => 'open-ils.circ.call_number_note.retrieve.all',
768 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
770 __PACKAGE__->register_method(
771 method => 'fetch_notes',
772 api_name => 'open-ils.circ.title_note.retrieve.all',
773 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
776 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
778 my( $self, $connection, $args ) = @_;
780 my $id = $$args{itemid};
781 my $authtoken = $$args{authtoken};
784 if( $self->api_name =~ /copy/ ) {
786 return $U->cstorereq(
787 'open-ils.cstore.direct.asset.copy_note.search.atomic',
788 { owning_copy => $id, pub => 't' } );
790 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
792 return $U->cstorereq(
793 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
796 } elsif( $self->api_name =~ /call_number/ ) {
798 return $U->cstorereq(
799 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
800 { call_number => $id, pub => 't' } );
802 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
804 return $U->cstorereq(
805 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
808 } elsif( $self->api_name =~ /title/ ) {
810 return $U->cstorereq(
811 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
812 { record => $id, pub => 't' } );
814 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
816 return $U->cstorereq(
817 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
824 __PACKAGE__->register_method(
825 method => 'has_notes',
826 api_name => 'open-ils.circ.copy.has_notes');
827 __PACKAGE__->register_method(
828 method => 'has_notes',
829 api_name => 'open-ils.circ.call_number.has_notes');
830 __PACKAGE__->register_method(
831 method => 'has_notes',
832 api_name => 'open-ils.circ.title.has_notes');
836 my( $self, $conn, $authtoken, $id ) = @_;
837 my $editor = new_editor(authtoken => $authtoken);
838 return $editor->event unless $editor->checkauth;
840 my $n = $editor->search_asset_copy_note(
841 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
843 $n = $editor->search_asset_call_number_note(
844 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
846 $n = $editor->search_biblio_record_note(
847 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
854 __PACKAGE__->register_method(
855 method => 'create_copy_note',
856 api_name => 'open-ils.circ.copy_note.create',
858 Creates a new copy note
859 @param authtoken The login session key
860 @param note The note object to create
861 @return The id of the new note object
864 sub create_copy_note {
865 my( $self, $connection, $authtoken, $note ) = @_;
867 my $e = new_editor(xact=>1, authtoken=>$authtoken);
868 return $e->event unless $e->checkauth;
869 my $copy = $e->retrieve_asset_copy(
873 flesh_fields => { 'acp' => ['call_number'] }
878 return $e->event unless
879 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
881 $note->create_date('now');
882 $note->creator($e->requestor->id);
883 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
886 $e->create_asset_copy_note($note) or return $e->event;
892 __PACKAGE__->register_method(
893 method => 'delete_copy_note',
894 api_name => 'open-ils.circ.copy_note.delete',
896 Deletes an existing copy note
897 @param authtoken The login session key
898 @param noteid The id of the note to delete
899 @return 1 on success - Event otherwise.
901 sub delete_copy_note {
902 my( $self, $conn, $authtoken, $noteid ) = @_;
904 my $e = new_editor(xact=>1, authtoken=>$authtoken);
905 return $e->die_event unless $e->checkauth;
907 my $note = $e->retrieve_asset_copy_note([
911 'acpn' => [ 'owning_copy' ],
912 'acp' => [ 'call_number' ],
915 ]) or return $e->die_event;
917 if( $note->creator ne $e->requestor->id ) {
918 return $e->die_event unless
919 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
922 $e->delete_asset_copy_note($note) or return $e->die_event;
928 __PACKAGE__->register_method(
929 method => 'age_hold_rules',
930 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
934 my( $self, $conn ) = @_;
935 return new_editor()->retrieve_all_config_rules_age_hold_protect();
940 __PACKAGE__->register_method(
941 method => 'copy_details_barcode',
943 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
944 sub copy_details_barcode {
945 my( $self, $conn, $auth, $barcode ) = @_;
946 my $e = new_editor();
947 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
948 return $e->event unless $cid;
949 return copy_details( $self, $conn, $auth, $cid );
953 __PACKAGE__->register_method(
954 method => 'copy_details',
955 api_name => 'open-ils.circ.copy_details.retrieve');
958 my( $self, $conn, $auth, $copy_id ) = @_;
959 my $e = new_editor(authtoken=>$auth);
960 return $e->event unless $e->checkauth;
962 my $flesh = { flesh => 1 };
964 my $copy = $e->retrieve_asset_copy(
970 acp => ['call_number','parts','peer_record_maps','floating'],
971 acn => ['record','prefix','suffix','label_class']
974 ]) or return $e->event;
977 # De-flesh the copy for backwards compatibility
979 my $vol = $copy->call_number;
981 $copy->call_number($vol->id);
982 my $record = $vol->record;
984 $vol->record($record->id);
985 $mvr = $U->record_to_mvr($record);
990 my $hold = $e->search_action_hold_request(
992 current_copy => $copy_id,
993 capture_time => { "!=" => undef },
994 fulfillment_time => undef,
995 cancel_time => undef,
999 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1001 my $transit = $e->search_action_transit_copy(
1002 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1004 # find the latest circ, open or closed
1005 my $circ = $e->search_action_circulation(
1007 { target_copy => $copy_id },
1013 'checkin_workstation',
1016 'recurring_fine_rule'
1019 order_by => { circ => 'xact_start desc' },
1029 transit => $transit,
1039 __PACKAGE__->register_method(
1040 method => 'mark_item',
1041 api_name => 'open-ils.circ.mark_item_damaged',
1043 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1044 @param authtoken The login session key
1045 @param copy_id The ID of the copy to mark as damaged
1046 @return 1 on success - Event otherwise.
1049 __PACKAGE__->register_method(
1050 method => 'mark_item',
1051 api_name => 'open-ils.circ.mark_item_missing',
1053 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1054 @param authtoken The login session key
1055 @param copy_id The ID of the copy to mark as missing
1056 @return 1 on success - Event otherwise.
1059 __PACKAGE__->register_method(
1060 method => 'mark_item',
1061 api_name => 'open-ils.circ.mark_item_bindery',
1063 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1064 @param authtoken The login session key
1065 @param copy_id The ID of the copy to mark as bindery
1066 @return 1 on success - Event otherwise.
1069 __PACKAGE__->register_method(
1070 method => 'mark_item',
1071 api_name => 'open-ils.circ.mark_item_on_order',
1073 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1074 @param authtoken The login session key
1075 @param copy_id The ID of the copy to mark as on order
1076 @return 1 on success - Event otherwise.
1079 __PACKAGE__->register_method(
1080 method => 'mark_item',
1081 api_name => 'open-ils.circ.mark_item_ill',
1083 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1084 @param authtoken The login session key
1085 @param copy_id The ID of the copy to mark as inter-library loan
1086 @return 1 on success - Event otherwise.
1089 __PACKAGE__->register_method(
1090 method => 'mark_item',
1091 api_name => 'open-ils.circ.mark_item_cataloging',
1093 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1094 @param authtoken The login session key
1095 @param copy_id The ID of the copy to mark as cataloging
1096 @return 1 on success - Event otherwise.
1099 __PACKAGE__->register_method(
1100 method => 'mark_item',
1101 api_name => 'open-ils.circ.mark_item_reserves',
1103 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1104 @param authtoken The login session key
1105 @param copy_id The ID of the copy to mark as reserves
1106 @return 1 on success - Event otherwise.
1109 __PACKAGE__->register_method(
1110 method => 'mark_item',
1111 api_name => 'open-ils.circ.mark_item_discard',
1113 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1114 @param authtoken The login session key
1115 @param copy_id The ID of the copy to mark as discard
1116 @return 1 on success - Event otherwise.
1121 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1122 my $e = new_editor(authtoken=>$auth, xact =>1);
1123 return $e->die_event unless $e->checkauth;
1126 my $copy = $e->retrieve_asset_copy([
1128 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1129 or return $e->die_event;
1132 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1133 $copy->circ_lib : $copy->call_number->owning_lib;
1135 my $perm = 'MARK_ITEM_MISSING';
1136 my $stat = OILS_COPY_STATUS_MISSING;
1138 if( $self->api_name =~ /damaged/ ) {
1139 $perm = 'MARK_ITEM_DAMAGED';
1140 $stat = OILS_COPY_STATUS_DAMAGED;
1141 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1142 return $evt if $evt;
1144 } elsif ( $self->api_name =~ /bindery/ ) {
1145 $perm = 'MARK_ITEM_BINDERY';
1146 $stat = OILS_COPY_STATUS_BINDERY;
1147 } elsif ( $self->api_name =~ /on_order/ ) {
1148 $perm = 'MARK_ITEM_ON_ORDER';
1149 $stat = OILS_COPY_STATUS_ON_ORDER;
1150 } elsif ( $self->api_name =~ /ill/ ) {
1151 $perm = 'MARK_ITEM_ILL';
1152 $stat = OILS_COPY_STATUS_ILL;
1153 } elsif ( $self->api_name =~ /cataloging/ ) {
1154 $perm = 'MARK_ITEM_CATALOGING';
1155 $stat = OILS_COPY_STATUS_CATALOGING;
1156 } elsif ( $self->api_name =~ /reserves/ ) {
1157 $perm = 'MARK_ITEM_RESERVES';
1158 $stat = OILS_COPY_STATUS_RESERVES;
1159 } elsif ( $self->api_name =~ /discard/ ) {
1160 $perm = 'MARK_ITEM_DISCARD';
1161 $stat = OILS_COPY_STATUS_DISCARD;
1164 # caller may proceed if either perm is allowed
1165 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1167 $copy->status($stat);
1168 $copy->edit_date('now');
1169 $copy->editor($e->requestor->id);
1171 $e->update_asset_copy($copy) or return $e->die_event;
1173 my $holds = $e->search_action_hold_request(
1175 current_copy => $copy->id,
1176 fulfillment_time => undef,
1177 cancel_time => undef,
1183 if( $self->api_name =~ /damaged/ ) {
1184 # now that we've committed the changes, create related A/T events
1185 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1186 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1189 $logger->debug("resetting holds that target the marked copy");
1190 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1195 sub handle_mark_damaged {
1196 my($e, $copy, $owning_lib, $args) = @_;
1198 my $apply = $args->{apply_fines} || '';
1199 return undef if $apply eq 'noapply';
1201 my $new_amount = $args->{override_amount};
1202 my $new_btype = $args->{override_btype};
1203 my $new_note = $args->{override_note};
1205 # grab the last circulation
1206 my $circ = $e->search_action_circulation([
1207 { target_copy => $copy->id},
1209 order_by => {circ => "xact_start DESC"},
1211 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1215 return undef unless $circ;
1217 my $charge_price = $U->ou_ancestor_setting_value(
1218 $owning_lib, 'circ.charge_on_damaged', $e);
1220 my $proc_fee = $U->ou_ancestor_setting_value(
1221 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1223 my $void_overdue = $U->ou_ancestor_setting_value(
1224 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1226 return undef unless $charge_price or $proc_fee;
1228 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1229 my $total = $copy_price + $proc_fee;
1233 if($new_amount and $new_btype) {
1235 # Allow staff to override the amount to charge for a damaged item
1236 # Consider the case where the item is only partially damaged
1237 # This value is meant to take the place of the item price and
1238 # optional processing fee.
1240 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1241 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1242 return $evt if $evt;
1246 if($charge_price and $copy_price) {
1247 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1248 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1249 return $evt if $evt;
1253 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1254 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1255 return $evt if $evt;
1259 # the assumption is that you would not void the overdues unless you
1260 # were also charging for the item and/or applying a processing fee
1262 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1263 return $evt if $evt;
1266 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1267 return $evt if $evt;
1269 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1270 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1272 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1273 return $evt2 if $evt2;
1278 return OpenILS::Event->new('DAMAGE_CHARGE',
1289 # ----------------------------------------------------------------------
1290 __PACKAGE__->register_method(
1291 method => 'mark_item_missing_pieces',
1292 api_name => 'open-ils.circ.mark_item_missing_pieces',
1294 Changes the status of a copy to "damaged" or to a custom status based on the
1295 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1297 @param authtoken The login session key
1298 @param copy_id The ID of the copy to mark as damaged
1299 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1303 sub mark_item_missing_pieces {
1304 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1305 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1306 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1308 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1309 return $e2->die_event unless $e2->checkauth;
1312 my $copy = $e2->retrieve_asset_copy([
1314 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1315 or return $e2->die_event;
1318 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1319 $copy->circ_lib : $copy->call_number->owning_lib;
1321 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1323 #### grab the last circulation
1324 my $circ = $e2->search_action_circulation([
1325 { target_copy => $copy->id},
1327 order_by => {circ => "xact_start DESC"}
1332 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1334 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1337 my $holds = $e2->search_action_hold_request(
1339 current_copy => $copy->id,
1340 fulfillment_time => undef,
1341 cancel_time => undef,
1345 $logger->debug("resetting holds that target the marked copy");
1346 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1349 if (! $e2->commit) {
1350 return $e2->die_event;
1353 my $e = new_editor(authtoken=>$auth, xact =>1);
1354 return $e->die_event unless $e->checkauth;
1356 if (! $circ->checkin_time) { # if circ active, attempt renew
1357 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1358 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1359 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1360 $circ = $res->[0]->{payload}{'circ'};
1361 $circ->target_copy( $copy->id );
1362 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1364 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1369 'copy_id'=>$circ->target_copy,
1370 'patron_id'=>$circ->usr,
1371 'skip_deposit_fee'=>1,
1372 'skip_rental_fee'=>1
1375 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1377 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1378 $e, $copy, $e->requestor, 1 );
1380 if ($hold) { # needed for hold? then due now
1382 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1383 my $due_date = DateTime->now(time_zone => 'local');
1384 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1386 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1390 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1391 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1392 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1393 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1394 $circ = $res->[0]->{payload}{'circ'};
1396 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1402 ### Update the item status
1404 my $custom_stat = $U->ou_ancestor_setting_value(
1405 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1406 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1408 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1409 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1411 $copy->status($stat);
1412 $copy->edit_date('now');
1413 $copy->editor($e->requestor->id);
1415 $e->update_asset_copy($copy) or return $e->die_event;
1419 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1420 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1422 return OpenILS::Event->new('SUCCESS',
1426 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1427 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1432 return $e->die_event;
1440 # ----------------------------------------------------------------------
1441 __PACKAGE__->register_method(
1442 method => 'magic_fetch',
1443 api_name => 'open-ils.agent.fetch'
1446 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1449 my( $self, $conn, $auth, $args ) = @_;
1450 my $e = new_editor( authtoken => $auth );
1451 return $e->event unless $e->checkauth;
1453 my $hint = $$args{hint};
1454 my $id = $$args{id};
1456 # Is the call allowed to fetch this type of object?
1457 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1459 # Find the class the implements the given hint
1460 my ($class) = grep {
1461 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1463 $class =~ s/Fieldmapper:://og;
1464 $class =~ s/::/_/og;
1465 my $method = "retrieve_$class";
1467 my $obj = $e->$method($id) or return $e->event;
1470 # ----------------------------------------------------------------------
1473 __PACKAGE__->register_method(
1474 method => "fleshed_circ_retrieve",
1476 api_name => "open-ils.circ.fleshed.retrieve",);
1478 sub fleshed_circ_retrieve {
1479 my( $self, $client, $id ) = @_;
1480 my $e = new_editor();
1481 my $circ = $e->retrieve_action_circulation(
1487 circ => [ qw/ target_copy / ],
1488 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1489 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1490 acn => [ qw/ record / ],
1494 ) or return $e->event;
1496 my $copy = $circ->target_copy;
1497 my $vol = $copy->call_number;
1498 my $rec = $circ->target_copy->call_number->record;
1500 $vol->record($rec->id);
1501 $copy->call_number($vol->id);
1502 $circ->target_copy($copy->id);
1506 if( $rec->id == OILS_PRECAT_RECORD ) {
1510 $mvr = $U->record_to_mvr($rec);
1511 $rec->marc(''); # drop the bulky marc data
1525 __PACKAGE__->register_method(
1526 method => "test_batch_circ_events",
1527 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1530 # method for testing the behavior of a given event definition
1531 sub test_batch_circ_events {
1532 my($self, $conn, $auth, $event_def, $barcode) = @_;
1534 my $e = new_editor(authtoken => $auth);
1535 return $e->event unless $e->checkauth;
1536 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1538 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1539 or return $e->event;
1541 my $circ = $e->search_action_circulation(
1542 {target_copy => $copy->id, checkin_time => undef})->[0]
1543 or return $e->event;
1545 return undef unless $circ;
1547 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1551 __PACKAGE__->register_method(
1552 method => "fire_circ_events",
1553 api_name => "open-ils.circ.fire_circ_trigger_events",
1555 General event def runner for circ objects. If no event def ID
1556 is provided, the hook will be used to find the best event_def
1557 match based on the context org unit
1561 __PACKAGE__->register_method(
1562 method => "fire_circ_events",
1563 api_name => "open-ils.circ.fire_hold_trigger_events",
1565 General event def runner for hold objects. If no event def ID
1566 is provided, the hook will be used to find the best event_def
1567 match based on the context org unit
1571 __PACKAGE__->register_method(
1572 method => "fire_circ_events",
1573 api_name => "open-ils.circ.fire_user_trigger_events",
1575 General event def runner for user objects. If no event def ID
1576 is provided, the hook will be used to find the best event_def
1577 match based on the context org unit
1582 sub fire_circ_events {
1583 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1585 my $e = new_editor(authtoken => $auth, xact => 1);
1586 return $e->event unless $e->checkauth;
1590 if($self->api_name =~ /hold/) {
1591 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1592 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1593 } elsif($self->api_name =~ /user/) {
1594 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1595 $targets = $e->batch_retrieve_actor_user($target_ids);
1597 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1598 $targets = $e->batch_retrieve_action_circulation($target_ids);
1600 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1601 # simply making this method authoritative because of weirdness
1602 # with transaction handling in A/T code that causes rollback
1603 # failure down the line if handling many targets
1605 return undef unless @$targets;
1606 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1609 __PACKAGE__->register_method(
1610 method => "user_payments_list",
1611 api_name => "open-ils.circ.user_payments.filtered.batch",
1614 desc => q/Returns a fleshed, date-limited set of all payments a user
1615 has made. By default, ordered by payment date. Optionally
1616 ordered by other columns in the top-level "mp" object/,
1618 {desc => 'Authentication token', type => 'string'},
1619 {desc => 'User ID', type => 'number'},
1620 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1622 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1623 and the related fully-realized payment object (e.g money.cash_payment)/}
1627 sub user_payments_list {
1628 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1630 my $e = new_editor(authtoken => $auth);
1631 return $e->event unless $e->checkauth;
1633 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1634 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1636 $order_by ||= ['payment_ts'];
1638 # all payments by user, between start_date and end_date
1639 my $payments = $e->json_query({
1640 select => {mp => ['id']},
1644 fkey => 'xact', field => 'id'}
1648 '+mbt' => {usr => $user_id},
1649 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1651 order_by => {mp => $order_by}
1654 for my $payment_id (@$payments) {
1655 my $payment = $e->retrieve_money_payment([
1663 'credit_card_payment',
1678 $conn->respond($payment);
1685 __PACKAGE__->register_method(
1686 method => "retrieve_circ_chain",
1687 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1690 desc => q/Given a circulation, this returns all circulation objects
1691 that are part of the same chain of renewals./,
1693 {desc => 'Authentication token', type => 'string'},
1694 {desc => 'Circ ID', type => 'number'},
1696 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1700 __PACKAGE__->register_method(
1701 method => "retrieve_circ_chain",
1702 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1704 desc => q/Given a circulation, this returns a summary of the circulation objects
1705 that are part of the same chain of renewals./,
1707 {desc => 'Authentication token', type => 'string'},
1708 {desc => 'Circ ID', type => 'number'},
1710 return => {desc => q/Circulation Chain Summary/}
1714 sub retrieve_circ_chain {
1715 my($self, $conn, $auth, $circ_id) = @_;
1717 my $e = new_editor(authtoken => $auth);
1718 return $e->event unless $e->checkauth;
1719 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1721 if($self->api_name =~ /summary/) {
1722 return $U->create_circ_chain_summary($e, $circ_id);
1726 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1728 for my $circ_info (@$chain) {
1729 my $circ = Fieldmapper::action::circulation->new;
1730 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1731 $conn->respond($circ);
1738 __PACKAGE__->register_method(
1739 method => "retrieve_prev_circ_chain",
1740 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1743 desc => q/Given a circulation, this returns all circulation objects
1744 that are part of the previous chain of renewals./,
1746 {desc => 'Authentication token', type => 'string'},
1747 {desc => 'Circ ID', type => 'number'},
1749 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1753 __PACKAGE__->register_method(
1754 method => "retrieve_prev_circ_chain",
1755 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1757 desc => q/Given a circulation, this returns a summary of the circulation objects
1758 that are part of the previous chain of renewals./,
1760 {desc => 'Authentication token', type => 'string'},
1761 {desc => 'Circ ID', type => 'number'},
1763 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1767 sub retrieve_prev_circ_chain {
1768 my($self, $conn, $auth, $circ_id) = @_;
1770 my $e = new_editor(authtoken => $auth);
1771 return $e->event unless $e->checkauth;
1772 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1774 if($self->api_name =~ /summary/) {
1775 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1776 my $target_copy = $$first_circ{'target_copy'};
1777 my $usr = $$first_circ{'usr'};
1778 my $last_circ_from_prev_chain = $e->json_query({
1779 'select' => { 'circ' => ['id','usr'] },
1782 target_copy => $target_copy,
1783 xact_start => { '<' => $$first_circ{'xact_start'} }
1785 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1788 return undef unless $last_circ_from_prev_chain;
1789 return undef unless $$last_circ_from_prev_chain{'id'};
1790 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1791 return undef unless $sum;
1792 my $obj = Fieldmapper::action::circ_chain_summary->new;
1793 $obj->$_($sum->{$_}) for keys %$sum;
1794 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1798 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1799 my $target_copy = $$first_circ{'target_copy'};
1800 my $last_circ_from_prev_chain = $e->json_query({
1801 'select' => { 'circ' => ['id'] },
1804 target_copy => $target_copy,
1805 xact_start => { '<' => $$first_circ{'xact_start'} }
1807 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1810 return undef unless $last_circ_from_prev_chain;
1811 return undef unless $$last_circ_from_prev_chain{'id'};
1812 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1814 for my $circ_info (@$chain) {
1815 my $circ = Fieldmapper::action::circulation->new;
1816 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1817 $conn->respond($circ);
1825 __PACKAGE__->register_method(
1826 method => "get_copy_due_date",
1827 api_name => "open-ils.circ.copy.due_date.retrieve",
1830 Given a copy ID, returns the due date for the copy if it's
1831 currently circulating. Otherwise, returns null. Note, this is a public
1832 method requiring no authentication. Only the due date is exposed.
1835 {desc => 'Copy ID', type => 'number'}
1837 return => {desc => q/
1838 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1843 sub get_copy_due_date {
1844 my($self, $conn, $copy_id) = @_;
1845 my $e = new_editor();
1847 my $circ = $e->json_query({
1848 select => {circ => ['due_date']},
1851 target_copy => $copy_id,
1852 checkin_time => undef,
1854 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1855 {stop_fines => undef}
1859 })->[0] or return undef;
1861 return $circ->{due_date};
1868 # {"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}}