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::CircNotify;
12 use OpenILS::Application::Circ::CreditCard;
13 use OpenILS::Application::Circ::Money;
14 use OpenILS::Application::Circ::NonCat;
15 use OpenILS::Application::Circ::CopyLocations;
16 use OpenILS::Application::Circ::CircCommon;
19 use DateTime::Format::ISO8601;
21 use OpenILS::Application::AppUtils;
23 use OpenSRF::Utils qw/:datetime/;
24 use OpenSRF::AppSession;
25 use OpenILS::Utils::ModsParser;
27 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::Fieldmapper;
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
58 @param all_circ Returns an action.all_circulation_slim object instead
59 of an action.circulation object to pick up aged circs.
64 my( $s, $c, $a, $i, $all_circ ) = @_;
65 my $e = new_editor(authtoken => $a);
66 return $e->event unless $e->checkauth;
67 my $method = $all_circ ?
68 'retrieve_action_all_circulation_slim' :
69 'retrieve_action_circulation';
70 my $circ = $e->$method($i) or return $e->event;
71 if( $e->requestor->id ne ($circ->usr || '') ) {
72 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
78 __PACKAGE__->register_method(
79 method => 'fetch_circ_mods',
80 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
82 my($self, $conn, $args) = @_;
83 my $mods = new_editor()->retrieve_all_config_circ_modifier;
84 return [ map {$_->code} @$mods ] unless $$args{full};
88 __PACKAGE__->register_method(
89 method => 'ranged_billing_types',
90 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
92 sub ranged_billing_types {
93 my($self, $conn, $auth, $org_id, $depth) = @_;
94 my $e = new_editor(authtoken => $auth);
95 return $e->event unless $e->checkauth;
96 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
97 return $e->search_config_billing_type(
98 {owner => $U->get_org_full_path($org_id, $depth)});
103 # ------------------------------------------------------------------------
104 # Returns an array of {circ, record} hashes checked out by the user.
105 # ------------------------------------------------------------------------
106 __PACKAGE__->register_method(
107 method => "checkouts_by_user",
108 api_name => "open-ils.circ.actor.user.checked_out",
110 NOTES => <<" NOTES");
111 Returns a list of open circulations as a pile of objects. Each object
112 contains the relevant copy, circ, and record
115 sub checkouts_by_user {
116 my($self, $client, $auth, $user_id) = @_;
118 my $e = new_editor(authtoken=>$auth);
119 return $e->event unless $e->checkauth;
121 my $circ_ids = $e->search_action_circulation(
123 checkin_time => undef,
125 {stop_fines => undef},
126 {stop_fines => ['MAXFINES','LONGOVERDUE']}
132 for my $id (@$circ_ids) {
133 my $circ = $e->retrieve_action_circulation([
137 circ => ['target_copy'],
138 acp => ['call_number'],
144 # un-flesh for consistency
145 my $c = $circ->target_copy;
146 $circ->target_copy($c->id);
148 my $cn = $c->call_number;
149 $c->call_number($cn->id);
157 record => $U->record_to_mvr($t)
167 __PACKAGE__->register_method(
168 method => "checkouts_by_user_slim",
169 api_name => "open-ils.circ.actor.user.checked_out.slim",
170 NOTES => <<" NOTES");
171 Returns a list of open circulation objects
175 sub checkouts_by_user_slim {
176 my( $self, $client, $user_session, $user_id ) = @_;
178 my( $requestor, $target, $copy, $record, $evt );
180 ( $requestor, $target, $evt ) =
181 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
184 $logger->debug( 'User ' . $requestor->id .
185 " retrieving checked out items for user " . $target->id );
187 # XXX Make the call correct..
188 return $apputils->simplereq(
190 "open-ils.cstore.direct.action.open_circulation.search.atomic",
191 { usr => $target->id, checkin_time => undef } );
192 # { usr => $target->id } );
196 __PACKAGE__->register_method(
197 method => "checkouts_by_user_opac",
198 api_name => "open-ils.circ.actor.user.checked_out.opac",);
201 sub checkouts_by_user_opac {
202 my( $self, $client, $auth, $user_id ) = @_;
204 my $e = new_editor( authtoken => $auth );
205 return $e->event unless $e->checkauth;
206 $user_id ||= $e->requestor->id;
207 return $e->event unless
208 my $patron = $e->retrieve_actor_user($user_id);
211 my $search = {usr => $user_id, stop_fines => undef};
213 if( $user_id ne $e->requestor->id ) {
214 $data = $e->search_action_circulation(
215 $search, {checkperm=>1, permorg=>$patron->home_ou})
219 $data = $e->search_action_circulation($search);
226 __PACKAGE__->register_method(
227 method => "title_from_transaction",
228 api_name => "open-ils.circ.circ_transaction.find_title",
229 NOTES => <<" NOTES");
230 Returns a mods object for the title that is linked to from the
231 copy from the hold that created the given transaction
234 sub title_from_transaction {
235 my( $self, $client, $login_session, $transactionid ) = @_;
237 my( $user, $circ, $title, $evt );
239 ( $user, $evt ) = $apputils->checkses( $login_session );
242 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
245 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
248 return $apputils->record_to_mvr($title);
251 __PACKAGE__->register_method(
252 method => "staff_age_to_lost",
253 api_name => "open-ils.circ.circulation.age_to_lost",
256 This fires a circ.staff_age_to_lost Action-Trigger event against all
257 overdue circulations in scope of the specified context library and
258 user profile, which effectively marks the associated items as Lost.
259 This is likely to be done at the end of a semester in an academic
262 @param args : circ_lib, user_profile
266 sub staff_age_to_lost {
267 my( $self, $conn, $auth, $args ) = @_;
268 my $e = new_editor(authtoken=>$auth);
269 return $e->event unless $e->checkauth;
270 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
272 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
273 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
275 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
277 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
278 my $hook = 'circ.staff_age_to_lost';
279 my $context_org = 'circ_lib';
280 my $opt_granularity = undef;
282 "checkin_time" => undef,
283 "due_date" => { "<" => "now" },
285 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
286 { "stop_fines" => undef }
290 "select" => {"au" => ["id"]},
293 "profile" => $profiles,
294 "id" => { "=" => {"+circ" => "usr"} }
298 "select" => {"aou" => ["id"]},
302 {"id" => { "=" => {"+circ" => "circ_lib"} }},
309 my $req_timeout = 10800;
310 my $chunk_size = 100;
313 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
314 my @event_ids; my @chunked_ids;
315 while (my $resp = $req->recv(timeout => $req_timeout)) {
316 push(@event_ids, $resp->content);
317 push(@chunked_ids, $resp->content);
318 if (scalar(@chunked_ids) > $chunk_size) {
319 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
323 if (scalar(@chunked_ids) > 0) {
324 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
328 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
329 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
330 } elsif($req->complete) {
331 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
332 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
334 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
335 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
342 __PACKAGE__->register_method(
343 method => "new_set_circ_lost",
344 api_name => "open-ils.circ.circulation.set_lost",
346 Sets the copy and related open circulation to lost
348 @param args : barcode
353 # ---------------------------------------------------------------------
354 # Sets a circulation to lost. updates copy status to lost
355 # applies copy and/or prcoessing fees depending on org settings
356 # ---------------------------------------------------------------------
357 sub new_set_circ_lost {
358 my( $self, $conn, $auth, $args ) = @_;
360 my $e = new_editor(authtoken=>$auth, xact=>1);
361 return $e->die_event unless $e->checkauth;
363 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
364 or return $e->die_event;
366 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
374 __PACKAGE__->register_method(
375 method => "set_circ_claims_returned",
376 api_name => "open-ils.circ.circulation.set_claims_returned",
378 desc => q/Sets the circ for a given item as claims returned
379 If a backdate is provided, overdue fines will be voided
380 back to the backdate/,
382 {desc => 'Authentication token', type => 'string'},
383 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
385 return => {desc => q/1 on success, failure event on error, and
386 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
387 configured claims return maximum/}
391 __PACKAGE__->register_method(
392 method => "set_circ_claims_returned",
393 api_name => "open-ils.circ.circulation.set_claims_returned.override",
395 desc => q/This adds support for overrideing the configured max
396 claims returned amount.
397 @see open-ils.circ.circulation.set_claims_returned./,
401 sub set_circ_claims_returned {
402 my( $self, $conn, $auth, $args, $oargs ) = @_;
404 my $e = new_editor(authtoken=>$auth, xact=>1);
405 return $e->die_event unless $e->checkauth;
407 $oargs = { all => 1 } unless defined $oargs;
409 my $barcode = $$args{barcode};
410 my $backdate = $$args{backdate};
412 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
413 or return $e->die_event;
415 my $circ = $e->search_action_circulation(
416 {checkin_time => undef, target_copy => $copy->id})->[0]
417 or return $e->die_event;
419 $backdate = $circ->due_date if $$args{use_due_date};
421 $logger->info("marking circ for item $barcode as claims returned".
422 (($backdate) ? " with backdate $backdate" : ''));
424 my $patron = $e->retrieve_actor_user($circ->usr);
425 my $max_count = $U->ou_ancestor_setting_value(
426 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
428 # If the patron has too instances of many claims returned,
429 # require an override to continue. A configured max of
430 # 0 means all attempts require an override
431 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
433 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
435 # see if we're allowed to override
436 return $e->die_event unless
437 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
441 # exit early and return the max claims return event
443 return OpenILS::Event->new(
444 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
446 patron_count => $patron->claims_returned_count,
447 max_count => $max_count
453 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
454 or return $e->die_event;
456 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
457 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
460 $backdate = cleanse_ISO8601($backdate);
462 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
463 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
464 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
466 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
467 $backdate = cleanse_ISO8601($backdate);
469 # make it look like the circ stopped at the cliams returned time
470 $circ->stop_fines_time($backdate);
471 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
475 $e->update_action_circulation($circ) or return $e->die_event;
477 # see if there is a configured post-claims-return copy status
478 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
479 $copy->status($stat);
480 $copy->edit_date('now');
481 $copy->editor($e->requestor->id);
482 $e->update_asset_copy($copy) or return $e->die_event;
485 # Check if the copy circ lib wants lost fees voided on claims
487 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
488 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
499 # Check if the copy circ lib wants lost processing fees voided on
501 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
502 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
513 # Check if the copy circ lib wants longoverdue fees voided on claims
515 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
516 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
527 # Check if the copy circ lib wants longoverdue processing fees voided on
529 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
530 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
541 # Now that all data has been munged, do a no-op update of
542 # the patron to force a change of the last_xact_id value.
543 $e->update_actor_user($e->retrieve_actor_user($circ->usr))
544 or return $e->die_event;
551 __PACKAGE__->register_method(
552 method => "post_checkin_backdate_circ",
553 api_name => "open-ils.circ.post_checkin_backdate",
555 desc => q/Back-date an already checked in circulation/,
557 {desc => 'Authentication token', type => 'string'},
558 {desc => 'Circ ID', type => 'number'},
559 {desc => 'ISO8601 backdate', type => 'string'},
561 return => {desc => q/1 on success, failure event on error/}
565 __PACKAGE__->register_method(
566 method => "post_checkin_backdate_circ",
567 api_name => "open-ils.circ.post_checkin_backdate.batch",
570 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
572 {desc => 'Authentication token', type => 'string'},
573 {desc => 'List of Circ ID', type => 'array'},
574 {desc => 'ISO8601 backdate', type => 'string'},
576 return => {desc => q/Set of: 1 on success, failure event on error/}
581 sub post_checkin_backdate_circ {
582 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
583 my $e = new_editor(authtoken=>$auth);
584 return $e->die_event unless $e->checkauth;
585 if($self->api_name =~ /batch/) {
586 foreach my $c (@$circ_id) {
587 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
590 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
598 sub post_checkin_backdate_circ_impl {
599 my($e, $circ_id, $backdate) = @_;
603 my $circ = $e->retrieve_action_circulation($circ_id)
604 or return $e->die_event;
606 # anyone with checkin perms can backdate (more restrictive?)
607 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
609 # don't allow back-dating an open circulation
610 return OpenILS::Event->new('BAD_PARAMS') unless
611 $backdate and $circ->checkin_time;
613 # update the checkin and stop_fines times to reflect the new backdate
614 $circ->stop_fines_time(cleanse_ISO8601($backdate));
615 $circ->checkin_time(cleanse_ISO8601($backdate));
616 $e->update_action_circulation($circ) or return $e->die_event;
618 # now void the overdues "erased" by the back-dating
619 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
622 # If the circ was closed before and the balance owned !=0, re-open the transaction
623 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
632 __PACKAGE__->register_method (
633 method => 'set_circ_due_date',
634 api_name => 'open-ils.circ.circulation.due_date.update',
636 Updates the due_date on the given circ
638 @param circid The id of the circ to update
639 @param date The timestamp of the new due date
643 sub set_circ_due_date {
644 my( $self, $conn, $auth, $circ_id, $date ) = @_;
646 my $e = new_editor(xact=>1, authtoken=>$auth);
647 return $e->die_event unless $e->checkauth;
648 my $circ = $e->retrieve_action_circulation($circ_id)
649 or return $e->die_event;
651 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
652 $date = cleanse_ISO8601($date);
654 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
655 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
656 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
657 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
660 $circ->due_date($date);
661 $e->update_action_circulation($circ) or return $e->die_event;
668 __PACKAGE__->register_method(
669 method => "create_in_house_use",
670 api_name => 'open-ils.circ.in_house_use.create',
672 Creates an in-house use action.
673 @param $authtoken The login session key
674 @param params A hash of params including
675 'location' The org unit id where the in-house use occurs
676 'copyid' The copy in question
677 'count' The number of in-house uses to apply to this copy
678 @return An array of id's representing the id's of the newly created
679 in-house use objects or an event on an error
682 __PACKAGE__->register_method(
683 method => "create_in_house_use",
684 api_name => 'open-ils.circ.non_cat_in_house_use.create',
688 sub create_in_house_use {
689 my( $self, $client, $auth, $params ) = @_;
692 my $org = $params->{location};
693 my $copyid = $params->{copyid};
694 my $count = $params->{count} || 1;
695 my $nc_type = $params->{non_cat_type};
696 my $use_time = $params->{use_time} || 'now';
698 my $e = new_editor(xact=>1,authtoken=>$auth);
699 return $e->event unless $e->checkauth;
700 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
702 my $non_cat = 1 if $self->api_name =~ /non_cat/;
706 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
708 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
714 if( $use_time ne 'now' ) {
715 $use_time = cleanse_ISO8601($use_time);
716 $logger->debug("in_house_use setting use time to $use_time");
727 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
728 $ihu->item_type($nc_type);
729 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
730 $cmeth = "create_action_non_cat_in_house_use";
733 $ihu = Fieldmapper::action::in_house_use->new;
735 $method = 'open-ils.storage.direct.action.in_house_use.create';
736 $cmeth = "create_action_in_house_use";
739 $ihu->staff($e->requestor->id);
740 $ihu->org_unit($org);
741 $ihu->use_time($use_time);
743 $ihu = $e->$cmeth($ihu) or return $e->event;
744 push( @ids, $ihu->id );
755 __PACKAGE__->register_method(
756 method => "view_circs",
757 api_name => "open-ils.circ.copy_checkout_history.retrieve",
759 Retrieves the last X circs for a given copy
760 @param authtoken The login session key
761 @param copyid The copy to check
762 @param count How far to go back in the item history
763 @return An array of circ ids
766 # ----------------------------------------------------------------------
767 # Returns $count most recent circs. If count exceeds the configured
768 # max, use the configured max instead
769 # ----------------------------------------------------------------------
771 my( $self, $client, $authtoken, $copyid, $count ) = @_;
773 my $e = new_editor(authtoken => $authtoken);
774 return $e->event unless $e->checkauth;
776 my $copy = $e->retrieve_asset_copy([
779 flesh_fields => {acp => ['call_number']}
781 ]) or return $e->event;
783 return $e->event unless $e->allowed(
784 'VIEW_COPY_CHECKOUT_HISTORY',
785 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
786 $copy->circ_lib : $copy->call_number->owning_lib);
788 my $max_history = $U->ou_ancestor_setting_value(
789 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
791 if(defined $max_history) {
792 $count = $max_history unless defined $count and $count < $max_history;
794 $count = 4 unless defined $count;
797 return $e->search_action_all_circulation_slim([
798 {target_copy => $copyid},
799 {limit => $count, order_by => { aacs => "xact_start DESC" }}
804 __PACKAGE__->register_method(
805 method => "circ_count",
806 api_name => "open-ils.circ.circulation.count",
808 Returns the number of times the item has circulated
809 @param copyid The copy to check
813 my( $self, $client, $copyid ) = @_;
815 my $count = new_editor()->json_query({
824 where => {'+circbyyr' => {copy => $copyid}}
836 __PACKAGE__->register_method(
837 method => 'fetch_notes',
839 api_name => 'open-ils.circ.copy_note.retrieve.all',
841 Returns an array of copy note objects.
842 @param args A named hash of parameters including:
843 authtoken : Required if viewing non-public notes
844 itemid : The id of the item whose notes we want to retrieve
845 pub : True if all the caller wants are public notes
846 @return An array of note objects
849 __PACKAGE__->register_method(
850 method => 'fetch_notes',
851 api_name => 'open-ils.circ.call_number_note.retrieve.all',
852 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
854 __PACKAGE__->register_method(
855 method => 'fetch_notes',
856 api_name => 'open-ils.circ.title_note.retrieve.all',
857 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
860 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
862 my( $self, $connection, $args ) = @_;
864 my $id = $$args{itemid};
865 my $authtoken = $$args{authtoken};
868 if( $self->api_name =~ /copy/ ) {
870 return $U->cstorereq(
871 'open-ils.cstore.direct.asset.copy_note.search.atomic',
872 { owning_copy => $id, pub => 't' } );
874 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
876 return $U->cstorereq(
877 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
880 } elsif( $self->api_name =~ /call_number/ ) {
882 return $U->cstorereq(
883 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
884 { call_number => $id, pub => 't' } );
886 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
888 return $U->cstorereq(
889 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
892 } elsif( $self->api_name =~ /title/ ) {
894 return $U->cstorereq(
895 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
896 { record => $id, pub => 't' } );
898 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
900 return $U->cstorereq(
901 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
908 __PACKAGE__->register_method(
909 method => 'has_notes',
910 api_name => 'open-ils.circ.copy.has_notes');
911 __PACKAGE__->register_method(
912 method => 'has_notes',
913 api_name => 'open-ils.circ.call_number.has_notes');
914 __PACKAGE__->register_method(
915 method => 'has_notes',
916 api_name => 'open-ils.circ.title.has_notes');
920 my( $self, $conn, $authtoken, $id ) = @_;
921 my $editor = new_editor(authtoken => $authtoken);
922 return $editor->event unless $editor->checkauth;
924 my $n = $editor->search_asset_copy_note(
925 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
927 $n = $editor->search_asset_call_number_note(
928 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
930 $n = $editor->search_biblio_record_note(
931 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
938 __PACKAGE__->register_method(
939 method => 'create_copy_note',
940 api_name => 'open-ils.circ.copy_note.create',
942 Creates a new copy note
943 @param authtoken The login session key
944 @param note The note object to create
945 @return The id of the new note object
948 sub create_copy_note {
949 my( $self, $connection, $authtoken, $note ) = @_;
951 my $e = new_editor(xact=>1, authtoken=>$authtoken);
952 return $e->event unless $e->checkauth;
953 my $copy = $e->retrieve_asset_copy(
957 flesh_fields => { 'acp' => ['call_number'] }
962 return $e->event unless
963 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
965 $note->create_date('now');
966 $note->creator($e->requestor->id);
967 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
970 $e->create_asset_copy_note($note) or return $e->event;
976 __PACKAGE__->register_method(
977 method => 'delete_copy_note',
978 api_name => 'open-ils.circ.copy_note.delete',
980 Deletes an existing copy note
981 @param authtoken The login session key
982 @param noteid The id of the note to delete
983 @return 1 on success - Event otherwise.
985 sub delete_copy_note {
986 my( $self, $conn, $authtoken, $noteid ) = @_;
988 my $e = new_editor(xact=>1, authtoken=>$authtoken);
989 return $e->die_event unless $e->checkauth;
991 my $note = $e->retrieve_asset_copy_note([
995 'acpn' => [ 'owning_copy' ],
996 'acp' => [ 'call_number' ],
999 ]) or return $e->die_event;
1001 if( $note->creator ne $e->requestor->id ) {
1002 return $e->die_event unless
1003 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1006 $e->delete_asset_copy_note($note) or return $e->die_event;
1011 __PACKAGE__->register_method(
1012 method => 'fetch_copy_tags',
1014 api_name => 'open-ils.circ.copy_tags.retrieve',
1016 Returns an array of publicly-visible copy tag objects.
1017 @param args A named hash of parameters including:
1018 copy_id : The id of the item whose notes we want to retrieve
1019 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1020 scope : top of org subtree whose copy tags we want to see
1021 depth : how far down to look for copy tags (optional)
1022 @return An array of copy tag objects
1024 __PACKAGE__->register_method(
1025 method => 'fetch_copy_tags',
1027 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1029 Returns an array of all copy tag objects.
1030 @param args A named hash of parameters including:
1031 authtoken : Required to view non-public notes
1032 copy_id : The id of the item whose notes we want to retrieve (optional)
1033 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1034 scope : top of org subtree whose copy tags we want to see
1035 depth : how far down to look for copy tags (optional)
1036 @return An array of copy tag objects
1039 sub fetch_copy_tags {
1040 my ($self, $conn, $args) = @_;
1042 my $org = $args->{scope};
1043 my $depth = $args->{depth};
1047 if ($self->api_name =~ /\.staff/) {
1048 my $authtoken = $args->{authtoken};
1049 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1050 $e = new_editor(authtoken => $args->{authtoken});
1051 return $e->event unless $e->checkauth;
1052 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1055 $filter->{pub} = 't';
1057 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1058 $filter->{'+acptcm'} = {
1059 copy => $args->{copy_id}
1062 # filter by owner of copy tag and depth
1063 $filter->{owner} = {
1065 select => {aou => [{
1067 transform => 'actor.org_unit_descendants',
1068 result_field => 'id',
1069 (defined($depth) ? ( params => [$depth] ) : ()),
1072 where => {id => $org}
1076 return $e->search_asset_copy_tag([$filter, { join => { acptcm => {} } }]);
1080 __PACKAGE__->register_method(
1081 method => 'age_hold_rules',
1082 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1085 sub age_hold_rules {
1086 my( $self, $conn ) = @_;
1087 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1092 __PACKAGE__->register_method(
1093 method => 'copy_details_barcode',
1095 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1096 sub copy_details_barcode {
1097 my( $self, $conn, $auth, $barcode ) = @_;
1098 my $e = new_editor();
1099 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1100 return $e->event unless $cid;
1101 return copy_details( $self, $conn, $auth, $cid );
1105 __PACKAGE__->register_method(
1106 method => 'copy_details',
1107 api_name => 'open-ils.circ.copy_details.retrieve');
1110 my( $self, $conn, $auth, $copy_id ) = @_;
1111 my $e = new_editor(authtoken=>$auth);
1112 return $e->event unless $e->checkauth;
1114 my $flesh = { flesh => 1 };
1116 my $copy = $e->retrieve_asset_copy(
1122 acp => ['call_number','parts','peer_record_maps','floating'],
1123 acn => ['record','prefix','suffix','label_class']
1126 ]) or return $e->event;
1129 # De-flesh the copy for backwards compatibility
1131 my $vol = $copy->call_number;
1133 $copy->call_number($vol->id);
1134 my $record = $vol->record;
1136 $vol->record($record->id);
1137 $mvr = $U->record_to_mvr($record);
1142 my $hold = $e->search_action_hold_request(
1144 current_copy => $copy_id,
1145 capture_time => { "!=" => undef },
1146 fulfillment_time => undef,
1147 cancel_time => undef,
1151 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1153 my $transit = $e->search_action_transit_copy(
1154 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1156 # find the most recent circulation for the requested copy,
1157 # be it active, completed, or aged.
1158 my $circ = $e->search_action_all_circulation_slim([
1159 { target_copy => $copy_id },
1165 'checkin_workstation',
1168 'recurring_fine_rule'
1171 order_by => { aacs => 'xact_start desc' },
1179 transit => $transit,
1189 __PACKAGE__->register_method(
1190 method => 'mark_item',
1191 api_name => 'open-ils.circ.mark_item_damaged',
1193 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1194 @param authtoken The login session key
1195 @param copy_id The ID of the copy to mark as damaged
1196 @return 1 on success - Event otherwise.
1199 __PACKAGE__->register_method(
1200 method => 'mark_item',
1201 api_name => 'open-ils.circ.mark_item_missing',
1203 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1204 @param authtoken The login session key
1205 @param copy_id The ID of the copy to mark as missing
1206 @return 1 on success - Event otherwise.
1209 __PACKAGE__->register_method(
1210 method => 'mark_item',
1211 api_name => 'open-ils.circ.mark_item_bindery',
1213 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1214 @param authtoken The login session key
1215 @param copy_id The ID of the copy to mark as bindery
1216 @return 1 on success - Event otherwise.
1219 __PACKAGE__->register_method(
1220 method => 'mark_item',
1221 api_name => 'open-ils.circ.mark_item_on_order',
1223 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1224 @param authtoken The login session key
1225 @param copy_id The ID of the copy to mark as on order
1226 @return 1 on success - Event otherwise.
1229 __PACKAGE__->register_method(
1230 method => 'mark_item',
1231 api_name => 'open-ils.circ.mark_item_ill',
1233 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1234 @param authtoken The login session key
1235 @param copy_id The ID of the copy to mark as inter-library loan
1236 @return 1 on success - Event otherwise.
1239 __PACKAGE__->register_method(
1240 method => 'mark_item',
1241 api_name => 'open-ils.circ.mark_item_cataloging',
1243 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1244 @param authtoken The login session key
1245 @param copy_id The ID of the copy to mark as cataloging
1246 @return 1 on success - Event otherwise.
1249 __PACKAGE__->register_method(
1250 method => 'mark_item',
1251 api_name => 'open-ils.circ.mark_item_reserves',
1253 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1254 @param authtoken The login session key
1255 @param copy_id The ID of the copy to mark as reserves
1256 @return 1 on success - Event otherwise.
1259 __PACKAGE__->register_method(
1260 method => 'mark_item',
1261 api_name => 'open-ils.circ.mark_item_discard',
1263 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1264 @param authtoken The login session key
1265 @param copy_id The ID of the copy to mark as discard
1266 @return 1 on success - Event otherwise.
1271 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1274 # Items must be checked in before any attempt is made to mark damaged
1275 my $evt = try_checkin($auth, $copy_id) if
1276 ($self->api_name=~ /damaged/ && $args->{handle_checkin});
1277 return $evt if $evt;
1279 my $e = new_editor(authtoken=>$auth, xact =>1);
1280 return $e->die_event unless $e->checkauth;
1281 my $copy = $e->retrieve_asset_copy([
1283 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1284 or return $e->die_event;
1287 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1288 $copy->circ_lib : $copy->call_number->owning_lib;
1290 my $perm = 'MARK_ITEM_MISSING';
1291 my $stat = OILS_COPY_STATUS_MISSING;
1293 if( $self->api_name =~ /damaged/ ) {
1294 $perm = 'MARK_ITEM_DAMAGED';
1295 $stat = OILS_COPY_STATUS_DAMAGED;
1296 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1297 return $evt if $evt;
1299 } elsif ( $self->api_name =~ /bindery/ ) {
1300 $perm = 'MARK_ITEM_BINDERY';
1301 $stat = OILS_COPY_STATUS_BINDERY;
1302 } elsif ( $self->api_name =~ /on_order/ ) {
1303 $perm = 'MARK_ITEM_ON_ORDER';
1304 $stat = OILS_COPY_STATUS_ON_ORDER;
1305 } elsif ( $self->api_name =~ /ill/ ) {
1306 $perm = 'MARK_ITEM_ILL';
1307 $stat = OILS_COPY_STATUS_ILL;
1308 } elsif ( $self->api_name =~ /cataloging/ ) {
1309 $perm = 'MARK_ITEM_CATALOGING';
1310 $stat = OILS_COPY_STATUS_CATALOGING;
1311 } elsif ( $self->api_name =~ /reserves/ ) {
1312 $perm = 'MARK_ITEM_RESERVES';
1313 $stat = OILS_COPY_STATUS_RESERVES;
1314 } elsif ( $self->api_name =~ /discard/ ) {
1315 $perm = 'MARK_ITEM_DISCARD';
1316 $stat = OILS_COPY_STATUS_DISCARD;
1319 # caller may proceed if either perm is allowed
1320 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1322 $copy->status($stat);
1323 $copy->edit_date('now');
1324 $copy->editor($e->requestor->id);
1326 $e->update_asset_copy($copy) or return $e->die_event;
1328 my $holds = $e->search_action_hold_request(
1330 current_copy => $copy->id,
1331 fulfillment_time => undef,
1332 cancel_time => undef,
1338 if( $self->api_name =~ /damaged/ ) {
1339 # now that we've committed the changes, create related A/T events
1340 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1341 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1344 $logger->debug("resetting holds that target the marked copy");
1345 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1351 my($auth, $copy_id) = @_;
1353 my $checkin = $U->simplereq(
1355 'open-ils.circ.checkin.override',
1357 copy_id => $copy_id,
1361 if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1363 my $evt_code = $checkin->[0]->{textcode};
1364 $logger->info("try_checkin() received event: $evt_code");
1366 if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1367 $logger->info('try_checkin() successful checkin');
1370 $logger->warn('try_checkin() un-successful checkin');
1375 sub handle_mark_damaged {
1376 my($e, $copy, $owning_lib, $args) = @_;
1378 my $apply = $args->{apply_fines} || '';
1379 return undef if $apply eq 'noapply';
1381 my $new_amount = $args->{override_amount};
1382 my $new_btype = $args->{override_btype};
1383 my $new_note = $args->{override_note};
1385 # grab the last circulation
1386 my $circ = $e->search_action_circulation([
1387 { target_copy => $copy->id},
1389 order_by => {circ => "xact_start DESC"},
1391 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1395 return undef unless $circ;
1397 my $charge_price = $U->ou_ancestor_setting_value(
1398 $owning_lib, 'circ.charge_on_damaged', $e);
1400 my $proc_fee = $U->ou_ancestor_setting_value(
1401 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1403 my $void_overdue = $U->ou_ancestor_setting_value(
1404 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1406 return undef unless $charge_price or $proc_fee;
1408 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1409 my $total = $copy_price + $proc_fee;
1413 if($new_amount and $new_btype) {
1415 # Allow staff to override the amount to charge for a damaged item
1416 # Consider the case where the item is only partially damaged
1417 # This value is meant to take the place of the item price and
1418 # optional processing fee.
1420 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1421 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1422 return $evt if $evt;
1426 if($charge_price and $copy_price) {
1427 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1428 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1429 return $evt if $evt;
1433 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1434 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1435 return $evt if $evt;
1439 # the assumption is that you would not void the overdues unless you
1440 # were also charging for the item and/or applying a processing fee
1442 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1443 return $evt if $evt;
1446 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1447 return $evt if $evt;
1449 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1450 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1452 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1453 return $evt2 if $evt2;
1456 return OpenILS::Event->new('DAMAGE_CHARGE',
1467 # ----------------------------------------------------------------------
1468 __PACKAGE__->register_method(
1469 method => 'mark_item_missing_pieces',
1470 api_name => 'open-ils.circ.mark_item_missing_pieces',
1472 Changes the status of a copy to "damaged" or to a custom status based on the
1473 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1475 @param authtoken The login session key
1476 @param copy_id The ID of the copy to mark as damaged
1477 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1481 sub mark_item_missing_pieces {
1482 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1483 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1484 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1486 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1487 return $e2->die_event unless $e2->checkauth;
1490 my $copy = $e2->retrieve_asset_copy([
1492 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1493 or return $e2->die_event;
1496 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1497 $copy->circ_lib : $copy->call_number->owning_lib;
1499 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1501 #### grab the last circulation
1502 my $circ = $e2->search_action_circulation([
1503 { target_copy => $copy->id},
1505 order_by => {circ => "xact_start DESC"}
1510 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1512 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1515 my $holds = $e2->search_action_hold_request(
1517 current_copy => $copy->id,
1518 fulfillment_time => undef,
1519 cancel_time => undef,
1523 $logger->debug("resetting holds that target the marked copy");
1524 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1527 if (! $e2->commit) {
1528 return $e2->die_event;
1531 my $e = new_editor(authtoken=>$auth, xact =>1);
1532 return $e->die_event unless $e->checkauth;
1534 if (! $circ->checkin_time) { # if circ active, attempt renew
1535 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1536 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1537 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1538 $circ = $res->[0]->{payload}{'circ'};
1539 $circ->target_copy( $copy->id );
1540 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1542 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1547 'copy_id'=>$circ->target_copy,
1548 'patron_id'=>$circ->usr,
1549 'skip_deposit_fee'=>1,
1550 'skip_rental_fee'=>1
1553 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1555 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1556 $e, $copy, $e->requestor, 1 );
1558 if ($hold) { # needed for hold? then due now
1560 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1561 my $due_date = DateTime->now(time_zone => 'local');
1562 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1564 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1568 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1569 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1570 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1571 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1572 $circ = $res->[0]->{payload}{'circ'};
1574 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1580 ### Update the item status
1582 my $custom_stat = $U->ou_ancestor_setting_value(
1583 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1584 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1586 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1587 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1589 $copy->status($stat);
1590 $copy->edit_date('now');
1591 $copy->editor($e->requestor->id);
1593 $e->update_asset_copy($copy) or return $e->die_event;
1597 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1598 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1600 return OpenILS::Event->new('SUCCESS',
1604 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1605 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1610 return $e->die_event;
1618 # ----------------------------------------------------------------------
1619 __PACKAGE__->register_method(
1620 method => 'magic_fetch',
1621 api_name => 'open-ils.agent.fetch'
1624 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1627 my( $self, $conn, $auth, $args ) = @_;
1628 my $e = new_editor( authtoken => $auth );
1629 return $e->event unless $e->checkauth;
1631 my $hint = $$args{hint};
1632 my $id = $$args{id};
1634 # Is the call allowed to fetch this type of object?
1635 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1637 # Find the class the implements the given hint
1638 my ($class) = grep {
1639 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1641 $class =~ s/Fieldmapper:://og;
1642 $class =~ s/::/_/og;
1643 my $method = "retrieve_$class";
1645 my $obj = $e->$method($id) or return $e->event;
1648 # ----------------------------------------------------------------------
1651 __PACKAGE__->register_method(
1652 method => "fleshed_circ_retrieve",
1654 api_name => "open-ils.circ.fleshed.retrieve",);
1656 sub fleshed_circ_retrieve {
1657 my( $self, $client, $id ) = @_;
1658 my $e = new_editor();
1659 my $circ = $e->retrieve_action_circulation(
1665 circ => [ qw/ target_copy / ],
1666 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1667 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1668 acn => [ qw/ record / ],
1672 ) or return $e->event;
1674 my $copy = $circ->target_copy;
1675 my $vol = $copy->call_number;
1676 my $rec = $circ->target_copy->call_number->record;
1678 $vol->record($rec->id);
1679 $copy->call_number($vol->id);
1680 $circ->target_copy($copy->id);
1684 if( $rec->id == OILS_PRECAT_RECORD ) {
1688 $mvr = $U->record_to_mvr($rec);
1689 $rec->marc(''); # drop the bulky marc data
1703 __PACKAGE__->register_method(
1704 method => "test_batch_circ_events",
1705 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1708 # method for testing the behavior of a given event definition
1709 sub test_batch_circ_events {
1710 my($self, $conn, $auth, $event_def, $barcode) = @_;
1712 my $e = new_editor(authtoken => $auth);
1713 return $e->event unless $e->checkauth;
1714 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1716 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1717 or return $e->event;
1719 my $circ = $e->search_action_circulation(
1720 {target_copy => $copy->id, checkin_time => undef})->[0]
1721 or return $e->event;
1723 return undef unless $circ;
1725 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1729 __PACKAGE__->register_method(
1730 method => "fire_circ_events",
1731 api_name => "open-ils.circ.fire_circ_trigger_events",
1733 General event def runner for circ objects. If no event def ID
1734 is provided, the hook will be used to find the best event_def
1735 match based on the context org unit
1739 __PACKAGE__->register_method(
1740 method => "fire_circ_events",
1741 api_name => "open-ils.circ.fire_hold_trigger_events",
1743 General event def runner for hold objects. If no event def ID
1744 is provided, the hook will be used to find the best event_def
1745 match based on the context org unit
1749 __PACKAGE__->register_method(
1750 method => "fire_circ_events",
1751 api_name => "open-ils.circ.fire_user_trigger_events",
1753 General event def runner for user objects. If no event def ID
1754 is provided, the hook will be used to find the best event_def
1755 match based on the context org unit
1760 sub fire_circ_events {
1761 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1763 my $e = new_editor(authtoken => $auth, xact => 1);
1764 return $e->event unless $e->checkauth;
1768 if($self->api_name =~ /hold/) {
1769 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1770 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1771 } elsif($self->api_name =~ /user/) {
1772 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1773 $targets = $e->batch_retrieve_actor_user($target_ids);
1775 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1776 $targets = $e->batch_retrieve_action_circulation($target_ids);
1778 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1779 # simply making this method authoritative because of weirdness
1780 # with transaction handling in A/T code that causes rollback
1781 # failure down the line if handling many targets
1783 return undef unless @$targets;
1784 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1787 __PACKAGE__->register_method(
1788 method => "user_payments_list",
1789 api_name => "open-ils.circ.user_payments.filtered.batch",
1792 desc => q/Returns a fleshed, date-limited set of all payments a user
1793 has made. By default, ordered by payment date. Optionally
1794 ordered by other columns in the top-level "mp" object/,
1796 {desc => 'Authentication token', type => 'string'},
1797 {desc => 'User ID', type => 'number'},
1798 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1800 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1801 and the related fully-realized payment object (e.g money.cash_payment)/}
1805 sub user_payments_list {
1806 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1808 my $e = new_editor(authtoken => $auth);
1809 return $e->event unless $e->checkauth;
1811 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1812 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1814 $order_by ||= ['payment_ts'];
1816 # all payments by user, between start_date and end_date
1817 my $payments = $e->json_query({
1818 select => {mp => ['id']},
1822 fkey => 'xact', field => 'id'}
1826 '+mbt' => {usr => $user_id},
1827 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1829 order_by => {mp => $order_by}
1832 for my $payment_id (@$payments) {
1833 my $payment = $e->retrieve_money_payment([
1841 'credit_card_payment',
1856 $conn->respond($payment);
1863 __PACKAGE__->register_method(
1864 method => "retrieve_circ_chain",
1865 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1868 desc => q/Given a circulation, this returns all circulation objects
1869 that are part of the same chain of renewals./,
1871 {desc => 'Authentication token', type => 'string'},
1872 {desc => 'Circ ID', type => 'number'},
1874 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1878 __PACKAGE__->register_method(
1879 method => "retrieve_circ_chain",
1880 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1882 desc => q/Given a circulation, this returns a summary of the circulation objects
1883 that are part of the same chain of renewals./,
1885 {desc => 'Authentication token', type => 'string'},
1886 {desc => 'Circ ID', type => 'number'},
1888 return => {desc => q/Circulation Chain Summary/}
1892 sub retrieve_circ_chain {
1893 my($self, $conn, $auth, $circ_id) = @_;
1895 my $e = new_editor(authtoken => $auth);
1896 return $e->event unless $e->checkauth;
1897 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1899 if($self->api_name =~ /summary/) {
1900 return $U->create_circ_chain_summary($e, $circ_id);
1904 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
1906 for my $circ_info (@$chain) {
1907 my $circ = Fieldmapper::action::all_circulation_slim->new;
1908 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1909 $conn->respond($circ);
1916 __PACKAGE__->register_method(
1917 method => "retrieve_prev_circ_chain",
1918 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1921 desc => q/Given a circulation, this returns all circulation objects
1922 that are part of the previous chain of renewals./,
1924 {desc => 'Authentication token', type => 'string'},
1925 {desc => 'Circ ID', type => 'number'},
1927 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1931 __PACKAGE__->register_method(
1932 method => "retrieve_prev_circ_chain",
1933 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1935 desc => q/Given a circulation, this returns a summary of the circulation objects
1936 that are part of the previous chain of renewals./,
1938 {desc => 'Authentication token', type => 'string'},
1939 {desc => 'Circ ID', type => 'number'},
1941 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1945 sub retrieve_prev_circ_chain {
1946 my($self, $conn, $auth, $circ_id) = @_;
1948 my $e = new_editor(authtoken => $auth);
1949 return $e->event unless $e->checkauth;
1950 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1953 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
1955 my $prev_circ = $e->search_action_all_circulation_slim([
1956 { target_copy => $first_circ->{target_copy},
1957 xact_start => {'<' => $first_circ->{xact_start}}
1966 order_by => { aacs => 'xact_start desc' },
1971 return undef unless $prev_circ;
1973 my $chain_usr = $prev_circ->usr; # note: may be undef
1975 if ($self->api_name =~ /summary/) {
1976 my $sum = $e->json_query({
1978 'action.summarize_all_circ_chain',
1983 my $summary = Fieldmapper::action::circ_chain_summary->new;
1984 $summary->$_($sum->{$_}) for keys %$sum;
1986 return {summary => $summary, usr => $chain_usr};
1990 my $chain = $e->json_query(
1991 {from => ['action.all_circ_chain', $prev_circ->id]});
1993 for my $circ_info (@$chain) {
1994 my $circ = Fieldmapper::action::all_circulation_slim->new;
1995 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1996 $conn->respond($circ);
2003 __PACKAGE__->register_method(
2004 method => "get_copy_due_date",
2005 api_name => "open-ils.circ.copy.due_date.retrieve",
2008 Given a copy ID, returns the due date for the copy if it's
2009 currently circulating. Otherwise, returns null. Note, this is a public
2010 method requiring no authentication. Only the due date is exposed.
2013 {desc => 'Copy ID', type => 'number'}
2015 return => {desc => q/
2016 Due date (ISO date stamp) if the copy is circulating, null otherwise.
2021 sub get_copy_due_date {
2022 my($self, $conn, $copy_id) = @_;
2023 my $e = new_editor();
2025 my $circ = $e->json_query({
2026 select => {circ => ['due_date']},
2029 target_copy => $copy_id,
2030 checkin_time => undef,
2032 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2033 {stop_fines => undef}
2037 })->[0] or return undef;
2039 return $circ->{due_date};
2046 # {"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}}