1 package OpenILS::Application::Circ;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
6 use OpenILS::Application::Circ::Circulate;
7 use OpenILS::Application::Circ::Survey;
8 use OpenILS::Application::Circ::StatCat;
9 use OpenILS::Application::Circ::Holds;
10 use OpenILS::Application::Circ::HoldNotify;
11 use OpenILS::Application::Circ::CreditCard;
12 use OpenILS::Application::Circ::Money;
13 use OpenILS::Application::Circ::NonCat;
14 use OpenILS::Application::Circ::CopyLocations;
15 use OpenILS::Application::Circ::CircCommon;
18 use DateTime::Format::ISO8601;
20 use OpenILS::Application::AppUtils;
22 use OpenSRF::Utils qw/:datetime/;
23 use OpenSRF::AppSession;
24 use OpenILS::Utils::ModsParser;
26 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::Fieldmapper;
29 use OpenILS::Utils::Editor;
30 use OpenILS::Utils::CStoreEditor q/:funcs/;
31 use OpenILS::Const qw/:const/;
32 use OpenSRF::Utils::SettingsClient;
33 use OpenILS::Application::Cat::AssetCommon;
35 my $apputils = "OpenILS::Application::AppUtils";
38 my $holdcode = "OpenILS::Application::Circ::Holds";
40 # ------------------------------------------------------------------------
41 # Top level Circ package;
42 # ------------------------------------------------------------------------
46 OpenILS::Application::Circ::Circulate->initialize();
50 __PACKAGE__->register_method(
51 method => 'retrieve_circ',
53 api_name => 'open-ils.circ.retrieve',
55 Retrieve a circ object by id
56 @param authtoken Login session key
57 @pararm circid The id of the circ object
61 my( $s, $c, $a, $i ) = @_;
62 my $e = new_editor(authtoken => $a);
63 return $e->event unless $e->checkauth;
64 my $circ = $e->retrieve_action_circulation($i) or return $e->event;
65 if( $e->requestor->id ne $circ->usr ) {
66 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
72 __PACKAGE__->register_method(
73 method => 'fetch_circ_mods',
74 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
76 my($self, $conn, $args) = @_;
77 my $mods = new_editor()->retrieve_all_config_circ_modifier;
78 return [ map {$_->code} @$mods ] unless $$args{full};
82 __PACKAGE__->register_method(
83 method => 'ranged_billing_types',
84 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
86 sub ranged_billing_types {
87 my($self, $conn, $auth, $org_id, $depth) = @_;
88 my $e = new_editor(authtoken => $auth);
89 return $e->event unless $e->checkauth;
90 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
91 return $e->search_config_billing_type(
92 {owner => $U->get_org_full_path($org_id, $depth)});
97 # ------------------------------------------------------------------------
98 # Returns an array of {circ, record} hashes checked out by the user.
99 # ------------------------------------------------------------------------
100 __PACKAGE__->register_method(
101 method => "checkouts_by_user",
102 api_name => "open-ils.circ.actor.user.checked_out",
104 NOTES => <<" NOTES");
105 Returns a list of open circulations as a pile of objects. Each object
106 contains the relevant copy, circ, and record
109 sub checkouts_by_user {
110 my($self, $client, $auth, $user_id) = @_;
112 my $e = new_editor(authtoken=>$auth);
113 return $e->event unless $e->checkauth;
115 my $circ_ids = $e->search_action_circulation(
117 checkin_time => undef,
119 {stop_fines => undef},
120 {stop_fines => ['MAXFINES','LONGOVERDUE']}
126 for my $id (@$circ_ids) {
127 my $circ = $e->retrieve_action_circulation([
131 circ => ['target_copy'],
132 acp => ['call_number'],
138 # un-flesh for consistency
139 my $c = $circ->target_copy;
140 $circ->target_copy($c->id);
142 my $cn = $c->call_number;
143 $c->call_number($cn->id);
151 record => $U->record_to_mvr($t)
161 __PACKAGE__->register_method(
162 method => "checkouts_by_user_slim",
163 api_name => "open-ils.circ.actor.user.checked_out.slim",
164 NOTES => <<" NOTES");
165 Returns a list of open circulation objects
169 sub checkouts_by_user_slim {
170 my( $self, $client, $user_session, $user_id ) = @_;
172 my( $requestor, $target, $copy, $record, $evt );
174 ( $requestor, $target, $evt ) =
175 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
178 $logger->debug( 'User ' . $requestor->id .
179 " retrieving checked out items for user " . $target->id );
181 # XXX Make the call correct..
182 return $apputils->simplereq(
184 "open-ils.cstore.direct.action.open_circulation.search.atomic",
185 { usr => $target->id, checkin_time => undef } );
186 # { usr => $target->id } );
190 __PACKAGE__->register_method(
191 method => "checkouts_by_user_opac",
192 api_name => "open-ils.circ.actor.user.checked_out.opac",);
195 sub checkouts_by_user_opac {
196 my( $self, $client, $auth, $user_id ) = @_;
198 my $e = OpenILS::Utils::Editor->new( authtoken => $auth );
199 return $e->event unless $e->checkauth;
200 $user_id ||= $e->requestor->id;
201 return $e->event unless
202 my $patron = $e->retrieve_actor_user($user_id);
205 my $search = {usr => $user_id, stop_fines => undef};
207 if( $user_id ne $e->requestor->id ) {
208 $data = $e->search_action_circulation(
209 $search, {checkperm=>1, permorg=>$patron->home_ou})
213 $data = $e->search_action_circulation($search);
220 __PACKAGE__->register_method(
221 method => "title_from_transaction",
222 api_name => "open-ils.circ.circ_transaction.find_title",
223 NOTES => <<" NOTES");
224 Returns a mods object for the title that is linked to from the
225 copy from the hold that created the given transaction
228 sub title_from_transaction {
229 my( $self, $client, $login_session, $transactionid ) = @_;
231 my( $user, $circ, $title, $evt );
233 ( $user, $evt ) = $apputils->checkses( $login_session );
236 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
239 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
242 return $apputils->record_to_mvr($title);
245 __PACKAGE__->register_method(
246 method => "staff_age_to_lost",
247 api_name => "open-ils.circ.circulation.age_to_lost",
250 This fires a circ.staff_age_to_lost Action-Trigger event against all
251 overdue circulations in scope of the specified context library and
252 user profile, which effectively marks the associated items as Lost.
253 This is likely to be done at the end of a semester in an academic
256 @param args : circ_lib, user_profile
260 sub staff_age_to_lost {
261 my( $self, $conn, $auth, $args ) = @_;
262 my $e = new_editor(authtoken=>$auth);
263 return $e->event unless $e->checkauth;
264 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
266 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
267 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
269 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
271 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
272 my $hook = 'circ.staff_age_to_lost';
273 my $context_org = 'circ_lib';
274 my $opt_granularity = undef;
276 "checkin_time" => undef,
277 "due_date" => { "<" => "now" },
279 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
280 { "stop_fines" => undef }
284 "select" => {"au" => ["id"]},
287 "profile" => $profiles,
288 "id" => { "=" => {"+circ" => "usr"} }
292 "select" => {"aou" => ["id"]},
296 {"id" => { "=" => {"+circ" => "circ_lib"} }},
303 my $req_timeout = 10800;
304 my $chunk_size = 100;
307 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
308 my @event_ids; my @chunked_ids;
309 while (my $resp = $req->recv(timeout => $req_timeout)) {
310 push(@event_ids, $resp->content);
311 push(@chunked_ids, $resp->content);
312 if (scalar(@chunked_ids) > $chunk_size) {
313 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
317 if (scalar(@chunked_ids) > 0) {
318 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
322 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
323 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
324 } elsif($req->complete) {
325 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
326 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
328 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
329 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
336 __PACKAGE__->register_method(
337 method => "new_set_circ_lost",
338 api_name => "open-ils.circ.circulation.set_lost",
340 Sets the copy and related open circulation to lost
342 @param args : barcode
347 # ---------------------------------------------------------------------
348 # Sets a circulation to lost. updates copy status to lost
349 # applies copy and/or prcoessing fees depending on org settings
350 # ---------------------------------------------------------------------
351 sub new_set_circ_lost {
352 my( $self, $conn, $auth, $args ) = @_;
354 my $e = new_editor(authtoken=>$auth, xact=>1);
355 return $e->die_event unless $e->checkauth;
357 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
358 or return $e->die_event;
360 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
368 __PACKAGE__->register_method(
369 method => "set_circ_claims_returned",
370 api_name => "open-ils.circ.circulation.set_claims_returned",
372 desc => q/Sets the circ for a given item as claims returned
373 If a backdate is provided, overdue fines will be voided
374 back to the backdate/,
376 {desc => 'Authentication token', type => 'string'},
377 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
379 return => {desc => q/1 on success, failure event on error, and
380 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
381 configured claims return maximum/}
385 __PACKAGE__->register_method(
386 method => "set_circ_claims_returned",
387 api_name => "open-ils.circ.circulation.set_claims_returned.override",
389 desc => q/This adds support for overrideing the configured max
390 claims returned amount.
391 @see open-ils.circ.circulation.set_claims_returned./,
395 sub set_circ_claims_returned {
396 my( $self, $conn, $auth, $args, $oargs ) = @_;
398 my $e = new_editor(authtoken=>$auth, xact=>1);
399 return $e->die_event unless $e->checkauth;
401 $oargs = { all => 1 } unless defined $oargs;
403 my $barcode = $$args{barcode};
404 my $backdate = $$args{backdate};
406 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
407 or return $e->die_event;
409 my $circ = $e->search_action_circulation(
410 {checkin_time => undef, target_copy => $copy->id})->[0]
411 or return $e->die_event;
413 $backdate = $circ->due_date if $$args{use_due_date};
415 $logger->info("marking circ for item $barcode as claims returned".
416 (($backdate) ? " with backdate $backdate" : ''));
418 my $patron = $e->retrieve_actor_user($circ->usr);
419 my $max_count = $U->ou_ancestor_setting_value(
420 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
422 # If the patron has too instances of many claims returned,
423 # require an override to continue. A configured max of
424 # 0 means all attempts require an override
425 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
427 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
429 # see if we're allowed to override
430 return $e->die_event unless
431 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
435 # exit early and return the max claims return event
437 return OpenILS::Event->new(
438 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
440 patron_count => $patron->claims_returned_count,
441 max_count => $max_count
447 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
448 or return $e->die_event;
450 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
451 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
454 $backdate = cleanse_ISO8601($backdate);
456 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
457 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
458 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
460 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
461 $backdate = cleanse_ISO8601($backdate);
463 # make it look like the circ stopped at the cliams returned time
464 $circ->stop_fines_time($backdate);
465 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
469 $e->update_action_circulation($circ) or return $e->die_event;
471 # see if there is a configured post-claims-return copy status
472 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
473 $copy->status($stat);
474 $copy->edit_date('now');
475 $copy->editor($e->requestor->id);
476 $e->update_asset_copy($copy) or return $e->die_event;
484 __PACKAGE__->register_method(
485 method => "post_checkin_backdate_circ",
486 api_name => "open-ils.circ.post_checkin_backdate",
488 desc => q/Back-date an already checked in circulation/,
490 {desc => 'Authentication token', type => 'string'},
491 {desc => 'Circ ID', type => 'number'},
492 {desc => 'ISO8601 backdate', type => 'string'},
494 return => {desc => q/1 on success, failure event on error/}
498 __PACKAGE__->register_method(
499 method => "post_checkin_backdate_circ",
500 api_name => "open-ils.circ.post_checkin_backdate.batch",
503 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
505 {desc => 'Authentication token', type => 'string'},
506 {desc => 'List of Circ ID', type => 'array'},
507 {desc => 'ISO8601 backdate', type => 'string'},
509 return => {desc => q/Set of: 1 on success, failure event on error/}
514 sub post_checkin_backdate_circ {
515 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
516 my $e = new_editor(authtoken=>$auth);
517 return $e->die_event unless $e->checkauth;
518 if($self->api_name =~ /batch/) {
519 foreach my $c (@$circ_id) {
520 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
523 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
531 sub post_checkin_backdate_circ_impl {
532 my($e, $circ_id, $backdate) = @_;
536 my $circ = $e->retrieve_action_circulation($circ_id)
537 or return $e->die_event;
539 # anyone with checkin perms can backdate (more restrictive?)
540 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
542 # don't allow back-dating an open circulation
543 return OpenILS::Event->new('BAD_PARAMS') unless
544 $backdate and $circ->checkin_time;
546 # update the checkin and stop_fines times to reflect the new backdate
547 $circ->stop_fines_time(cleanse_ISO8601($backdate));
548 $circ->checkin_time(cleanse_ISO8601($backdate));
549 $e->update_action_circulation($circ) or return $e->die_event;
551 # now void the overdues "erased" by the back-dating
552 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
555 # If the circ was closed before and the balance owned !=0, re-open the transaction
556 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
565 __PACKAGE__->register_method (
566 method => 'set_circ_due_date',
567 api_name => 'open-ils.circ.circulation.due_date.update',
569 Updates the due_date on the given circ
571 @param circid The id of the circ to update
572 @param date The timestamp of the new due date
576 sub set_circ_due_date {
577 my( $self, $conn, $auth, $circ_id, $date ) = @_;
579 my $e = new_editor(xact=>1, authtoken=>$auth);
580 return $e->die_event unless $e->checkauth;
581 my $circ = $e->retrieve_action_circulation($circ_id)
582 or return $e->die_event;
584 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
585 $date = cleanse_ISO8601($date);
587 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
588 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
589 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
590 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
593 $circ->due_date($date);
594 $e->update_action_circulation($circ) or return $e->die_event;
601 __PACKAGE__->register_method(
602 method => "create_in_house_use",
603 api_name => 'open-ils.circ.in_house_use.create',
605 Creates an in-house use action.
606 @param $authtoken The login session key
607 @param params A hash of params including
608 'location' The org unit id where the in-house use occurs
609 'copyid' The copy in question
610 'count' The number of in-house uses to apply to this copy
611 @return An array of id's representing the id's of the newly created
612 in-house use objects or an event on an error
615 __PACKAGE__->register_method(
616 method => "create_in_house_use",
617 api_name => 'open-ils.circ.non_cat_in_house_use.create',
621 sub create_in_house_use {
622 my( $self, $client, $auth, $params ) = @_;
625 my $org = $params->{location};
626 my $copyid = $params->{copyid};
627 my $count = $params->{count} || 1;
628 my $nc_type = $params->{non_cat_type};
629 my $use_time = $params->{use_time} || 'now';
631 my $e = new_editor(xact=>1,authtoken=>$auth);
632 return $e->event unless $e->checkauth;
633 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
635 my $non_cat = 1 if $self->api_name =~ /non_cat/;
639 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
641 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
647 if( $use_time ne 'now' ) {
648 $use_time = cleanse_ISO8601($use_time);
649 $logger->debug("in_house_use setting use time to $use_time");
660 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
661 $ihu->item_type($nc_type);
662 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
663 $cmeth = "create_action_non_cat_in_house_use";
666 $ihu = Fieldmapper::action::in_house_use->new;
668 $method = 'open-ils.storage.direct.action.in_house_use.create';
669 $cmeth = "create_action_in_house_use";
672 $ihu->staff($e->requestor->id);
673 $ihu->org_unit($org);
674 $ihu->use_time($use_time);
676 $ihu = $e->$cmeth($ihu) or return $e->event;
677 push( @ids, $ihu->id );
688 __PACKAGE__->register_method(
689 method => "view_circs",
690 api_name => "open-ils.circ.copy_checkout_history.retrieve",
692 Retrieves the last X circs for a given copy
693 @param authtoken The login session key
694 @param copyid The copy to check
695 @param count How far to go back in the item history
696 @return An array of circ ids
699 # ----------------------------------------------------------------------
700 # Returns $count most recent circs. If count exceeds the configured
701 # max, use the configured max instead
702 # ----------------------------------------------------------------------
704 my( $self, $client, $authtoken, $copyid, $count ) = @_;
706 my $e = new_editor(authtoken => $authtoken);
707 return $e->event unless $e->checkauth;
709 my $copy = $e->retrieve_asset_copy([
712 flesh_fields => {acp => ['call_number']}
714 ]) or return $e->event;
716 return $e->event unless $e->allowed(
717 'VIEW_COPY_CHECKOUT_HISTORY',
718 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
719 $copy->circ_lib : $copy->call_number->owning_lib);
721 my $max_history = $U->ou_ancestor_setting_value(
722 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
724 if(defined $max_history) {
725 $count = $max_history unless defined $count and $count < $max_history;
727 $count = 4 unless defined $count;
730 return $e->search_action_circulation([
731 {target_copy => $copyid},
732 {limit => $count, order_by => { circ => "xact_start DESC" }}
737 __PACKAGE__->register_method(
738 method => "circ_count",
739 api_name => "open-ils.circ.circulation.count",
741 Returns the number of times the item has circulated
742 @param copyid The copy to check
746 my( $self, $client, $copyid, $range ) = @_;
747 my $e = OpenILS::Utils::Editor->new;
748 return $e->request('open-ils.storage.asset.copy.circ_count', $copyid, $range);
753 __PACKAGE__->register_method(
754 method => 'fetch_notes',
756 api_name => 'open-ils.circ.copy_note.retrieve.all',
758 Returns an array of copy note objects.
759 @param args A named hash of parameters including:
760 authtoken : Required if viewing non-public notes
761 itemid : The id of the item whose notes we want to retrieve
762 pub : True if all the caller wants are public notes
763 @return An array of note objects
766 __PACKAGE__->register_method(
767 method => 'fetch_notes',
768 api_name => 'open-ils.circ.call_number_note.retrieve.all',
769 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
771 __PACKAGE__->register_method(
772 method => 'fetch_notes',
773 api_name => 'open-ils.circ.title_note.retrieve.all',
774 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
777 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
779 my( $self, $connection, $args ) = @_;
781 my $id = $$args{itemid};
782 my $authtoken = $$args{authtoken};
785 if( $self->api_name =~ /copy/ ) {
787 return $U->cstorereq(
788 'open-ils.cstore.direct.asset.copy_note.search.atomic',
789 { owning_copy => $id, pub => 't' } );
791 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
793 return $U->cstorereq(
794 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
797 } elsif( $self->api_name =~ /call_number/ ) {
799 return $U->cstorereq(
800 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
801 { call_number => $id, pub => 't' } );
803 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
805 return $U->cstorereq(
806 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
809 } elsif( $self->api_name =~ /title/ ) {
811 return $U->cstorereq(
812 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
813 { record => $id, pub => 't' } );
815 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
817 return $U->cstorereq(
818 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
825 __PACKAGE__->register_method(
826 method => 'has_notes',
827 api_name => 'open-ils.circ.copy.has_notes');
828 __PACKAGE__->register_method(
829 method => 'has_notes',
830 api_name => 'open-ils.circ.call_number.has_notes');
831 __PACKAGE__->register_method(
832 method => 'has_notes',
833 api_name => 'open-ils.circ.title.has_notes');
837 my( $self, $conn, $authtoken, $id ) = @_;
838 my $editor = OpenILS::Utils::Editor->new(authtoken => $authtoken);
839 return $editor->event unless $editor->checkauth;
841 my $n = $editor->search_asset_copy_note(
842 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
844 $n = $editor->search_asset_call_number_note(
845 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
847 $n = $editor->search_biblio_record_note(
848 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
855 __PACKAGE__->register_method(
856 method => 'create_copy_note',
857 api_name => 'open-ils.circ.copy_note.create',
859 Creates a new copy note
860 @param authtoken The login session key
861 @param note The note object to create
862 @return The id of the new note object
865 sub create_copy_note {
866 my( $self, $connection, $authtoken, $note ) = @_;
868 my $e = new_editor(xact=>1, authtoken=>$authtoken);
869 return $e->event unless $e->checkauth;
870 my $copy = $e->retrieve_asset_copy(
874 flesh_fields => { 'acp' => ['call_number'] }
879 return $e->event unless
880 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
882 $note->create_date('now');
883 $note->creator($e->requestor->id);
884 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
887 $e->create_asset_copy_note($note) or return $e->event;
893 __PACKAGE__->register_method(
894 method => 'delete_copy_note',
895 api_name => 'open-ils.circ.copy_note.delete',
897 Deletes an existing copy note
898 @param authtoken The login session key
899 @param noteid The id of the note to delete
900 @return 1 on success - Event otherwise.
902 sub delete_copy_note {
903 my( $self, $conn, $authtoken, $noteid ) = @_;
905 my $e = new_editor(xact=>1, authtoken=>$authtoken);
906 return $e->die_event unless $e->checkauth;
908 my $note = $e->retrieve_asset_copy_note([
912 'acpn' => [ 'owning_copy' ],
913 'acp' => [ 'call_number' ],
916 ]) or return $e->die_event;
918 if( $note->creator ne $e->requestor->id ) {
919 return $e->die_event unless
920 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
923 $e->delete_asset_copy_note($note) or return $e->die_event;
929 __PACKAGE__->register_method(
930 method => 'age_hold_rules',
931 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
935 my( $self, $conn ) = @_;
936 return new_editor()->retrieve_all_config_rules_age_hold_protect();
941 __PACKAGE__->register_method(
942 method => 'copy_details_barcode',
944 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
945 sub copy_details_barcode {
946 my( $self, $conn, $auth, $barcode ) = @_;
947 my $e = new_editor();
948 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
949 return $e->event unless $cid;
950 return copy_details( $self, $conn, $auth, $cid );
954 __PACKAGE__->register_method(
955 method => 'copy_details',
956 api_name => 'open-ils.circ.copy_details.retrieve');
959 my( $self, $conn, $auth, $copy_id ) = @_;
960 my $e = new_editor(authtoken=>$auth);
961 return $e->event unless $e->checkauth;
963 my $flesh = { flesh => 1 };
965 my $copy = $e->retrieve_asset_copy(
971 acp => ['call_number','parts','peer_record_maps','floating'],
972 acn => ['record','prefix','suffix','label_class']
975 ]) or return $e->event;
978 # De-flesh the copy for backwards compatibility
980 my $vol = $copy->call_number;
982 $copy->call_number($vol->id);
983 my $record = $vol->record;
985 $vol->record($record->id);
986 $mvr = $U->record_to_mvr($record);
991 my $hold = $e->search_action_hold_request(
993 current_copy => $copy_id,
994 capture_time => { "!=" => undef },
995 fulfillment_time => undef,
996 cancel_time => undef,
1000 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1002 my $transit = $e->search_action_transit_copy(
1003 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1005 # find the latest circ, open or closed
1006 my $circ = $e->search_action_circulation(
1008 { target_copy => $copy_id },
1014 'checkin_workstation',
1017 'recurring_fine_rule'
1020 order_by => { circ => 'xact_start desc' },
1030 transit => $transit,
1040 __PACKAGE__->register_method(
1041 method => 'mark_item',
1042 api_name => 'open-ils.circ.mark_item_damaged',
1044 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1045 @param authtoken The login session key
1046 @param copy_id The ID of the copy to mark as damaged
1047 @return 1 on success - Event otherwise.
1050 __PACKAGE__->register_method(
1051 method => 'mark_item',
1052 api_name => 'open-ils.circ.mark_item_missing',
1054 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1055 @param authtoken The login session key
1056 @param copy_id The ID of the copy to mark as missing
1057 @return 1 on success - Event otherwise.
1060 __PACKAGE__->register_method(
1061 method => 'mark_item',
1062 api_name => 'open-ils.circ.mark_item_bindery',
1064 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1065 @param authtoken The login session key
1066 @param copy_id The ID of the copy to mark as bindery
1067 @return 1 on success - Event otherwise.
1070 __PACKAGE__->register_method(
1071 method => 'mark_item',
1072 api_name => 'open-ils.circ.mark_item_on_order',
1074 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1075 @param authtoken The login session key
1076 @param copy_id The ID of the copy to mark as on order
1077 @return 1 on success - Event otherwise.
1080 __PACKAGE__->register_method(
1081 method => 'mark_item',
1082 api_name => 'open-ils.circ.mark_item_ill',
1084 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1085 @param authtoken The login session key
1086 @param copy_id The ID of the copy to mark as inter-library loan
1087 @return 1 on success - Event otherwise.
1090 __PACKAGE__->register_method(
1091 method => 'mark_item',
1092 api_name => 'open-ils.circ.mark_item_cataloging',
1094 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1095 @param authtoken The login session key
1096 @param copy_id The ID of the copy to mark as cataloging
1097 @return 1 on success - Event otherwise.
1100 __PACKAGE__->register_method(
1101 method => 'mark_item',
1102 api_name => 'open-ils.circ.mark_item_reserves',
1104 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1105 @param authtoken The login session key
1106 @param copy_id The ID of the copy to mark as reserves
1107 @return 1 on success - Event otherwise.
1110 __PACKAGE__->register_method(
1111 method => 'mark_item',
1112 api_name => 'open-ils.circ.mark_item_discard',
1114 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1115 @param authtoken The login session key
1116 @param copy_id The ID of the copy to mark as discard
1117 @return 1 on success - Event otherwise.
1122 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1123 my $e = new_editor(authtoken=>$auth, xact =>1);
1124 return $e->die_event unless $e->checkauth;
1127 my $copy = $e->retrieve_asset_copy([
1129 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1130 or return $e->die_event;
1133 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1134 $copy->circ_lib : $copy->call_number->owning_lib;
1136 my $perm = 'MARK_ITEM_MISSING';
1137 my $stat = OILS_COPY_STATUS_MISSING;
1139 if( $self->api_name =~ /damaged/ ) {
1140 $perm = 'MARK_ITEM_DAMAGED';
1141 $stat = OILS_COPY_STATUS_DAMAGED;
1142 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1143 return $evt if $evt;
1145 } elsif ( $self->api_name =~ /bindery/ ) {
1146 $perm = 'MARK_ITEM_BINDERY';
1147 $stat = OILS_COPY_STATUS_BINDERY;
1148 } elsif ( $self->api_name =~ /on_order/ ) {
1149 $perm = 'MARK_ITEM_ON_ORDER';
1150 $stat = OILS_COPY_STATUS_ON_ORDER;
1151 } elsif ( $self->api_name =~ /ill/ ) {
1152 $perm = 'MARK_ITEM_ILL';
1153 $stat = OILS_COPY_STATUS_ILL;
1154 } elsif ( $self->api_name =~ /cataloging/ ) {
1155 $perm = 'MARK_ITEM_CATALOGING';
1156 $stat = OILS_COPY_STATUS_CATALOGING;
1157 } elsif ( $self->api_name =~ /reserves/ ) {
1158 $perm = 'MARK_ITEM_RESERVES';
1159 $stat = OILS_COPY_STATUS_RESERVES;
1160 } elsif ( $self->api_name =~ /discard/ ) {
1161 $perm = 'MARK_ITEM_DISCARD';
1162 $stat = OILS_COPY_STATUS_DISCARD;
1165 # caller may proceed if either perm is allowed
1166 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1168 $copy->status($stat);
1169 $copy->edit_date('now');
1170 $copy->editor($e->requestor->id);
1172 $e->update_asset_copy($copy) or return $e->die_event;
1174 my $holds = $e->search_action_hold_request(
1176 current_copy => $copy->id,
1177 fulfillment_time => undef,
1178 cancel_time => undef,
1184 if( $self->api_name =~ /damaged/ ) {
1185 # now that we've committed the changes, create related A/T events
1186 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1187 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1190 $logger->debug("resetting holds that target the marked copy");
1191 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1196 sub handle_mark_damaged {
1197 my($e, $copy, $owning_lib, $args) = @_;
1199 my $apply = $args->{apply_fines} || '';
1200 return undef if $apply eq 'noapply';
1202 my $new_amount = $args->{override_amount};
1203 my $new_btype = $args->{override_btype};
1204 my $new_note = $args->{override_note};
1206 # grab the last circulation
1207 my $circ = $e->search_action_circulation([
1208 { target_copy => $copy->id},
1210 order_by => {circ => "xact_start DESC"},
1212 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1216 return undef unless $circ;
1218 my $charge_price = $U->ou_ancestor_setting_value(
1219 $owning_lib, 'circ.charge_on_damaged', $e);
1221 my $proc_fee = $U->ou_ancestor_setting_value(
1222 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1224 my $void_overdue = $U->ou_ancestor_setting_value(
1225 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1227 return undef unless $charge_price or $proc_fee;
1229 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1230 my $total = $copy_price + $proc_fee;
1234 if($new_amount and $new_btype) {
1236 # Allow staff to override the amount to charge for a damaged item
1237 # Consider the case where the item is only partially damaged
1238 # This value is meant to take the place of the item price and
1239 # optional processing fee.
1241 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1242 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1243 return $evt if $evt;
1247 if($charge_price and $copy_price) {
1248 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1249 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1250 return $evt if $evt;
1254 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1255 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1256 return $evt if $evt;
1260 # the assumption is that you would not void the overdues unless you
1261 # were also charging for the item and/or applying a processing fee
1263 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1264 return $evt if $evt;
1267 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1268 return $evt if $evt;
1270 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1271 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1273 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1274 return $evt2 if $evt2;
1279 return OpenILS::Event->new('DAMAGE_CHARGE',
1290 # ----------------------------------------------------------------------
1291 __PACKAGE__->register_method(
1292 method => 'mark_item_missing_pieces',
1293 api_name => 'open-ils.circ.mark_item_missing_pieces',
1295 Changes the status of a copy to "damaged" or to a custom status based on the
1296 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1298 @param authtoken The login session key
1299 @param copy_id The ID of the copy to mark as damaged
1300 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1304 sub mark_item_missing_pieces {
1305 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1306 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1307 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1309 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1310 return $e2->die_event unless $e2->checkauth;
1313 my $copy = $e2->retrieve_asset_copy([
1315 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1316 or return $e2->die_event;
1319 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1320 $copy->circ_lib : $copy->call_number->owning_lib;
1322 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1324 #### grab the last circulation
1325 my $circ = $e2->search_action_circulation([
1326 { target_copy => $copy->id},
1328 order_by => {circ => "xact_start DESC"}
1333 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1335 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1338 my $holds = $e2->search_action_hold_request(
1340 current_copy => $copy->id,
1341 fulfillment_time => undef,
1342 cancel_time => undef,
1346 $logger->debug("resetting holds that target the marked copy");
1347 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1350 if (! $e2->commit) {
1351 return $e2->die_event;
1354 my $e = new_editor(authtoken=>$auth, xact =>1);
1355 return $e->die_event unless $e->checkauth;
1357 if (! $circ->checkin_time) { # if circ active, attempt renew
1358 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1359 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1360 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1361 $circ = $res->[0]->{payload}{'circ'};
1362 $circ->target_copy( $copy->id );
1363 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1365 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1370 'copy_id'=>$circ->target_copy,
1371 'patron_id'=>$circ->usr,
1372 'skip_deposit_fee'=>1,
1373 'skip_rental_fee'=>1
1376 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1378 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1379 $e, $copy, $e->requestor, 1 );
1381 if ($hold) { # needed for hold? then due now
1383 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1384 my $due_date = DateTime->now(time_zone => 'local');
1385 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1387 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1391 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1392 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1393 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1394 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1395 $circ = $res->[0]->{payload}{'circ'};
1397 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1403 ### Update the item status
1405 my $custom_stat = $U->ou_ancestor_setting_value(
1406 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1407 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1409 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1410 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1412 $copy->status($stat);
1413 $copy->edit_date('now');
1414 $copy->editor($e->requestor->id);
1416 $e->update_asset_copy($copy) or return $e->die_event;
1420 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1421 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1423 return OpenILS::Event->new('SUCCESS',
1427 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1428 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1433 return $e->die_event;
1441 # ----------------------------------------------------------------------
1442 __PACKAGE__->register_method(
1443 method => 'magic_fetch',
1444 api_name => 'open-ils.agent.fetch'
1447 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1450 my( $self, $conn, $auth, $args ) = @_;
1451 my $e = new_editor( authtoken => $auth );
1452 return $e->event unless $e->checkauth;
1454 my $hint = $$args{hint};
1455 my $id = $$args{id};
1457 # Is the call allowed to fetch this type of object?
1458 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1460 # Find the class the implements the given hint
1461 my ($class) = grep {
1462 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1464 $class =~ s/Fieldmapper:://og;
1465 $class =~ s/::/_/og;
1466 my $method = "retrieve_$class";
1468 my $obj = $e->$method($id) or return $e->event;
1471 # ----------------------------------------------------------------------
1474 __PACKAGE__->register_method(
1475 method => "fleshed_circ_retrieve",
1477 api_name => "open-ils.circ.fleshed.retrieve",);
1479 sub fleshed_circ_retrieve {
1480 my( $self, $client, $id ) = @_;
1481 my $e = new_editor();
1482 my $circ = $e->retrieve_action_circulation(
1488 circ => [ qw/ target_copy / ],
1489 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1490 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1491 acn => [ qw/ record / ],
1495 ) or return $e->event;
1497 my $copy = $circ->target_copy;
1498 my $vol = $copy->call_number;
1499 my $rec = $circ->target_copy->call_number->record;
1501 $vol->record($rec->id);
1502 $copy->call_number($vol->id);
1503 $circ->target_copy($copy->id);
1507 if( $rec->id == OILS_PRECAT_RECORD ) {
1511 $mvr = $U->record_to_mvr($rec);
1512 $rec->marc(''); # drop the bulky marc data
1526 __PACKAGE__->register_method(
1527 method => "test_batch_circ_events",
1528 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1531 # method for testing the behavior of a given event definition
1532 sub test_batch_circ_events {
1533 my($self, $conn, $auth, $event_def, $barcode) = @_;
1535 my $e = new_editor(authtoken => $auth);
1536 return $e->event unless $e->checkauth;
1537 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1539 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1540 or return $e->event;
1542 my $circ = $e->search_action_circulation(
1543 {target_copy => $copy->id, checkin_time => undef})->[0]
1544 or return $e->event;
1546 return undef unless $circ;
1548 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1552 __PACKAGE__->register_method(
1553 method => "fire_circ_events",
1554 api_name => "open-ils.circ.fire_circ_trigger_events",
1556 General event def runner for circ objects. If no event def ID
1557 is provided, the hook will be used to find the best event_def
1558 match based on the context org unit
1562 __PACKAGE__->register_method(
1563 method => "fire_circ_events",
1564 api_name => "open-ils.circ.fire_hold_trigger_events",
1566 General event def runner for hold objects. If no event def ID
1567 is provided, the hook will be used to find the best event_def
1568 match based on the context org unit
1572 __PACKAGE__->register_method(
1573 method => "fire_circ_events",
1574 api_name => "open-ils.circ.fire_user_trigger_events",
1576 General event def runner for user objects. If no event def ID
1577 is provided, the hook will be used to find the best event_def
1578 match based on the context org unit
1583 sub fire_circ_events {
1584 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1586 my $e = new_editor(authtoken => $auth, xact => 1);
1587 return $e->event unless $e->checkauth;
1591 if($self->api_name =~ /hold/) {
1592 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1593 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1594 } elsif($self->api_name =~ /user/) {
1595 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1596 $targets = $e->batch_retrieve_actor_user($target_ids);
1598 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1599 $targets = $e->batch_retrieve_action_circulation($target_ids);
1601 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1602 # simply making this method authoritative because of weirdness
1603 # with transaction handling in A/T code that causes rollback
1604 # failure down the line if handling many targets
1606 return undef unless @$targets;
1607 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1610 __PACKAGE__->register_method(
1611 method => "user_payments_list",
1612 api_name => "open-ils.circ.user_payments.filtered.batch",
1615 desc => q/Returns a fleshed, date-limited set of all payments a user
1616 has made. By default, ordered by payment date. Optionally
1617 ordered by other columns in the top-level "mp" object/,
1619 {desc => 'Authentication token', type => 'string'},
1620 {desc => 'User ID', type => 'number'},
1621 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1623 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1624 and the related fully-realized payment object (e.g money.cash_payment)/}
1628 sub user_payments_list {
1629 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1631 my $e = new_editor(authtoken => $auth);
1632 return $e->event unless $e->checkauth;
1634 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1635 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1637 $order_by ||= ['payment_ts'];
1639 # all payments by user, between start_date and end_date
1640 my $payments = $e->json_query({
1641 select => {mp => ['id']},
1645 fkey => 'xact', field => 'id'}
1649 '+mbt' => {usr => $user_id},
1650 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1652 order_by => {mp => $order_by}
1655 for my $payment_id (@$payments) {
1656 my $payment = $e->retrieve_money_payment([
1664 'credit_card_payment',
1679 $conn->respond($payment);
1686 __PACKAGE__->register_method(
1687 method => "retrieve_circ_chain",
1688 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1691 desc => q/Given a circulation, this returns all circulation objects
1692 that are part of the same chain of renewals./,
1694 {desc => 'Authentication token', type => 'string'},
1695 {desc => 'Circ ID', type => 'number'},
1697 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1701 __PACKAGE__->register_method(
1702 method => "retrieve_circ_chain",
1703 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1705 desc => q/Given a circulation, this returns a summary of the circulation objects
1706 that are part of the same chain of renewals./,
1708 {desc => 'Authentication token', type => 'string'},
1709 {desc => 'Circ ID', type => 'number'},
1711 return => {desc => q/Circulation Chain Summary/}
1715 sub retrieve_circ_chain {
1716 my($self, $conn, $auth, $circ_id) = @_;
1718 my $e = new_editor(authtoken => $auth);
1719 return $e->event unless $e->checkauth;
1720 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1722 if($self->api_name =~ /summary/) {
1723 return $U->create_circ_chain_summary($e, $circ_id);
1727 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1729 for my $circ_info (@$chain) {
1730 my $circ = Fieldmapper::action::circulation->new;
1731 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1732 $conn->respond($circ);
1739 __PACKAGE__->register_method(
1740 method => "retrieve_prev_circ_chain",
1741 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1744 desc => q/Given a circulation, this returns all circulation objects
1745 that are part of the previous chain of renewals./,
1747 {desc => 'Authentication token', type => 'string'},
1748 {desc => 'Circ ID', type => 'number'},
1750 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1754 __PACKAGE__->register_method(
1755 method => "retrieve_prev_circ_chain",
1756 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1758 desc => q/Given a circulation, this returns a summary of the circulation objects
1759 that are part of the previous chain of renewals./,
1761 {desc => 'Authentication token', type => 'string'},
1762 {desc => 'Circ ID', type => 'number'},
1764 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1768 sub retrieve_prev_circ_chain {
1769 my($self, $conn, $auth, $circ_id) = @_;
1771 my $e = new_editor(authtoken => $auth);
1772 return $e->event unless $e->checkauth;
1773 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1775 if($self->api_name =~ /summary/) {
1776 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1777 my $target_copy = $$first_circ{'target_copy'};
1778 my $usr = $$first_circ{'usr'};
1779 my $last_circ_from_prev_chain = $e->json_query({
1780 'select' => { 'circ' => ['id','usr'] },
1783 target_copy => $target_copy,
1784 xact_start => { '<' => $$first_circ{'xact_start'} }
1786 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1789 return undef unless $last_circ_from_prev_chain;
1790 return undef unless $$last_circ_from_prev_chain{'id'};
1791 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1792 return undef unless $sum;
1793 my $obj = Fieldmapper::action::circ_chain_summary->new;
1794 $obj->$_($sum->{$_}) for keys %$sum;
1795 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1799 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1800 my $target_copy = $$first_circ{'target_copy'};
1801 my $last_circ_from_prev_chain = $e->json_query({
1802 'select' => { 'circ' => ['id'] },
1805 target_copy => $target_copy,
1806 xact_start => { '<' => $$first_circ{'xact_start'} }
1808 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1811 return undef unless $last_circ_from_prev_chain;
1812 return undef unless $$last_circ_from_prev_chain{'id'};
1813 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1815 for my $circ_info (@$chain) {
1816 my $circ = Fieldmapper::action::circulation->new;
1817 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1818 $conn->respond($circ);
1826 __PACKAGE__->register_method(
1827 method => "get_copy_due_date",
1828 api_name => "open-ils.circ.copy.due_date.retrieve",
1831 Given a copy ID, returns the due date for the copy if it's
1832 currently circulating. Otherwise, returns null. Note, this is a public
1833 method requiring no authentication. Only the due date is exposed.
1836 {desc => 'Copy ID', type => 'number'}
1838 return => {desc => q/
1839 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1844 sub get_copy_due_date {
1845 my($self, $conn, $copy_id) = @_;
1846 my $e = new_editor();
1848 my $circ = $e->json_query({
1849 select => {circ => ['due_date']},
1852 target_copy => $copy_id,
1853 checkin_time => undef,
1855 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1856 {stop_fines => undef}
1860 })->[0] or return undef;
1862 return $circ->{due_date};
1869 # {"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}}