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 => 'fetch_bill_types',
84 api_name => 'open-ils.circ.billing_type.retrieve.all');
85 sub fetch_bill_types {
86 my $conf = OpenSRF::Utils::SettingsClient->new;
87 return $conf->config_value(
88 'apps', 'open-ils.circ', 'app_settings', 'billing_types', 'type' );
92 __PACKAGE__->register_method(
93 method => 'ranged_billing_types',
94 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
96 sub ranged_billing_types {
97 my($self, $conn, $auth, $org_id, $depth) = @_;
98 my $e = new_editor(authtoken => $auth);
99 return $e->event unless $e->checkauth;
100 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
101 return $e->search_config_billing_type(
102 {owner => $U->get_org_full_path($org_id, $depth)});
107 # ------------------------------------------------------------------------
108 # Returns an array of {circ, record} hashes checked out by the user.
109 # ------------------------------------------------------------------------
110 __PACKAGE__->register_method(
111 method => "checkouts_by_user",
112 api_name => "open-ils.circ.actor.user.checked_out",
114 NOTES => <<" NOTES");
115 Returns a list of open circulations as a pile of objects. Each object
116 contains the relevant copy, circ, and record
119 sub checkouts_by_user {
120 my($self, $client, $auth, $user_id) = @_;
122 my $e = new_editor(authtoken=>$auth);
123 return $e->event unless $e->checkauth;
125 my $circ_ids = $e->search_action_circulation(
127 checkin_time => undef,
129 {stop_fines => undef},
130 {stop_fines => ['MAXFINES','LONGOVERDUE']}
136 for my $id (@$circ_ids) {
137 my $circ = $e->retrieve_action_circulation([
141 circ => ['target_copy'],
142 acp => ['call_number'],
148 # un-flesh for consistency
149 my $c = $circ->target_copy;
150 $circ->target_copy($c->id);
152 my $cn = $c->call_number;
153 $c->call_number($cn->id);
161 record => $U->record_to_mvr($t)
171 __PACKAGE__->register_method(
172 method => "checkouts_by_user_slim",
173 api_name => "open-ils.circ.actor.user.checked_out.slim",
174 NOTES => <<" NOTES");
175 Returns a list of open circulation objects
179 sub checkouts_by_user_slim {
180 my( $self, $client, $user_session, $user_id ) = @_;
182 my( $requestor, $target, $copy, $record, $evt );
184 ( $requestor, $target, $evt ) =
185 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
188 $logger->debug( 'User ' . $requestor->id .
189 " retrieving checked out items for user " . $target->id );
191 # XXX Make the call correct..
192 return $apputils->simplereq(
194 "open-ils.cstore.direct.action.open_circulation.search.atomic",
195 { usr => $target->id, checkin_time => undef } );
196 # { usr => $target->id } );
200 __PACKAGE__->register_method(
201 method => "checkouts_by_user_opac",
202 api_name => "open-ils.circ.actor.user.checked_out.opac",);
205 sub checkouts_by_user_opac {
206 my( $self, $client, $auth, $user_id ) = @_;
208 my $e = OpenILS::Utils::Editor->new( authtoken => $auth );
209 return $e->event unless $e->checkauth;
210 $user_id ||= $e->requestor->id;
211 return $e->event unless
212 my $patron = $e->retrieve_actor_user($user_id);
215 my $search = {usr => $user_id, stop_fines => undef};
217 if( $user_id ne $e->requestor->id ) {
218 $data = $e->search_action_circulation(
219 $search, {checkperm=>1, permorg=>$patron->home_ou})
223 $data = $e->search_action_circulation($search);
230 __PACKAGE__->register_method(
231 method => "title_from_transaction",
232 api_name => "open-ils.circ.circ_transaction.find_title",
233 NOTES => <<" NOTES");
234 Returns a mods object for the title that is linked to from the
235 copy from the hold that created the given transaction
238 sub title_from_transaction {
239 my( $self, $client, $login_session, $transactionid ) = @_;
241 my( $user, $circ, $title, $evt );
243 ( $user, $evt ) = $apputils->checkses( $login_session );
246 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
249 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
252 return $apputils->record_to_mvr($title);
255 __PACKAGE__->register_method(
256 method => "staff_age_to_lost",
257 api_name => "open-ils.circ.circulation.age_to_lost",
260 This fires a circ.staff_age_to_lost Action-Trigger event against all
261 overdue circulations in scope of the specified context library and
262 user profile, which effectively marks the associated items as Lost.
263 This is likely to be done at the end of a semester in an academic
266 @param args : circ_lib, user_profile
270 sub staff_age_to_lost {
271 my( $self, $conn, $auth, $args ) = @_;
273 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
274 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
276 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
278 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
279 my $hook = 'circ.staff_age_to_lost';
280 my $context_org = 'circ_lib';
281 my $opt_granularity = undef;
283 "checkin_time" => undef,
284 "due_date" => { "<" => "now" },
286 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
287 { "stop_fines" => undef }
291 "select" => {"au" => ["id"]},
294 "profile" => $profiles,
295 "id" => { "=" => {"+circ" => "usr"} }
299 "select" => {"aou" => ["id"]},
303 {"id" => { "=" => {"+circ" => "circ_lib"} }},
310 my $req_timeout = 10800;
311 my $chunk_size = 100;
314 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
315 my @event_ids; my @chunked_ids;
316 while (my $resp = $req->recv(timeout => $req_timeout)) {
317 push(@event_ids, $resp->content);
318 push(@chunked_ids, $resp->content);
319 if (scalar(@chunked_ids) > $chunk_size) {
320 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
324 if (scalar(@chunked_ids) > 0) {
325 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
329 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
330 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
331 } elsif($req->complete) {
332 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
333 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
335 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
336 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
343 __PACKAGE__->register_method(
344 method => "new_set_circ_lost",
345 api_name => "open-ils.circ.circulation.set_lost",
347 Sets the copy and related open circulation to lost
349 @param args : barcode
354 # ---------------------------------------------------------------------
355 # Sets a circulation to lost. updates copy status to lost
356 # applies copy and/or prcoessing fees depending on org settings
357 # ---------------------------------------------------------------------
358 sub new_set_circ_lost {
359 my( $self, $conn, $auth, $args ) = @_;
361 my $e = new_editor(authtoken=>$auth, xact=>1);
362 return $e->die_event unless $e->checkauth;
364 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
365 or return $e->die_event;
367 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
375 __PACKAGE__->register_method(
376 method => "set_circ_claims_returned",
377 api_name => "open-ils.circ.circulation.set_claims_returned",
379 desc => q/Sets the circ for a given item as claims returned
380 If a backdate is provided, overdue fines will be voided
381 back to the backdate/,
383 {desc => 'Authentication token', type => 'string'},
384 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
386 return => {desc => q/1 on success, failure event on error, and
387 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
388 configured claims return maximum/}
392 __PACKAGE__->register_method(
393 method => "set_circ_claims_returned",
394 api_name => "open-ils.circ.circulation.set_claims_returned.override",
396 desc => q/This adds support for overrideing the configured max
397 claims returned amount.
398 @see open-ils.circ.circulation.set_claims_returned./,
402 sub set_circ_claims_returned {
403 my( $self, $conn, $auth, $args, $oargs ) = @_;
405 my $e = new_editor(authtoken=>$auth, xact=>1);
406 return $e->die_event unless $e->checkauth;
408 $oargs = { all => 1 } unless defined $oargs;
410 my $barcode = $$args{barcode};
411 my $backdate = $$args{backdate};
413 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
414 or return $e->die_event;
416 my $circ = $e->search_action_circulation(
417 {checkin_time => undef, target_copy => $copy->id})->[0]
418 or return $e->die_event;
420 $backdate = $circ->due_date if $$args{use_due_date};
422 $logger->info("marking circ for item $barcode as claims returned".
423 (($backdate) ? " with backdate $backdate" : ''));
425 my $patron = $e->retrieve_actor_user($circ->usr);
426 my $max_count = $U->ou_ancestor_setting_value(
427 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
429 # If the patron has too instances of many claims returned,
430 # require an override to continue. A configured max of
431 # 0 means all attempts require an override
432 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
434 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
436 # see if we're allowed to override
437 return $e->die_event unless
438 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
442 # exit early and return the max claims return event
444 return OpenILS::Event->new(
445 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
447 patron_count => $patron->claims_returned_count,
448 max_count => $max_count
454 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
455 or return $e->die_event;
457 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
458 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
461 $backdate = cleanse_ISO8601($backdate);
463 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
464 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
465 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
467 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
468 $backdate = cleanse_ISO8601($backdate);
470 # make it look like the circ stopped at the cliams returned time
471 $circ->stop_fines_time($backdate);
472 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
476 $e->update_action_circulation($circ) or return $e->die_event;
478 # see if there is a configured post-claims-return copy status
479 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
480 $copy->status($stat);
481 $copy->edit_date('now');
482 $copy->editor($e->requestor->id);
483 $e->update_asset_copy($copy) or return $e->die_event;
491 __PACKAGE__->register_method(
492 method => "post_checkin_backdate_circ",
493 api_name => "open-ils.circ.post_checkin_backdate",
495 desc => q/Back-date an already checked in circulation/,
497 {desc => 'Authentication token', type => 'string'},
498 {desc => 'Circ ID', type => 'number'},
499 {desc => 'ISO8601 backdate', type => 'string'},
501 return => {desc => q/1 on success, failure event on error/}
505 __PACKAGE__->register_method(
506 method => "post_checkin_backdate_circ",
507 api_name => "open-ils.circ.post_checkin_backdate.batch",
510 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
512 {desc => 'Authentication token', type => 'string'},
513 {desc => 'List of Circ ID', type => 'array'},
514 {desc => 'ISO8601 backdate', type => 'string'},
516 return => {desc => q/Set of: 1 on success, failure event on error/}
521 sub post_checkin_backdate_circ {
522 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
523 my $e = new_editor(authtoken=>$auth);
524 return $e->die_event unless $e->checkauth;
525 if($self->api_name =~ /batch/) {
526 foreach my $c (@$circ_id) {
527 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
530 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
538 sub post_checkin_backdate_circ_impl {
539 my($e, $circ_id, $backdate) = @_;
543 my $circ = $e->retrieve_action_circulation($circ_id)
544 or return $e->die_event;
546 # anyone with checkin perms can backdate (more restrictive?)
547 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
549 # don't allow back-dating an open circulation
550 return OpenILS::Event->new('BAD_PARAMS') unless
551 $backdate and $circ->checkin_time;
553 # update the checkin and stop_fines times to reflect the new backdate
554 $circ->stop_fines_time(cleanse_ISO8601($backdate));
555 $circ->checkin_time(cleanse_ISO8601($backdate));
556 $e->update_action_circulation($circ) or return $e->die_event;
558 # now void the overdues "erased" by the back-dating
559 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
562 # If the circ was closed before and the balance owned !=0, re-open the transaction
563 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
572 __PACKAGE__->register_method (
573 method => 'set_circ_due_date',
574 api_name => 'open-ils.circ.circulation.due_date.update',
576 Updates the due_date on the given circ
578 @param circid The id of the circ to update
579 @param date The timestamp of the new due date
583 sub set_circ_due_date {
584 my( $self, $conn, $auth, $circ_id, $date ) = @_;
586 my $e = new_editor(xact=>1, authtoken=>$auth);
587 return $e->die_event unless $e->checkauth;
588 my $circ = $e->retrieve_action_circulation($circ_id)
589 or return $e->die_event;
591 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
592 $date = cleanse_ISO8601($date);
594 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
595 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
596 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
597 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
600 $circ->due_date($date);
601 $e->update_action_circulation($circ) or return $e->die_event;
608 __PACKAGE__->register_method(
609 method => "create_in_house_use",
610 api_name => 'open-ils.circ.in_house_use.create',
612 Creates an in-house use action.
613 @param $authtoken The login session key
614 @param params A hash of params including
615 'location' The org unit id where the in-house use occurs
616 'copyid' The copy in question
617 'count' The number of in-house uses to apply to this copy
618 @return An array of id's representing the id's of the newly created
619 in-house use objects or an event on an error
622 __PACKAGE__->register_method(
623 method => "create_in_house_use",
624 api_name => 'open-ils.circ.non_cat_in_house_use.create',
628 sub create_in_house_use {
629 my( $self, $client, $auth, $params ) = @_;
632 my $org = $params->{location};
633 my $copyid = $params->{copyid};
634 my $count = $params->{count} || 1;
635 my $nc_type = $params->{non_cat_type};
636 my $use_time = $params->{use_time} || 'now';
638 my $e = new_editor(xact=>1,authtoken=>$auth);
639 return $e->event unless $e->checkauth;
640 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
642 my $non_cat = 1 if $self->api_name =~ /non_cat/;
646 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
648 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
654 if( $use_time ne 'now' ) {
655 $use_time = cleanse_ISO8601($use_time);
656 $logger->debug("in_house_use setting use time to $use_time");
667 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
668 $ihu->item_type($nc_type);
669 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
670 $cmeth = "create_action_non_cat_in_house_use";
673 $ihu = Fieldmapper::action::in_house_use->new;
675 $method = 'open-ils.storage.direct.action.in_house_use.create';
676 $cmeth = "create_action_in_house_use";
679 $ihu->staff($e->requestor->id);
680 $ihu->org_unit($org);
681 $ihu->use_time($use_time);
683 $ihu = $e->$cmeth($ihu) or return $e->event;
684 push( @ids, $ihu->id );
695 __PACKAGE__->register_method(
696 method => "view_circs",
697 api_name => "open-ils.circ.copy_checkout_history.retrieve",
699 Retrieves the last X circs for a given copy
700 @param authtoken The login session key
701 @param copyid The copy to check
702 @param count How far to go back in the item history
703 @return An array of circ ids
706 # ----------------------------------------------------------------------
707 # Returns $count most recent circs. If count exceeds the configured
708 # max, use the configured max instead
709 # ----------------------------------------------------------------------
711 my( $self, $client, $authtoken, $copyid, $count ) = @_;
713 my $e = new_editor(authtoken => $authtoken);
714 return $e->event unless $e->checkauth;
716 my $copy = $e->retrieve_asset_copy([
719 flesh_fields => {acp => ['call_number']}
721 ]) or return $e->event;
723 return $e->event unless $e->allowed(
724 'VIEW_COPY_CHECKOUT_HISTORY',
725 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
726 $copy->circ_lib : $copy->call_number->owning_lib);
728 my $max_history = $U->ou_ancestor_setting_value(
729 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
731 if(defined $max_history) {
732 $count = $max_history unless defined $count and $count < $max_history;
734 $count = 4 unless defined $count;
737 return $e->search_action_circulation([
738 {target_copy => $copyid},
739 {limit => $count, order_by => { circ => "xact_start DESC" }}
744 __PACKAGE__->register_method(
745 method => "circ_count",
746 api_name => "open-ils.circ.circulation.count",
748 Returns the number of times the item has circulated
749 @param copyid The copy to check
753 my( $self, $client, $copyid, $range ) = @_;
754 my $e = OpenILS::Utils::Editor->new;
755 return $e->request('open-ils.storage.asset.copy.circ_count', $copyid, $range);
760 __PACKAGE__->register_method(
761 method => 'fetch_notes',
763 api_name => 'open-ils.circ.copy_note.retrieve.all',
765 Returns an array of copy note objects.
766 @param args A named hash of parameters including:
767 authtoken : Required if viewing non-public notes
768 itemid : The id of the item whose notes we want to retrieve
769 pub : True if all the caller wants are public notes
770 @return An array of note objects
773 __PACKAGE__->register_method(
774 method => 'fetch_notes',
775 api_name => 'open-ils.circ.call_number_note.retrieve.all',
776 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
778 __PACKAGE__->register_method(
779 method => 'fetch_notes',
780 api_name => 'open-ils.circ.title_note.retrieve.all',
781 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
784 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
786 my( $self, $connection, $args ) = @_;
788 my $id = $$args{itemid};
789 my $authtoken = $$args{authtoken};
792 if( $self->api_name =~ /copy/ ) {
794 return $U->cstorereq(
795 'open-ils.cstore.direct.asset.copy_note.search.atomic',
796 { owning_copy => $id, pub => 't' } );
798 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
800 return $U->cstorereq(
801 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
804 } elsif( $self->api_name =~ /call_number/ ) {
806 return $U->cstorereq(
807 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
808 { call_number => $id, pub => 't' } );
810 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
812 return $U->cstorereq(
813 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
816 } elsif( $self->api_name =~ /title/ ) {
818 return $U->cstorereq(
819 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
820 { record => $id, pub => 't' } );
822 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
824 return $U->cstorereq(
825 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
832 __PACKAGE__->register_method(
833 method => 'has_notes',
834 api_name => 'open-ils.circ.copy.has_notes');
835 __PACKAGE__->register_method(
836 method => 'has_notes',
837 api_name => 'open-ils.circ.call_number.has_notes');
838 __PACKAGE__->register_method(
839 method => 'has_notes',
840 api_name => 'open-ils.circ.title.has_notes');
844 my( $self, $conn, $authtoken, $id ) = @_;
845 my $editor = OpenILS::Utils::Editor->new(authtoken => $authtoken);
846 return $editor->event unless $editor->checkauth;
848 my $n = $editor->search_asset_copy_note(
849 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
851 $n = $editor->search_asset_call_number_note(
852 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
854 $n = $editor->search_biblio_record_note(
855 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
862 __PACKAGE__->register_method(
863 method => 'create_copy_note',
864 api_name => 'open-ils.circ.copy_note.create',
866 Creates a new copy note
867 @param authtoken The login session key
868 @param note The note object to create
869 @return The id of the new note object
872 sub create_copy_note {
873 my( $self, $connection, $authtoken, $note ) = @_;
875 my $e = new_editor(xact=>1, authtoken=>$authtoken);
876 return $e->event unless $e->checkauth;
877 my $copy = $e->retrieve_asset_copy(
881 flesh_fields => { 'acp' => ['call_number'] }
886 return $e->event unless
887 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
889 $note->create_date('now');
890 $note->creator($e->requestor->id);
891 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
894 $e->create_asset_copy_note($note) or return $e->event;
900 __PACKAGE__->register_method(
901 method => 'delete_copy_note',
902 api_name => 'open-ils.circ.copy_note.delete',
904 Deletes an existing copy note
905 @param authtoken The login session key
906 @param noteid The id of the note to delete
907 @return 1 on success - Event otherwise.
909 sub delete_copy_note {
910 my( $self, $conn, $authtoken, $noteid ) = @_;
912 my $e = new_editor(xact=>1, authtoken=>$authtoken);
913 return $e->die_event unless $e->checkauth;
915 my $note = $e->retrieve_asset_copy_note([
919 'acpn' => [ 'owning_copy' ],
920 'acp' => [ 'call_number' ],
923 ]) or return $e->die_event;
925 if( $note->creator ne $e->requestor->id ) {
926 return $e->die_event unless
927 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
930 $e->delete_asset_copy_note($note) or return $e->die_event;
936 __PACKAGE__->register_method(
937 method => 'age_hold_rules',
938 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
942 my( $self, $conn ) = @_;
943 return new_editor()->retrieve_all_config_rules_age_hold_protect();
948 __PACKAGE__->register_method(
949 method => 'copy_details_barcode',
951 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
952 sub copy_details_barcode {
953 my( $self, $conn, $auth, $barcode ) = @_;
954 my $e = new_editor();
955 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
956 return $e->event unless $cid;
957 return copy_details( $self, $conn, $auth, $cid );
961 __PACKAGE__->register_method(
962 method => 'copy_details',
963 api_name => 'open-ils.circ.copy_details.retrieve');
966 my( $self, $conn, $auth, $copy_id ) = @_;
967 my $e = new_editor(authtoken=>$auth);
968 return $e->event unless $e->checkauth;
970 my $flesh = { flesh => 1 };
972 my $copy = $e->retrieve_asset_copy(
978 acp => ['call_number','parts','peer_record_maps'],
979 acn => ['record','prefix','suffix','label_class']
982 ]) or return $e->event;
985 # De-flesh the copy for backwards compatibility
987 my $vol = $copy->call_number;
989 $copy->call_number($vol->id);
990 my $record = $vol->record;
992 $vol->record($record->id);
993 $mvr = $U->record_to_mvr($record);
998 my $hold = $e->search_action_hold_request(
1000 current_copy => $copy_id,
1001 capture_time => { "!=" => undef },
1002 fulfillment_time => undef,
1003 cancel_time => undef,
1007 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1009 my $transit = $e->search_action_transit_copy(
1010 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1012 # find the latest circ, open or closed
1013 my $circ = $e->search_action_circulation(
1015 { target_copy => $copy_id },
1021 'checkin_workstation',
1024 'recurring_fine_rule'
1027 order_by => { circ => 'xact_start desc' },
1037 transit => $transit,
1047 __PACKAGE__->register_method(
1048 method => 'mark_item',
1049 api_name => 'open-ils.circ.mark_item_damaged',
1051 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1052 @param authtoken The login session key
1053 @param copy_id The ID of the copy to mark as damaged
1054 @return 1 on success - Event otherwise.
1057 __PACKAGE__->register_method(
1058 method => 'mark_item',
1059 api_name => 'open-ils.circ.mark_item_missing',
1061 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1062 @param authtoken The login session key
1063 @param copy_id The ID of the copy to mark as missing
1064 @return 1 on success - Event otherwise.
1067 __PACKAGE__->register_method(
1068 method => 'mark_item',
1069 api_name => 'open-ils.circ.mark_item_bindery',
1071 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1072 @param authtoken The login session key
1073 @param copy_id The ID of the copy to mark as bindery
1074 @return 1 on success - Event otherwise.
1077 __PACKAGE__->register_method(
1078 method => 'mark_item',
1079 api_name => 'open-ils.circ.mark_item_on_order',
1081 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1082 @param authtoken The login session key
1083 @param copy_id The ID of the copy to mark as on order
1084 @return 1 on success - Event otherwise.
1087 __PACKAGE__->register_method(
1088 method => 'mark_item',
1089 api_name => 'open-ils.circ.mark_item_ill',
1091 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1092 @param authtoken The login session key
1093 @param copy_id The ID of the copy to mark as inter-library loan
1094 @return 1 on success - Event otherwise.
1097 __PACKAGE__->register_method(
1098 method => 'mark_item',
1099 api_name => 'open-ils.circ.mark_item_cataloging',
1101 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1102 @param authtoken The login session key
1103 @param copy_id The ID of the copy to mark as cataloging
1104 @return 1 on success - Event otherwise.
1107 __PACKAGE__->register_method(
1108 method => 'mark_item',
1109 api_name => 'open-ils.circ.mark_item_reserves',
1111 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1112 @param authtoken The login session key
1113 @param copy_id The ID of the copy to mark as reserves
1114 @return 1 on success - Event otherwise.
1117 __PACKAGE__->register_method(
1118 method => 'mark_item',
1119 api_name => 'open-ils.circ.mark_item_discard',
1121 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1122 @param authtoken The login session key
1123 @param copy_id The ID of the copy to mark as discard
1124 @return 1 on success - Event otherwise.
1129 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1130 my $e = new_editor(authtoken=>$auth, xact =>1);
1131 return $e->die_event unless $e->checkauth;
1134 my $copy = $e->retrieve_asset_copy([
1136 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1137 or return $e->die_event;
1140 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1141 $copy->circ_lib : $copy->call_number->owning_lib;
1143 my $perm = 'MARK_ITEM_MISSING';
1144 my $stat = OILS_COPY_STATUS_MISSING;
1146 if( $self->api_name =~ /damaged/ ) {
1147 $perm = 'MARK_ITEM_DAMAGED';
1148 $stat = OILS_COPY_STATUS_DAMAGED;
1149 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1150 return $evt if $evt;
1152 } elsif ( $self->api_name =~ /bindery/ ) {
1153 $perm = 'MARK_ITEM_BINDERY';
1154 $stat = OILS_COPY_STATUS_BINDERY;
1155 } elsif ( $self->api_name =~ /on_order/ ) {
1156 $perm = 'MARK_ITEM_ON_ORDER';
1157 $stat = OILS_COPY_STATUS_ON_ORDER;
1158 } elsif ( $self->api_name =~ /ill/ ) {
1159 $perm = 'MARK_ITEM_ILL';
1160 $stat = OILS_COPY_STATUS_ILL;
1161 } elsif ( $self->api_name =~ /cataloging/ ) {
1162 $perm = 'MARK_ITEM_CATALOGING';
1163 $stat = OILS_COPY_STATUS_CATALOGING;
1164 } elsif ( $self->api_name =~ /reserves/ ) {
1165 $perm = 'MARK_ITEM_RESERVES';
1166 $stat = OILS_COPY_STATUS_RESERVES;
1167 } elsif ( $self->api_name =~ /discard/ ) {
1168 $perm = 'MARK_ITEM_DISCARD';
1169 $stat = OILS_COPY_STATUS_DISCARD;
1172 # caller may proceed if either perm is allowed
1173 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1175 $copy->status($stat);
1176 $copy->edit_date('now');
1177 $copy->editor($e->requestor->id);
1179 $e->update_asset_copy($copy) or return $e->die_event;
1181 my $holds = $e->search_action_hold_request(
1183 current_copy => $copy->id,
1184 fulfillment_time => undef,
1185 cancel_time => undef,
1191 if( $self->api_name =~ /damaged/ ) {
1192 # now that we've committed the changes, create related A/T events
1193 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1194 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1197 $logger->debug("resetting holds that target the marked copy");
1198 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1203 sub handle_mark_damaged {
1204 my($e, $copy, $owning_lib, $args) = @_;
1206 my $apply = $args->{apply_fines} || '';
1207 return undef if $apply eq 'noapply';
1209 my $new_amount = $args->{override_amount};
1210 my $new_btype = $args->{override_btype};
1211 my $new_note = $args->{override_note};
1213 # grab the last circulation
1214 my $circ = $e->search_action_circulation([
1215 { target_copy => $copy->id},
1217 order_by => {circ => "xact_start DESC"},
1219 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1223 return undef unless $circ;
1225 my $charge_price = $U->ou_ancestor_setting_value(
1226 $owning_lib, 'circ.charge_on_damaged', $e);
1228 my $proc_fee = $U->ou_ancestor_setting_value(
1229 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1231 my $void_overdue = $U->ou_ancestor_setting_value(
1232 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1234 return undef unless $charge_price or $proc_fee;
1236 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1237 my $total = $copy_price + $proc_fee;
1241 if($new_amount and $new_btype) {
1243 # Allow staff to override the amount to charge for a damaged item
1244 # Consider the case where the item is only partially damaged
1245 # This value is meant to take the place of the item price and
1246 # optional processing fee.
1248 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1249 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1250 return $evt if $evt;
1254 if($charge_price and $copy_price) {
1255 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1256 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1257 return $evt if $evt;
1261 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1262 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1263 return $evt if $evt;
1267 # the assumption is that you would not void the overdues unless you
1268 # were also charging for the item and/or applying a processing fee
1270 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1271 return $evt if $evt;
1274 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1275 return $evt if $evt;
1277 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1278 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1280 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1281 return $evt2 if $evt2;
1286 return OpenILS::Event->new('DAMAGE_CHARGE',
1297 # ----------------------------------------------------------------------
1298 __PACKAGE__->register_method(
1299 method => 'mark_item_missing_pieces',
1300 api_name => 'open-ils.circ.mark_item_missing_pieces',
1302 Changes the status of a copy to "damaged" or to a custom status based on the
1303 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1305 @param authtoken The login session key
1306 @param copy_id The ID of the copy to mark as damaged
1307 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1311 sub mark_item_missing_pieces {
1312 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1313 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1314 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1316 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1317 return $e2->die_event unless $e2->checkauth;
1320 my $copy = $e2->retrieve_asset_copy([
1322 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1323 or return $e2->die_event;
1326 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1327 $copy->circ_lib : $copy->call_number->owning_lib;
1329 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1331 #### grab the last circulation
1332 my $circ = $e2->search_action_circulation([
1333 { target_copy => $copy->id},
1335 order_by => {circ => "xact_start DESC"}
1340 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1342 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1345 my $holds = $e2->search_action_hold_request(
1347 current_copy => $copy->id,
1348 fulfillment_time => undef,
1349 cancel_time => undef,
1353 $logger->debug("resetting holds that target the marked copy");
1354 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1357 if (! $e2->commit) {
1358 return $e2->die_event;
1361 my $e = new_editor(authtoken=>$auth, xact =>1);
1362 return $e->die_event unless $e->checkauth;
1364 if (! $circ->checkin_time) { # if circ active, attempt renew
1365 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1366 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1367 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1368 $circ = $res->[0]->{payload}{'circ'};
1369 $circ->target_copy( $copy->id );
1370 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1372 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1377 'copy_id'=>$circ->target_copy,
1378 'patron_id'=>$circ->usr,
1379 'skip_deposit_fee'=>1,
1380 'skip_rental_fee'=>1
1383 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1385 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1386 $e, $copy, $e->requestor, 1 );
1388 if ($hold) { # needed for hold? then due now
1390 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1391 my $due_date = DateTime->now(time_zone => 'local');
1392 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1394 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1398 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1399 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1400 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1401 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1402 $circ = $res->[0]->{payload}{'circ'};
1404 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1410 ### Update the item status
1412 my $custom_stat = $U->ou_ancestor_setting_value(
1413 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1414 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1416 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1417 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1419 $copy->status($stat);
1420 $copy->edit_date('now');
1421 $copy->editor($e->requestor->id);
1423 $e->update_asset_copy($copy) or return $e->die_event;
1427 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1428 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1430 return OpenILS::Event->new('SUCCESS',
1434 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1435 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1440 return $e->die_event;
1448 # ----------------------------------------------------------------------
1449 __PACKAGE__->register_method(
1450 method => 'magic_fetch',
1451 api_name => 'open-ils.agent.fetch'
1454 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1457 my( $self, $conn, $auth, $args ) = @_;
1458 my $e = new_editor( authtoken => $auth );
1459 return $e->event unless $e->checkauth;
1461 my $hint = $$args{hint};
1462 my $id = $$args{id};
1464 # Is the call allowed to fetch this type of object?
1465 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1467 # Find the class the implements the given hint
1468 my ($class) = grep {
1469 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1471 $class =~ s/Fieldmapper:://og;
1472 $class =~ s/::/_/og;
1473 my $method = "retrieve_$class";
1475 my $obj = $e->$method($id) or return $e->event;
1478 # ----------------------------------------------------------------------
1481 __PACKAGE__->register_method(
1482 method => "fleshed_circ_retrieve",
1484 api_name => "open-ils.circ.fleshed.retrieve",);
1486 sub fleshed_circ_retrieve {
1487 my( $self, $client, $id ) = @_;
1488 my $e = new_editor();
1489 my $circ = $e->retrieve_action_circulation(
1495 circ => [ qw/ target_copy / ],
1496 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1497 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1498 acn => [ qw/ record / ],
1502 ) or return $e->event;
1504 my $copy = $circ->target_copy;
1505 my $vol = $copy->call_number;
1506 my $rec = $circ->target_copy->call_number->record;
1508 $vol->record($rec->id);
1509 $copy->call_number($vol->id);
1510 $circ->target_copy($copy->id);
1514 if( $rec->id == OILS_PRECAT_RECORD ) {
1518 $mvr = $U->record_to_mvr($rec);
1519 $rec->marc(''); # drop the bulky marc data
1533 __PACKAGE__->register_method(
1534 method => "test_batch_circ_events",
1535 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1538 # method for testing the behavior of a given event definition
1539 sub test_batch_circ_events {
1540 my($self, $conn, $auth, $event_def, $barcode) = @_;
1542 my $e = new_editor(authtoken => $auth);
1543 return $e->event unless $e->checkauth;
1544 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1546 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1547 or return $e->event;
1549 my $circ = $e->search_action_circulation(
1550 {target_copy => $copy->id, checkin_time => undef})->[0]
1551 or return $e->event;
1553 return undef unless $circ;
1555 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1559 __PACKAGE__->register_method(
1560 method => "fire_circ_events",
1561 api_name => "open-ils.circ.fire_circ_trigger_events",
1563 General event def runner for circ objects. If no event def ID
1564 is provided, the hook will be used to find the best event_def
1565 match based on the context org unit
1569 __PACKAGE__->register_method(
1570 method => "fire_circ_events",
1571 api_name => "open-ils.circ.fire_hold_trigger_events",
1573 General event def runner for hold objects. If no event def ID
1574 is provided, the hook will be used to find the best event_def
1575 match based on the context org unit
1579 __PACKAGE__->register_method(
1580 method => "fire_circ_events",
1581 api_name => "open-ils.circ.fire_user_trigger_events",
1583 General event def runner for user objects. If no event def ID
1584 is provided, the hook will be used to find the best event_def
1585 match based on the context org unit
1590 sub fire_circ_events {
1591 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1593 my $e = new_editor(authtoken => $auth, xact => 1);
1594 return $e->event unless $e->checkauth;
1598 if($self->api_name =~ /hold/) {
1599 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1600 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1601 } elsif($self->api_name =~ /user/) {
1602 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1603 $targets = $e->batch_retrieve_actor_user($target_ids);
1605 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1606 $targets = $e->batch_retrieve_action_circulation($target_ids);
1608 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1609 # simply making this method authoritative because of weirdness
1610 # with transaction handling in A/T code that causes rollback
1611 # failure down the line if handling many targets
1613 return undef unless @$targets;
1614 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1617 __PACKAGE__->register_method(
1618 method => "user_payments_list",
1619 api_name => "open-ils.circ.user_payments.filtered.batch",
1622 desc => q/Returns a fleshed, date-limited set of all payments a user
1623 has made. By default, ordered by payment date. Optionally
1624 ordered by other columns in the top-level "mp" object/,
1626 {desc => 'Authentication token', type => 'string'},
1627 {desc => 'User ID', type => 'number'},
1628 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1630 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1631 and the related fully-realized payment object (e.g money.cash_payment)/}
1635 sub user_payments_list {
1636 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1638 my $e = new_editor(authtoken => $auth);
1639 return $e->event unless $e->checkauth;
1641 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1642 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1644 $order_by ||= ['payment_ts'];
1646 # all payments by user, between start_date and end_date
1647 my $payments = $e->json_query({
1648 select => {mp => ['id']},
1652 fkey => 'xact', field => 'id'}
1656 '+mbt' => {usr => $user_id},
1657 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1659 order_by => {mp => $order_by}
1662 for my $payment_id (@$payments) {
1663 my $payment = $e->retrieve_money_payment([
1671 'credit_card_payment',
1686 $conn->respond($payment);
1693 __PACKAGE__->register_method(
1694 method => "retrieve_circ_chain",
1695 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1698 desc => q/Given a circulation, this returns all circulation objects
1699 that are part of the same chain of renewals./,
1701 {desc => 'Authentication token', type => 'string'},
1702 {desc => 'Circ ID', type => 'number'},
1704 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1708 __PACKAGE__->register_method(
1709 method => "retrieve_circ_chain",
1710 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1712 desc => q/Given a circulation, this returns a summary of the circulation objects
1713 that are part of the same chain of renewals./,
1715 {desc => 'Authentication token', type => 'string'},
1716 {desc => 'Circ ID', type => 'number'},
1718 return => {desc => q/Circulation Chain Summary/}
1722 sub retrieve_circ_chain {
1723 my($self, $conn, $auth, $circ_id) = @_;
1725 my $e = new_editor(authtoken => $auth);
1726 return $e->event unless $e->checkauth;
1727 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1729 if($self->api_name =~ /summary/) {
1730 return $U->create_circ_chain_summary($e, $circ_id);
1734 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1736 for my $circ_info (@$chain) {
1737 my $circ = Fieldmapper::action::circulation->new;
1738 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1739 $conn->respond($circ);
1746 __PACKAGE__->register_method(
1747 method => "retrieve_prev_circ_chain",
1748 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1751 desc => q/Given a circulation, this returns all circulation objects
1752 that are part of the previous chain of renewals./,
1754 {desc => 'Authentication token', type => 'string'},
1755 {desc => 'Circ ID', type => 'number'},
1757 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1761 __PACKAGE__->register_method(
1762 method => "retrieve_prev_circ_chain",
1763 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1765 desc => q/Given a circulation, this returns a summary of the circulation objects
1766 that are part of the previous chain of renewals./,
1768 {desc => 'Authentication token', type => 'string'},
1769 {desc => 'Circ ID', type => 'number'},
1771 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1775 sub retrieve_prev_circ_chain {
1776 my($self, $conn, $auth, $circ_id) = @_;
1778 my $e = new_editor(authtoken => $auth);
1779 return $e->event unless $e->checkauth;
1780 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1782 if($self->api_name =~ /summary/) {
1783 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1784 my $target_copy = $$first_circ{'target_copy'};
1785 my $usr = $$first_circ{'usr'};
1786 my $last_circ_from_prev_chain = $e->json_query({
1787 'select' => { 'circ' => ['id','usr'] },
1790 target_copy => $target_copy,
1791 xact_start => { '<' => $$first_circ{'xact_start'} }
1793 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1796 return undef unless $last_circ_from_prev_chain;
1797 return undef unless $$last_circ_from_prev_chain{'id'};
1798 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1799 return undef unless $sum;
1800 my $obj = Fieldmapper::action::circ_chain_summary->new;
1801 $obj->$_($sum->{$_}) for keys %$sum;
1802 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1806 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1807 my $target_copy = $$first_circ{'target_copy'};
1808 my $last_circ_from_prev_chain = $e->json_query({
1809 'select' => { 'circ' => ['id'] },
1812 target_copy => $target_copy,
1813 xact_start => { '<' => $$first_circ{'xact_start'} }
1815 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1818 return undef unless $last_circ_from_prev_chain;
1819 return undef unless $$last_circ_from_prev_chain{'id'};
1820 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1822 for my $circ_info (@$chain) {
1823 my $circ = Fieldmapper::action::circulation->new;
1824 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1825 $conn->respond($circ);
1833 __PACKAGE__->register_method(
1834 method => "get_copy_due_date",
1835 api_name => "open-ils.circ.copy.due_date.retrieve",
1838 Given a copy ID, returns the due date for the copy if it's
1839 currently circulating. Otherwise, returns null. Note, this is a public
1840 method requiring no authentication. Only the due date is exposed.
1843 {desc => 'Copy ID', type => 'number'}
1845 return => {desc => q/
1846 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1851 sub get_copy_due_date {
1852 my($self, $conn, $copy_id) = @_;
1853 my $e = new_editor();
1855 my $circ = $e->json_query({
1856 select => {circ => ['due_date']},
1859 target_copy => $copy_id,
1860 checkin_time => undef,
1862 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1863 {stop_fines => undef}
1867 })->[0] or return undef;
1869 return $circ->{due_date};
1876 # {"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}}