1 package OpenILS::Application::Circ;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
6 use OpenILS::Application::Circ::Circulate;
7 use OpenILS::Application::Circ::Survey;
8 use OpenILS::Application::Circ::StatCat;
9 use OpenILS::Application::Circ::Holds;
10 use OpenILS::Application::Circ::HoldNotify;
11 use OpenILS::Application::Circ::CreditCard;
12 use OpenILS::Application::Circ::Money;
13 use OpenILS::Application::Circ::NonCat;
14 use OpenILS::Application::Circ::CopyLocations;
15 use OpenILS::Application::Circ::CircCommon;
18 use DateTime::Format::ISO8601;
20 use OpenILS::Application::AppUtils;
22 use OpenSRF::Utils qw/:datetime/;
23 use OpenSRF::AppSession;
24 use OpenILS::Utils::ModsParser;
26 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::Fieldmapper;
29 use OpenILS::Utils::CStoreEditor q/:funcs/;
30 use OpenILS::Const qw/:const/;
31 use OpenSRF::Utils::SettingsClient;
32 use OpenILS::Application::Cat::AssetCommon;
34 my $apputils = "OpenILS::Application::AppUtils";
37 my $holdcode = "OpenILS::Application::Circ::Holds";
39 # ------------------------------------------------------------------------
40 # Top level Circ package;
41 # ------------------------------------------------------------------------
45 OpenILS::Application::Circ::Circulate->initialize();
49 __PACKAGE__->register_method(
50 method => 'retrieve_circ',
52 api_name => 'open-ils.circ.retrieve',
54 Retrieve a circ object by id
55 @param authtoken Login session key
56 @pararm circid The id of the circ object
60 my( $s, $c, $a, $i ) = @_;
61 my $e = new_editor(authtoken => $a);
62 return $e->event unless $e->checkauth;
63 my $circ = $e->retrieve_action_circulation($i) or return $e->event;
64 if( $e->requestor->id ne $circ->usr ) {
65 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
71 __PACKAGE__->register_method(
72 method => 'fetch_circ_mods',
73 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
75 my($self, $conn, $args) = @_;
76 my $mods = new_editor()->retrieve_all_config_circ_modifier;
77 return [ map {$_->code} @$mods ] unless $$args{full};
81 __PACKAGE__->register_method(
82 method => 'ranged_billing_types',
83 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
85 sub ranged_billing_types {
86 my($self, $conn, $auth, $org_id, $depth) = @_;
87 my $e = new_editor(authtoken => $auth);
88 return $e->event unless $e->checkauth;
89 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
90 return $e->search_config_billing_type(
91 {owner => $U->get_org_full_path($org_id, $depth)});
96 # ------------------------------------------------------------------------
97 # Returns an array of {circ, record} hashes checked out by the user.
98 # ------------------------------------------------------------------------
99 __PACKAGE__->register_method(
100 method => "checkouts_by_user",
101 api_name => "open-ils.circ.actor.user.checked_out",
103 NOTES => <<" NOTES");
104 Returns a list of open circulations as a pile of objects. Each object
105 contains the relevant copy, circ, and record
108 sub checkouts_by_user {
109 my($self, $client, $auth, $user_id) = @_;
111 my $e = new_editor(authtoken=>$auth);
112 return $e->event unless $e->checkauth;
114 my $circ_ids = $e->search_action_circulation(
116 checkin_time => undef,
118 {stop_fines => undef},
119 {stop_fines => ['MAXFINES','LONGOVERDUE']}
125 for my $id (@$circ_ids) {
126 my $circ = $e->retrieve_action_circulation([
130 circ => ['target_copy'],
131 acp => ['call_number'],
137 # un-flesh for consistency
138 my $c = $circ->target_copy;
139 $circ->target_copy($c->id);
141 my $cn = $c->call_number;
142 $c->call_number($cn->id);
150 record => $U->record_to_mvr($t)
160 __PACKAGE__->register_method(
161 method => "checkouts_by_user_slim",
162 api_name => "open-ils.circ.actor.user.checked_out.slim",
163 NOTES => <<" NOTES");
164 Returns a list of open circulation objects
168 sub checkouts_by_user_slim {
169 my( $self, $client, $user_session, $user_id ) = @_;
171 my( $requestor, $target, $copy, $record, $evt );
173 ( $requestor, $target, $evt ) =
174 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
177 $logger->debug( 'User ' . $requestor->id .
178 " retrieving checked out items for user " . $target->id );
180 # XXX Make the call correct..
181 return $apputils->simplereq(
183 "open-ils.cstore.direct.action.open_circulation.search.atomic",
184 { usr => $target->id, checkin_time => undef } );
185 # { usr => $target->id } );
189 __PACKAGE__->register_method(
190 method => "checkouts_by_user_opac",
191 api_name => "open-ils.circ.actor.user.checked_out.opac",);
194 sub checkouts_by_user_opac {
195 my( $self, $client, $auth, $user_id ) = @_;
197 my $e = new_editor( authtoken => $auth );
198 return $e->event unless $e->checkauth;
199 $user_id ||= $e->requestor->id;
200 return $e->event unless
201 my $patron = $e->retrieve_actor_user($user_id);
204 my $search = {usr => $user_id, stop_fines => undef};
206 if( $user_id ne $e->requestor->id ) {
207 $data = $e->search_action_circulation(
208 $search, {checkperm=>1, permorg=>$patron->home_ou})
212 $data = $e->search_action_circulation($search);
219 __PACKAGE__->register_method(
220 method => "title_from_transaction",
221 api_name => "open-ils.circ.circ_transaction.find_title",
222 NOTES => <<" NOTES");
223 Returns a mods object for the title that is linked to from the
224 copy from the hold that created the given transaction
227 sub title_from_transaction {
228 my( $self, $client, $login_session, $transactionid ) = @_;
230 my( $user, $circ, $title, $evt );
232 ( $user, $evt ) = $apputils->checkses( $login_session );
235 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
238 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
241 return $apputils->record_to_mvr($title);
244 __PACKAGE__->register_method(
245 method => "staff_age_to_lost",
246 api_name => "open-ils.circ.circulation.age_to_lost",
249 This fires a circ.staff_age_to_lost Action-Trigger event against all
250 overdue circulations in scope of the specified context library and
251 user profile, which effectively marks the associated items as Lost.
252 This is likely to be done at the end of a semester in an academic
255 @param args : circ_lib, user_profile
259 sub staff_age_to_lost {
260 my( $self, $conn, $auth, $args ) = @_;
261 my $e = new_editor(authtoken=>$auth);
262 return $e->event unless $e->checkauth;
263 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
265 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
266 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
268 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
270 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
271 my $hook = 'circ.staff_age_to_lost';
272 my $context_org = 'circ_lib';
273 my $opt_granularity = undef;
275 "checkin_time" => undef,
276 "due_date" => { "<" => "now" },
278 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
279 { "stop_fines" => undef }
283 "select" => {"au" => ["id"]},
286 "profile" => $profiles,
287 "id" => { "=" => {"+circ" => "usr"} }
291 "select" => {"aou" => ["id"]},
295 {"id" => { "=" => {"+circ" => "circ_lib"} }},
302 my $req_timeout = 10800;
303 my $chunk_size = 100;
306 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
307 my @event_ids; my @chunked_ids;
308 while (my $resp = $req->recv(timeout => $req_timeout)) {
309 push(@event_ids, $resp->content);
310 push(@chunked_ids, $resp->content);
311 if (scalar(@chunked_ids) > $chunk_size) {
312 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
316 if (scalar(@chunked_ids) > 0) {
317 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
321 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
322 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
323 } elsif($req->complete) {
324 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
325 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
327 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
328 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
335 __PACKAGE__->register_method(
336 method => "new_set_circ_lost",
337 api_name => "open-ils.circ.circulation.set_lost",
339 Sets the copy and related open circulation to lost
341 @param args : barcode
346 # ---------------------------------------------------------------------
347 # Sets a circulation to lost. updates copy status to lost
348 # applies copy and/or prcoessing fees depending on org settings
349 # ---------------------------------------------------------------------
350 sub new_set_circ_lost {
351 my( $self, $conn, $auth, $args ) = @_;
353 my $e = new_editor(authtoken=>$auth, xact=>1);
354 return $e->die_event unless $e->checkauth;
356 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
357 or return $e->die_event;
359 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
367 __PACKAGE__->register_method(
368 method => "set_circ_claims_returned",
369 api_name => "open-ils.circ.circulation.set_claims_returned",
371 desc => q/Sets the circ for a given item as claims returned
372 If a backdate is provided, overdue fines will be voided
373 back to the backdate/,
375 {desc => 'Authentication token', type => 'string'},
376 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
378 return => {desc => q/1 on success, failure event on error, and
379 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
380 configured claims return maximum/}
384 __PACKAGE__->register_method(
385 method => "set_circ_claims_returned",
386 api_name => "open-ils.circ.circulation.set_claims_returned.override",
388 desc => q/This adds support for overrideing the configured max
389 claims returned amount.
390 @see open-ils.circ.circulation.set_claims_returned./,
394 sub set_circ_claims_returned {
395 my( $self, $conn, $auth, $args, $oargs ) = @_;
397 my $e = new_editor(authtoken=>$auth, xact=>1);
398 return $e->die_event unless $e->checkauth;
400 $oargs = { all => 1 } unless defined $oargs;
402 my $barcode = $$args{barcode};
403 my $backdate = $$args{backdate};
405 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
406 or return $e->die_event;
408 my $circ = $e->search_action_circulation(
409 {checkin_time => undef, target_copy => $copy->id})->[0]
410 or return $e->die_event;
412 $backdate = $circ->due_date if $$args{use_due_date};
414 $logger->info("marking circ for item $barcode as claims returned".
415 (($backdate) ? " with backdate $backdate" : ''));
417 my $patron = $e->retrieve_actor_user($circ->usr);
418 my $max_count = $U->ou_ancestor_setting_value(
419 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
421 # If the patron has too instances of many claims returned,
422 # require an override to continue. A configured max of
423 # 0 means all attempts require an override
424 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
426 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
428 # see if we're allowed to override
429 return $e->die_event unless
430 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
434 # exit early and return the max claims return event
436 return OpenILS::Event->new(
437 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
439 patron_count => $patron->claims_returned_count,
440 max_count => $max_count
446 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
447 or return $e->die_event;
449 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
450 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
453 $backdate = cleanse_ISO8601($backdate);
455 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
456 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
457 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
459 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
460 $backdate = cleanse_ISO8601($backdate);
462 # make it look like the circ stopped at the cliams returned time
463 $circ->stop_fines_time($backdate);
464 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
468 $e->update_action_circulation($circ) or return $e->die_event;
470 # see if there is a configured post-claims-return copy status
471 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
472 $copy->status($stat);
473 $copy->edit_date('now');
474 $copy->editor($e->requestor->id);
475 $e->update_asset_copy($copy) or return $e->die_event;
478 # Check if the copy circ lib wants lost fees voided on claims
480 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
481 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
486 return $result if ($result);
489 # Check if the copy circ lib wants lost processing fees voided on
491 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
492 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
497 return $result if ($result);
505 __PACKAGE__->register_method(
506 method => "post_checkin_backdate_circ",
507 api_name => "open-ils.circ.post_checkin_backdate",
509 desc => q/Back-date an already checked in circulation/,
511 {desc => 'Authentication token', type => 'string'},
512 {desc => 'Circ ID', type => 'number'},
513 {desc => 'ISO8601 backdate', type => 'string'},
515 return => {desc => q/1 on success, failure event on error/}
519 __PACKAGE__->register_method(
520 method => "post_checkin_backdate_circ",
521 api_name => "open-ils.circ.post_checkin_backdate.batch",
524 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
526 {desc => 'Authentication token', type => 'string'},
527 {desc => 'List of Circ ID', type => 'array'},
528 {desc => 'ISO8601 backdate', type => 'string'},
530 return => {desc => q/Set of: 1 on success, failure event on error/}
535 sub post_checkin_backdate_circ {
536 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
537 my $e = new_editor(authtoken=>$auth);
538 return $e->die_event unless $e->checkauth;
539 if($self->api_name =~ /batch/) {
540 foreach my $c (@$circ_id) {
541 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
544 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
552 sub post_checkin_backdate_circ_impl {
553 my($e, $circ_id, $backdate) = @_;
557 my $circ = $e->retrieve_action_circulation($circ_id)
558 or return $e->die_event;
560 # anyone with checkin perms can backdate (more restrictive?)
561 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
563 # don't allow back-dating an open circulation
564 return OpenILS::Event->new('BAD_PARAMS') unless
565 $backdate and $circ->checkin_time;
567 # update the checkin and stop_fines times to reflect the new backdate
568 $circ->stop_fines_time(cleanse_ISO8601($backdate));
569 $circ->checkin_time(cleanse_ISO8601($backdate));
570 $e->update_action_circulation($circ) or return $e->die_event;
572 # now void the overdues "erased" by the back-dating
573 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
576 # If the circ was closed before and the balance owned !=0, re-open the transaction
577 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
586 __PACKAGE__->register_method (
587 method => 'set_circ_due_date',
588 api_name => 'open-ils.circ.circulation.due_date.update',
590 Updates the due_date on the given circ
592 @param circid The id of the circ to update
593 @param date The timestamp of the new due date
597 sub set_circ_due_date {
598 my( $self, $conn, $auth, $circ_id, $date ) = @_;
600 my $e = new_editor(xact=>1, authtoken=>$auth);
601 return $e->die_event unless $e->checkauth;
602 my $circ = $e->retrieve_action_circulation($circ_id)
603 or return $e->die_event;
605 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
606 $date = cleanse_ISO8601($date);
608 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
609 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
610 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
611 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
614 $circ->due_date($date);
615 $e->update_action_circulation($circ) or return $e->die_event;
622 __PACKAGE__->register_method(
623 method => "create_in_house_use",
624 api_name => 'open-ils.circ.in_house_use.create',
626 Creates an in-house use action.
627 @param $authtoken The login session key
628 @param params A hash of params including
629 'location' The org unit id where the in-house use occurs
630 'copyid' The copy in question
631 'count' The number of in-house uses to apply to this copy
632 @return An array of id's representing the id's of the newly created
633 in-house use objects or an event on an error
636 __PACKAGE__->register_method(
637 method => "create_in_house_use",
638 api_name => 'open-ils.circ.non_cat_in_house_use.create',
642 sub create_in_house_use {
643 my( $self, $client, $auth, $params ) = @_;
646 my $org = $params->{location};
647 my $copyid = $params->{copyid};
648 my $count = $params->{count} || 1;
649 my $nc_type = $params->{non_cat_type};
650 my $use_time = $params->{use_time} || 'now';
652 my $e = new_editor(xact=>1,authtoken=>$auth);
653 return $e->event unless $e->checkauth;
654 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
656 my $non_cat = 1 if $self->api_name =~ /non_cat/;
660 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
662 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
668 if( $use_time ne 'now' ) {
669 $use_time = cleanse_ISO8601($use_time);
670 $logger->debug("in_house_use setting use time to $use_time");
681 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
682 $ihu->item_type($nc_type);
683 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
684 $cmeth = "create_action_non_cat_in_house_use";
687 $ihu = Fieldmapper::action::in_house_use->new;
689 $method = 'open-ils.storage.direct.action.in_house_use.create';
690 $cmeth = "create_action_in_house_use";
693 $ihu->staff($e->requestor->id);
694 $ihu->org_unit($org);
695 $ihu->use_time($use_time);
697 $ihu = $e->$cmeth($ihu) or return $e->event;
698 push( @ids, $ihu->id );
709 __PACKAGE__->register_method(
710 method => "view_circs",
711 api_name => "open-ils.circ.copy_checkout_history.retrieve",
713 Retrieves the last X circs for a given copy
714 @param authtoken The login session key
715 @param copyid The copy to check
716 @param count How far to go back in the item history
717 @return An array of circ ids
720 # ----------------------------------------------------------------------
721 # Returns $count most recent circs. If count exceeds the configured
722 # max, use the configured max instead
723 # ----------------------------------------------------------------------
725 my( $self, $client, $authtoken, $copyid, $count ) = @_;
727 my $e = new_editor(authtoken => $authtoken);
728 return $e->event unless $e->checkauth;
730 my $copy = $e->retrieve_asset_copy([
733 flesh_fields => {acp => ['call_number']}
735 ]) or return $e->event;
737 return $e->event unless $e->allowed(
738 'VIEW_COPY_CHECKOUT_HISTORY',
739 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
740 $copy->circ_lib : $copy->call_number->owning_lib);
742 my $max_history = $U->ou_ancestor_setting_value(
743 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
745 if(defined $max_history) {
746 $count = $max_history unless defined $count and $count < $max_history;
748 $count = 4 unless defined $count;
751 return $e->search_action_circulation([
752 {target_copy => $copyid},
753 {limit => $count, order_by => { circ => "xact_start DESC" }}
758 __PACKAGE__->register_method(
759 method => "circ_count",
760 api_name => "open-ils.circ.circulation.count",
762 Returns the number of times the item has circulated
763 @param copyid The copy to check
767 my( $self, $client, $copyid ) = @_;
769 my $count = new_editor()->json_query({
778 where => {'+circbyyr' => {copy => $copyid}}
790 __PACKAGE__->register_method(
791 method => 'fetch_notes',
793 api_name => 'open-ils.circ.copy_note.retrieve.all',
795 Returns an array of copy note objects.
796 @param args A named hash of parameters including:
797 authtoken : Required if viewing non-public notes
798 itemid : The id of the item whose notes we want to retrieve
799 pub : True if all the caller wants are public notes
800 @return An array of note objects
803 __PACKAGE__->register_method(
804 method => 'fetch_notes',
805 api_name => 'open-ils.circ.call_number_note.retrieve.all',
806 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
808 __PACKAGE__->register_method(
809 method => 'fetch_notes',
810 api_name => 'open-ils.circ.title_note.retrieve.all',
811 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
814 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
816 my( $self, $connection, $args ) = @_;
818 my $id = $$args{itemid};
819 my $authtoken = $$args{authtoken};
822 if( $self->api_name =~ /copy/ ) {
824 return $U->cstorereq(
825 'open-ils.cstore.direct.asset.copy_note.search.atomic',
826 { owning_copy => $id, pub => 't' } );
828 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
830 return $U->cstorereq(
831 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
834 } elsif( $self->api_name =~ /call_number/ ) {
836 return $U->cstorereq(
837 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
838 { call_number => $id, pub => 't' } );
840 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
842 return $U->cstorereq(
843 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
846 } elsif( $self->api_name =~ /title/ ) {
848 return $U->cstorereq(
849 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
850 { record => $id, pub => 't' } );
852 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
854 return $U->cstorereq(
855 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
862 __PACKAGE__->register_method(
863 method => 'has_notes',
864 api_name => 'open-ils.circ.copy.has_notes');
865 __PACKAGE__->register_method(
866 method => 'has_notes',
867 api_name => 'open-ils.circ.call_number.has_notes');
868 __PACKAGE__->register_method(
869 method => 'has_notes',
870 api_name => 'open-ils.circ.title.has_notes');
874 my( $self, $conn, $authtoken, $id ) = @_;
875 my $editor = new_editor(authtoken => $authtoken);
876 return $editor->event unless $editor->checkauth;
878 my $n = $editor->search_asset_copy_note(
879 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
881 $n = $editor->search_asset_call_number_note(
882 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
884 $n = $editor->search_biblio_record_note(
885 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
892 __PACKAGE__->register_method(
893 method => 'create_copy_note',
894 api_name => 'open-ils.circ.copy_note.create',
896 Creates a new copy note
897 @param authtoken The login session key
898 @param note The note object to create
899 @return The id of the new note object
902 sub create_copy_note {
903 my( $self, $connection, $authtoken, $note ) = @_;
905 my $e = new_editor(xact=>1, authtoken=>$authtoken);
906 return $e->event unless $e->checkauth;
907 my $copy = $e->retrieve_asset_copy(
911 flesh_fields => { 'acp' => ['call_number'] }
916 return $e->event unless
917 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
919 $note->create_date('now');
920 $note->creator($e->requestor->id);
921 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
924 $e->create_asset_copy_note($note) or return $e->event;
930 __PACKAGE__->register_method(
931 method => 'delete_copy_note',
932 api_name => 'open-ils.circ.copy_note.delete',
934 Deletes an existing copy note
935 @param authtoken The login session key
936 @param noteid The id of the note to delete
937 @return 1 on success - Event otherwise.
939 sub delete_copy_note {
940 my( $self, $conn, $authtoken, $noteid ) = @_;
942 my $e = new_editor(xact=>1, authtoken=>$authtoken);
943 return $e->die_event unless $e->checkauth;
945 my $note = $e->retrieve_asset_copy_note([
949 'acpn' => [ 'owning_copy' ],
950 'acp' => [ 'call_number' ],
953 ]) or return $e->die_event;
955 if( $note->creator ne $e->requestor->id ) {
956 return $e->die_event unless
957 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
960 $e->delete_asset_copy_note($note) or return $e->die_event;
966 __PACKAGE__->register_method(
967 method => 'age_hold_rules',
968 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
972 my( $self, $conn ) = @_;
973 return new_editor()->retrieve_all_config_rules_age_hold_protect();
978 __PACKAGE__->register_method(
979 method => 'copy_details_barcode',
981 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
982 sub copy_details_barcode {
983 my( $self, $conn, $auth, $barcode ) = @_;
984 my $e = new_editor();
985 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
986 return $e->event unless $cid;
987 return copy_details( $self, $conn, $auth, $cid );
991 __PACKAGE__->register_method(
992 method => 'copy_details',
993 api_name => 'open-ils.circ.copy_details.retrieve');
996 my( $self, $conn, $auth, $copy_id ) = @_;
997 my $e = new_editor(authtoken=>$auth);
998 return $e->event unless $e->checkauth;
1000 my $flesh = { flesh => 1 };
1002 my $copy = $e->retrieve_asset_copy(
1008 acp => ['call_number','parts','peer_record_maps','floating'],
1009 acn => ['record','prefix','suffix','label_class']
1012 ]) or return $e->event;
1015 # De-flesh the copy for backwards compatibility
1017 my $vol = $copy->call_number;
1019 $copy->call_number($vol->id);
1020 my $record = $vol->record;
1022 $vol->record($record->id);
1023 $mvr = $U->record_to_mvr($record);
1028 my $hold = $e->search_action_hold_request(
1030 current_copy => $copy_id,
1031 capture_time => { "!=" => undef },
1032 fulfillment_time => undef,
1033 cancel_time => undef,
1037 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1039 my $transit = $e->search_action_transit_copy(
1040 { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1042 # find the latest circ, open or closed
1043 my $circ = $e->search_action_circulation(
1045 { target_copy => $copy_id },
1051 'checkin_workstation',
1054 'recurring_fine_rule'
1057 order_by => { circ => 'xact_start desc' },
1067 transit => $transit,
1077 __PACKAGE__->register_method(
1078 method => 'mark_item',
1079 api_name => 'open-ils.circ.mark_item_damaged',
1081 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1082 @param authtoken The login session key
1083 @param copy_id The ID of the copy to mark as damaged
1084 @return 1 on success - Event otherwise.
1087 __PACKAGE__->register_method(
1088 method => 'mark_item',
1089 api_name => 'open-ils.circ.mark_item_missing',
1091 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1092 @param authtoken The login session key
1093 @param copy_id The ID of the copy to mark as missing
1094 @return 1 on success - Event otherwise.
1097 __PACKAGE__->register_method(
1098 method => 'mark_item',
1099 api_name => 'open-ils.circ.mark_item_bindery',
1101 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1102 @param authtoken The login session key
1103 @param copy_id The ID of the copy to mark as bindery
1104 @return 1 on success - Event otherwise.
1107 __PACKAGE__->register_method(
1108 method => 'mark_item',
1109 api_name => 'open-ils.circ.mark_item_on_order',
1111 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1112 @param authtoken The login session key
1113 @param copy_id The ID of the copy to mark as on order
1114 @return 1 on success - Event otherwise.
1117 __PACKAGE__->register_method(
1118 method => 'mark_item',
1119 api_name => 'open-ils.circ.mark_item_ill',
1121 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1122 @param authtoken The login session key
1123 @param copy_id The ID of the copy to mark as inter-library loan
1124 @return 1 on success - Event otherwise.
1127 __PACKAGE__->register_method(
1128 method => 'mark_item',
1129 api_name => 'open-ils.circ.mark_item_cataloging',
1131 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1132 @param authtoken The login session key
1133 @param copy_id The ID of the copy to mark as cataloging
1134 @return 1 on success - Event otherwise.
1137 __PACKAGE__->register_method(
1138 method => 'mark_item',
1139 api_name => 'open-ils.circ.mark_item_reserves',
1141 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1142 @param authtoken The login session key
1143 @param copy_id The ID of the copy to mark as reserves
1144 @return 1 on success - Event otherwise.
1147 __PACKAGE__->register_method(
1148 method => 'mark_item',
1149 api_name => 'open-ils.circ.mark_item_discard',
1151 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1152 @param authtoken The login session key
1153 @param copy_id The ID of the copy to mark as discard
1154 @return 1 on success - Event otherwise.
1159 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1160 my $e = new_editor(authtoken=>$auth, xact =>1);
1161 return $e->die_event unless $e->checkauth;
1164 my $copy = $e->retrieve_asset_copy([
1166 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1167 or return $e->die_event;
1170 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1171 $copy->circ_lib : $copy->call_number->owning_lib;
1173 my $perm = 'MARK_ITEM_MISSING';
1174 my $stat = OILS_COPY_STATUS_MISSING;
1176 if( $self->api_name =~ /damaged/ ) {
1177 $perm = 'MARK_ITEM_DAMAGED';
1178 $stat = OILS_COPY_STATUS_DAMAGED;
1179 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1180 return $evt if $evt;
1182 } elsif ( $self->api_name =~ /bindery/ ) {
1183 $perm = 'MARK_ITEM_BINDERY';
1184 $stat = OILS_COPY_STATUS_BINDERY;
1185 } elsif ( $self->api_name =~ /on_order/ ) {
1186 $perm = 'MARK_ITEM_ON_ORDER';
1187 $stat = OILS_COPY_STATUS_ON_ORDER;
1188 } elsif ( $self->api_name =~ /ill/ ) {
1189 $perm = 'MARK_ITEM_ILL';
1190 $stat = OILS_COPY_STATUS_ILL;
1191 } elsif ( $self->api_name =~ /cataloging/ ) {
1192 $perm = 'MARK_ITEM_CATALOGING';
1193 $stat = OILS_COPY_STATUS_CATALOGING;
1194 } elsif ( $self->api_name =~ /reserves/ ) {
1195 $perm = 'MARK_ITEM_RESERVES';
1196 $stat = OILS_COPY_STATUS_RESERVES;
1197 } elsif ( $self->api_name =~ /discard/ ) {
1198 $perm = 'MARK_ITEM_DISCARD';
1199 $stat = OILS_COPY_STATUS_DISCARD;
1202 # caller may proceed if either perm is allowed
1203 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1205 $copy->status($stat);
1206 $copy->edit_date('now');
1207 $copy->editor($e->requestor->id);
1209 $e->update_asset_copy($copy) or return $e->die_event;
1211 my $holds = $e->search_action_hold_request(
1213 current_copy => $copy->id,
1214 fulfillment_time => undef,
1215 cancel_time => undef,
1221 if( $self->api_name =~ /damaged/ ) {
1222 # now that we've committed the changes, create related A/T events
1223 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1224 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1227 $logger->debug("resetting holds that target the marked copy");
1228 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1233 sub handle_mark_damaged {
1234 my($e, $copy, $owning_lib, $args) = @_;
1236 my $apply = $args->{apply_fines} || '';
1237 return undef if $apply eq 'noapply';
1239 my $new_amount = $args->{override_amount};
1240 my $new_btype = $args->{override_btype};
1241 my $new_note = $args->{override_note};
1243 # grab the last circulation
1244 my $circ = $e->search_action_circulation([
1245 { target_copy => $copy->id},
1247 order_by => {circ => "xact_start DESC"},
1249 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1253 return undef unless $circ;
1255 my $charge_price = $U->ou_ancestor_setting_value(
1256 $owning_lib, 'circ.charge_on_damaged', $e);
1258 my $proc_fee = $U->ou_ancestor_setting_value(
1259 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1261 my $void_overdue = $U->ou_ancestor_setting_value(
1262 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1264 return undef unless $charge_price or $proc_fee;
1266 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1267 my $total = $copy_price + $proc_fee;
1271 if($new_amount and $new_btype) {
1273 # Allow staff to override the amount to charge for a damaged item
1274 # Consider the case where the item is only partially damaged
1275 # This value is meant to take the place of the item price and
1276 # optional processing fee.
1278 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1279 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1280 return $evt if $evt;
1284 if($charge_price and $copy_price) {
1285 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1286 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1287 return $evt if $evt;
1291 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1292 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1293 return $evt if $evt;
1297 # the assumption is that you would not void the overdues unless you
1298 # were also charging for the item and/or applying a processing fee
1300 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1301 return $evt if $evt;
1304 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1305 return $evt if $evt;
1307 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1308 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1310 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1311 return $evt2 if $evt2;
1316 return OpenILS::Event->new('DAMAGE_CHARGE',
1327 # ----------------------------------------------------------------------
1328 __PACKAGE__->register_method(
1329 method => 'mark_item_missing_pieces',
1330 api_name => 'open-ils.circ.mark_item_missing_pieces',
1332 Changes the status of a copy to "damaged" or to a custom status based on the
1333 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1335 @param authtoken The login session key
1336 @param copy_id The ID of the copy to mark as damaged
1337 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1341 sub mark_item_missing_pieces {
1342 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1343 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1344 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1346 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1347 return $e2->die_event unless $e2->checkauth;
1350 my $copy = $e2->retrieve_asset_copy([
1352 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1353 or return $e2->die_event;
1356 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1357 $copy->circ_lib : $copy->call_number->owning_lib;
1359 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1361 #### grab the last circulation
1362 my $circ = $e2->search_action_circulation([
1363 { target_copy => $copy->id},
1365 order_by => {circ => "xact_start DESC"}
1370 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1372 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1375 my $holds = $e2->search_action_hold_request(
1377 current_copy => $copy->id,
1378 fulfillment_time => undef,
1379 cancel_time => undef,
1383 $logger->debug("resetting holds that target the marked copy");
1384 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1387 if (! $e2->commit) {
1388 return $e2->die_event;
1391 my $e = new_editor(authtoken=>$auth, xact =>1);
1392 return $e->die_event unless $e->checkauth;
1394 if (! $circ->checkin_time) { # if circ active, attempt renew
1395 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1396 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1397 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1398 $circ = $res->[0]->{payload}{'circ'};
1399 $circ->target_copy( $copy->id );
1400 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1402 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1407 'copy_id'=>$circ->target_copy,
1408 'patron_id'=>$circ->usr,
1409 'skip_deposit_fee'=>1,
1410 'skip_rental_fee'=>1
1413 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1415 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1416 $e, $copy, $e->requestor, 1 );
1418 if ($hold) { # needed for hold? then due now
1420 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1421 my $due_date = DateTime->now(time_zone => 'local');
1422 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1424 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1428 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1429 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1430 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1431 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1432 $circ = $res->[0]->{payload}{'circ'};
1434 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1440 ### Update the item status
1442 my $custom_stat = $U->ou_ancestor_setting_value(
1443 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1444 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1446 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1447 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1449 $copy->status($stat);
1450 $copy->edit_date('now');
1451 $copy->editor($e->requestor->id);
1453 $e->update_asset_copy($copy) or return $e->die_event;
1457 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1458 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1460 return OpenILS::Event->new('SUCCESS',
1464 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1465 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1470 return $e->die_event;
1478 # ----------------------------------------------------------------------
1479 __PACKAGE__->register_method(
1480 method => 'magic_fetch',
1481 api_name => 'open-ils.agent.fetch'
1484 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1487 my( $self, $conn, $auth, $args ) = @_;
1488 my $e = new_editor( authtoken => $auth );
1489 return $e->event unless $e->checkauth;
1491 my $hint = $$args{hint};
1492 my $id = $$args{id};
1494 # Is the call allowed to fetch this type of object?
1495 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1497 # Find the class the implements the given hint
1498 my ($class) = grep {
1499 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1501 $class =~ s/Fieldmapper:://og;
1502 $class =~ s/::/_/og;
1503 my $method = "retrieve_$class";
1505 my $obj = $e->$method($id) or return $e->event;
1508 # ----------------------------------------------------------------------
1511 __PACKAGE__->register_method(
1512 method => "fleshed_circ_retrieve",
1514 api_name => "open-ils.circ.fleshed.retrieve",);
1516 sub fleshed_circ_retrieve {
1517 my( $self, $client, $id ) = @_;
1518 my $e = new_editor();
1519 my $circ = $e->retrieve_action_circulation(
1525 circ => [ qw/ target_copy / ],
1526 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1527 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1528 acn => [ qw/ record / ],
1532 ) or return $e->event;
1534 my $copy = $circ->target_copy;
1535 my $vol = $copy->call_number;
1536 my $rec = $circ->target_copy->call_number->record;
1538 $vol->record($rec->id);
1539 $copy->call_number($vol->id);
1540 $circ->target_copy($copy->id);
1544 if( $rec->id == OILS_PRECAT_RECORD ) {
1548 $mvr = $U->record_to_mvr($rec);
1549 $rec->marc(''); # drop the bulky marc data
1563 __PACKAGE__->register_method(
1564 method => "test_batch_circ_events",
1565 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1568 # method for testing the behavior of a given event definition
1569 sub test_batch_circ_events {
1570 my($self, $conn, $auth, $event_def, $barcode) = @_;
1572 my $e = new_editor(authtoken => $auth);
1573 return $e->event unless $e->checkauth;
1574 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1576 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1577 or return $e->event;
1579 my $circ = $e->search_action_circulation(
1580 {target_copy => $copy->id, checkin_time => undef})->[0]
1581 or return $e->event;
1583 return undef unless $circ;
1585 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1589 __PACKAGE__->register_method(
1590 method => "fire_circ_events",
1591 api_name => "open-ils.circ.fire_circ_trigger_events",
1593 General event def runner for circ objects. If no event def ID
1594 is provided, the hook will be used to find the best event_def
1595 match based on the context org unit
1599 __PACKAGE__->register_method(
1600 method => "fire_circ_events",
1601 api_name => "open-ils.circ.fire_hold_trigger_events",
1603 General event def runner for hold objects. If no event def ID
1604 is provided, the hook will be used to find the best event_def
1605 match based on the context org unit
1609 __PACKAGE__->register_method(
1610 method => "fire_circ_events",
1611 api_name => "open-ils.circ.fire_user_trigger_events",
1613 General event def runner for user objects. If no event def ID
1614 is provided, the hook will be used to find the best event_def
1615 match based on the context org unit
1620 sub fire_circ_events {
1621 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1623 my $e = new_editor(authtoken => $auth, xact => 1);
1624 return $e->event unless $e->checkauth;
1628 if($self->api_name =~ /hold/) {
1629 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1630 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1631 } elsif($self->api_name =~ /user/) {
1632 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1633 $targets = $e->batch_retrieve_actor_user($target_ids);
1635 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1636 $targets = $e->batch_retrieve_action_circulation($target_ids);
1638 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1639 # simply making this method authoritative because of weirdness
1640 # with transaction handling in A/T code that causes rollback
1641 # failure down the line if handling many targets
1643 return undef unless @$targets;
1644 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1647 __PACKAGE__->register_method(
1648 method => "user_payments_list",
1649 api_name => "open-ils.circ.user_payments.filtered.batch",
1652 desc => q/Returns a fleshed, date-limited set of all payments a user
1653 has made. By default, ordered by payment date. Optionally
1654 ordered by other columns in the top-level "mp" object/,
1656 {desc => 'Authentication token', type => 'string'},
1657 {desc => 'User ID', type => 'number'},
1658 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1660 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1661 and the related fully-realized payment object (e.g money.cash_payment)/}
1665 sub user_payments_list {
1666 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1668 my $e = new_editor(authtoken => $auth);
1669 return $e->event unless $e->checkauth;
1671 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1672 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1674 $order_by ||= ['payment_ts'];
1676 # all payments by user, between start_date and end_date
1677 my $payments = $e->json_query({
1678 select => {mp => ['id']},
1682 fkey => 'xact', field => 'id'}
1686 '+mbt' => {usr => $user_id},
1687 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1689 order_by => {mp => $order_by}
1692 for my $payment_id (@$payments) {
1693 my $payment = $e->retrieve_money_payment([
1701 'credit_card_payment',
1716 $conn->respond($payment);
1723 __PACKAGE__->register_method(
1724 method => "retrieve_circ_chain",
1725 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1728 desc => q/Given a circulation, this returns all circulation objects
1729 that are part of the same chain of renewals./,
1731 {desc => 'Authentication token', type => 'string'},
1732 {desc => 'Circ ID', type => 'number'},
1734 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1738 __PACKAGE__->register_method(
1739 method => "retrieve_circ_chain",
1740 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1742 desc => q/Given a circulation, this returns a summary of the circulation objects
1743 that are part of the same chain of renewals./,
1745 {desc => 'Authentication token', type => 'string'},
1746 {desc => 'Circ ID', type => 'number'},
1748 return => {desc => q/Circulation Chain Summary/}
1752 sub retrieve_circ_chain {
1753 my($self, $conn, $auth, $circ_id) = @_;
1755 my $e = new_editor(authtoken => $auth);
1756 return $e->event unless $e->checkauth;
1757 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1759 if($self->api_name =~ /summary/) {
1760 return $U->create_circ_chain_summary($e, $circ_id);
1764 my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1766 for my $circ_info (@$chain) {
1767 my $circ = Fieldmapper::action::circulation->new;
1768 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1769 $conn->respond($circ);
1776 __PACKAGE__->register_method(
1777 method => "retrieve_prev_circ_chain",
1778 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1781 desc => q/Given a circulation, this returns all circulation objects
1782 that are part of the previous chain of renewals./,
1784 {desc => 'Authentication token', type => 'string'},
1785 {desc => 'Circ ID', type => 'number'},
1787 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1791 __PACKAGE__->register_method(
1792 method => "retrieve_prev_circ_chain",
1793 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1795 desc => q/Given a circulation, this returns a summary of the circulation objects
1796 that are part of the previous chain of renewals./,
1798 {desc => 'Authentication token', type => 'string'},
1799 {desc => 'Circ ID', type => 'number'},
1801 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1805 sub retrieve_prev_circ_chain {
1806 my($self, $conn, $auth, $circ_id) = @_;
1808 my $e = new_editor(authtoken => $auth);
1809 return $e->event unless $e->checkauth;
1810 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1812 if($self->api_name =~ /summary/) {
1813 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1814 my $target_copy = $$first_circ{'target_copy'};
1815 my $usr = $$first_circ{'usr'};
1816 my $last_circ_from_prev_chain = $e->json_query({
1817 'select' => { 'circ' => ['id','usr'] },
1820 target_copy => $target_copy,
1821 xact_start => { '<' => $$first_circ{'xact_start'} }
1823 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1826 return undef unless $last_circ_from_prev_chain;
1827 return undef unless $$last_circ_from_prev_chain{'id'};
1828 my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1829 return undef unless $sum;
1830 my $obj = Fieldmapper::action::circ_chain_summary->new;
1831 $obj->$_($sum->{$_}) for keys %$sum;
1832 return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1836 my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1837 my $target_copy = $$first_circ{'target_copy'};
1838 my $last_circ_from_prev_chain = $e->json_query({
1839 'select' => { 'circ' => ['id'] },
1842 target_copy => $target_copy,
1843 xact_start => { '<' => $$first_circ{'xact_start'} }
1845 'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1848 return undef unless $last_circ_from_prev_chain;
1849 return undef unless $$last_circ_from_prev_chain{'id'};
1850 my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1852 for my $circ_info (@$chain) {
1853 my $circ = Fieldmapper::action::circulation->new;
1854 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1855 $conn->respond($circ);
1863 __PACKAGE__->register_method(
1864 method => "get_copy_due_date",
1865 api_name => "open-ils.circ.copy.due_date.retrieve",
1868 Given a copy ID, returns the due date for the copy if it's
1869 currently circulating. Otherwise, returns null. Note, this is a public
1870 method requiring no authentication. Only the due date is exposed.
1873 {desc => 'Copy ID', type => 'number'}
1875 return => {desc => q/
1876 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1881 sub get_copy_due_date {
1882 my($self, $conn, $copy_id) = @_;
1883 my $e = new_editor();
1885 my $circ = $e->json_query({
1886 select => {circ => ['due_date']},
1889 target_copy => $copy_id,
1890 checkin_time => undef,
1892 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1893 {stop_fines => undef}
1897 })->[0] or return undef;
1899 return $circ->{due_date};
1906 # {"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}}