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 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' :
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(
546 __PACKAGE__->register_method(
547 method => "post_checkin_backdate_circ",
548 api_name => "open-ils.circ.post_checkin_backdate",
550 desc => q/Back-date an already checked in circulation/,
552 {desc => 'Authentication token', type => 'string'},
553 {desc => 'Circ ID', type => 'number'},
554 {desc => 'ISO8601 backdate', type => 'string'},
556 return => {desc => q/1 on success, failure event on error/}
560 __PACKAGE__->register_method(
561 method => "post_checkin_backdate_circ",
562 api_name => "open-ils.circ.post_checkin_backdate.batch",
565 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
567 {desc => 'Authentication token', type => 'string'},
568 {desc => 'List of Circ ID', type => 'array'},
569 {desc => 'ISO8601 backdate', type => 'string'},
571 return => {desc => q/Set of: 1 on success, failure event on error/}
576 sub post_checkin_backdate_circ {
577 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
578 my $e = new_editor(authtoken=>$auth);
579 return $e->die_event unless $e->checkauth;
580 if($self->api_name =~ /batch/) {
581 foreach my $c (@$circ_id) {
582 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
585 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
593 sub post_checkin_backdate_circ_impl {
594 my($e, $circ_id, $backdate) = @_;
598 my $circ = $e->retrieve_action_circulation($circ_id)
599 or return $e->die_event;
601 # anyone with checkin perms can backdate (more restrictive?)
602 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
604 # don't allow back-dating an open circulation
605 return OpenILS::Event->new('BAD_PARAMS') unless
606 $backdate and $circ->checkin_time;
608 # update the checkin and stop_fines times to reflect the new backdate
609 $circ->stop_fines_time(cleanse_ISO8601($backdate));
610 $circ->checkin_time(cleanse_ISO8601($backdate));
611 $e->update_action_circulation($circ) or return $e->die_event;
613 # now void the overdues "erased" by the back-dating
614 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
617 # If the circ was closed before and the balance owned !=0, re-open the transaction
618 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
627 __PACKAGE__->register_method (
628 method => 'set_circ_due_date',
629 api_name => 'open-ils.circ.circulation.due_date.update',
631 Updates the due_date on the given circ
633 @param circid The id of the circ to update
634 @param date The timestamp of the new due date
638 sub set_circ_due_date {
639 my( $self, $conn, $auth, $circ_id, $date ) = @_;
641 my $e = new_editor(xact=>1, authtoken=>$auth);
642 return $e->die_event unless $e->checkauth;
643 my $circ = $e->retrieve_action_circulation($circ_id)
644 or return $e->die_event;
646 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
647 $date = cleanse_ISO8601($date);
649 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
650 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
651 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
652 $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
655 $circ->due_date($date);
656 $e->update_action_circulation($circ) or return $e->die_event;
663 __PACKAGE__->register_method(
664 method => "create_in_house_use",
665 api_name => 'open-ils.circ.in_house_use.create',
667 Creates an in-house use action.
668 @param $authtoken The login session key
669 @param params A hash of params including
670 'location' The org unit id where the in-house use occurs
671 'copyid' The copy in question
672 'count' The number of in-house uses to apply to this copy
673 @return An array of id's representing the id's of the newly created
674 in-house use objects or an event on an error
677 __PACKAGE__->register_method(
678 method => "create_in_house_use",
679 api_name => 'open-ils.circ.non_cat_in_house_use.create',
683 sub create_in_house_use {
684 my( $self, $client, $auth, $params ) = @_;
687 my $org = $params->{location};
688 my $copyid = $params->{copyid};
689 my $count = $params->{count} || 1;
690 my $nc_type = $params->{non_cat_type};
691 my $use_time = $params->{use_time} || 'now';
693 my $e = new_editor(xact=>1,authtoken=>$auth);
694 return $e->event unless $e->checkauth;
695 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
697 my $non_cat = 1 if $self->api_name =~ /non_cat/;
701 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
703 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
709 if( $use_time ne 'now' ) {
710 $use_time = cleanse_ISO8601($use_time);
711 $logger->debug("in_house_use setting use time to $use_time");
722 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
723 $ihu->item_type($nc_type);
724 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
725 $cmeth = "create_action_non_cat_in_house_use";
728 $ihu = Fieldmapper::action::in_house_use->new;
730 $method = 'open-ils.storage.direct.action.in_house_use.create';
731 $cmeth = "create_action_in_house_use";
734 $ihu->staff($e->requestor->id);
735 $ihu->org_unit($org);
736 $ihu->use_time($use_time);
738 $ihu = $e->$cmeth($ihu) or return $e->event;
739 push( @ids, $ihu->id );
750 __PACKAGE__->register_method(
751 method => "view_circs",
752 api_name => "open-ils.circ.copy_checkout_history.retrieve",
754 Retrieves the last X circs for a given copy
755 @param authtoken The login session key
756 @param copyid The copy to check
757 @param count How far to go back in the item history
758 @return An array of circ ids
761 # ----------------------------------------------------------------------
762 # Returns $count most recent circs. If count exceeds the configured
763 # max, use the configured max instead
764 # ----------------------------------------------------------------------
766 my( $self, $client, $authtoken, $copyid, $count ) = @_;
768 my $e = new_editor(authtoken => $authtoken);
769 return $e->event unless $e->checkauth;
771 my $copy = $e->retrieve_asset_copy([
774 flesh_fields => {acp => ['call_number']}
776 ]) or return $e->event;
778 return $e->event unless $e->allowed(
779 'VIEW_COPY_CHECKOUT_HISTORY',
780 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
781 $copy->circ_lib : $copy->call_number->owning_lib);
783 my $max_history = $U->ou_ancestor_setting_value(
784 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
786 if(defined $max_history) {
787 $count = $max_history unless defined $count and $count < $max_history;
789 $count = 4 unless defined $count;
792 return $e->search_action_all_circulation([
793 {target_copy => $copyid},
794 {limit => $count, order_by => { combcirc => "xact_start DESC" }}
799 __PACKAGE__->register_method(
800 method => "circ_count",
801 api_name => "open-ils.circ.circulation.count",
803 Returns the number of times the item has circulated
804 @param copyid The copy to check
808 my( $self, $client, $copyid ) = @_;
810 my $count = new_editor()->json_query({
819 where => {'+circbyyr' => {copy => $copyid}}
831 __PACKAGE__->register_method(
832 method => 'fetch_notes',
834 api_name => 'open-ils.circ.copy_note.retrieve.all',
836 Returns an array of copy note objects.
837 @param args A named hash of parameters including:
838 authtoken : Required if viewing non-public notes
839 itemid : The id of the item whose notes we want to retrieve
840 pub : True if all the caller wants are public notes
841 @return An array of note objects
844 __PACKAGE__->register_method(
845 method => 'fetch_notes',
846 api_name => 'open-ils.circ.call_number_note.retrieve.all',
847 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
849 __PACKAGE__->register_method(
850 method => 'fetch_notes',
851 api_name => 'open-ils.circ.title_note.retrieve.all',
852 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
855 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
857 my( $self, $connection, $args ) = @_;
859 my $id = $$args{itemid};
860 my $authtoken = $$args{authtoken};
863 if( $self->api_name =~ /copy/ ) {
865 return $U->cstorereq(
866 'open-ils.cstore.direct.asset.copy_note.search.atomic',
867 { owning_copy => $id, pub => 't' } );
869 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
871 return $U->cstorereq(
872 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
875 } elsif( $self->api_name =~ /call_number/ ) {
877 return $U->cstorereq(
878 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
879 { call_number => $id, pub => 't' } );
881 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
883 return $U->cstorereq(
884 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
887 } elsif( $self->api_name =~ /title/ ) {
889 return $U->cstorereq(
890 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
891 { record => $id, pub => 't' } );
893 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
895 return $U->cstorereq(
896 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
903 __PACKAGE__->register_method(
904 method => 'has_notes',
905 api_name => 'open-ils.circ.copy.has_notes');
906 __PACKAGE__->register_method(
907 method => 'has_notes',
908 api_name => 'open-ils.circ.call_number.has_notes');
909 __PACKAGE__->register_method(
910 method => 'has_notes',
911 api_name => 'open-ils.circ.title.has_notes');
915 my( $self, $conn, $authtoken, $id ) = @_;
916 my $editor = new_editor(authtoken => $authtoken);
917 return $editor->event unless $editor->checkauth;
919 my $n = $editor->search_asset_copy_note(
920 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
922 $n = $editor->search_asset_call_number_note(
923 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
925 $n = $editor->search_biblio_record_note(
926 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
933 __PACKAGE__->register_method(
934 method => 'create_copy_note',
935 api_name => 'open-ils.circ.copy_note.create',
937 Creates a new copy note
938 @param authtoken The login session key
939 @param note The note object to create
940 @return The id of the new note object
943 sub create_copy_note {
944 my( $self, $connection, $authtoken, $note ) = @_;
946 my $e = new_editor(xact=>1, authtoken=>$authtoken);
947 return $e->event unless $e->checkauth;
948 my $copy = $e->retrieve_asset_copy(
952 flesh_fields => { 'acp' => ['call_number'] }
957 return $e->event unless
958 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
960 $note->create_date('now');
961 $note->creator($e->requestor->id);
962 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
965 $e->create_asset_copy_note($note) or return $e->event;
971 __PACKAGE__->register_method(
972 method => 'delete_copy_note',
973 api_name => 'open-ils.circ.copy_note.delete',
975 Deletes an existing copy note
976 @param authtoken The login session key
977 @param noteid The id of the note to delete
978 @return 1 on success - Event otherwise.
980 sub delete_copy_note {
981 my( $self, $conn, $authtoken, $noteid ) = @_;
983 my $e = new_editor(xact=>1, authtoken=>$authtoken);
984 return $e->die_event unless $e->checkauth;
986 my $note = $e->retrieve_asset_copy_note([
990 'acpn' => [ 'owning_copy' ],
991 'acp' => [ 'call_number' ],
994 ]) or return $e->die_event;
996 if( $note->creator ne $e->requestor->id ) {
997 return $e->die_event unless
998 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1001 $e->delete_asset_copy_note($note) or return $e->die_event;
1006 __PACKAGE__->register_method(
1007 method => 'fetch_copy_tags',
1009 api_name => 'open-ils.circ.copy_tags.retrieve',
1011 Returns an array of publicly-visible copy tag objects.
1012 @param args A named hash of parameters including:
1013 copy_id : The id of the item whose notes we want to retrieve
1014 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1015 scope : top of org subtree whose copy tags we want to see
1016 depth : how far down to look for copy tags (optional)
1017 @return An array of copy tag objects
1019 __PACKAGE__->register_method(
1020 method => 'fetch_copy_tags',
1022 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1024 Returns an array of all copy tag objects.
1025 @param args A named hash of parameters including:
1026 authtoken : Required to view non-public notes
1027 copy_id : The id of the item whose notes we want to retrieve (optional)
1028 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1029 scope : top of org subtree whose copy tags we want to see
1030 depth : how far down to look for copy tags (optional)
1031 @return An array of copy tag objects
1034 sub fetch_copy_tags {
1035 my ($self, $conn, $args) = @_;
1037 my $org = $args->{scope};
1038 my $depth = $args->{depth};
1042 if ($self->api_name =~ /\.staff/) {
1043 my $authtoken = $args->{authtoken};
1044 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1045 $e = new_editor(authtoken => $args->{authtoken});
1046 return $e->event unless $e->checkauth;
1047 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1050 $filter->{pub} = 't';
1052 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1053 $filter->{'+acptcm'} = {
1054 copy => $args->{copy_id}
1057 # filter by owner of copy tag and depth
1058 $filter->{owner} = {
1060 select => {aou => [{
1062 transform => 'actor.org_unit_descendants',
1063 result_field => 'id',
1064 (defined($depth) ? ( params => [$depth] ) : ()),
1067 where => {id => $org}
1071 return $e->search_asset_copy_tag([$filter, { join => { acptcm => {} } }]);
1075 __PACKAGE__->register_method(
1076 method => 'age_hold_rules',
1077 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1080 sub age_hold_rules {
1081 my( $self, $conn ) = @_;
1082 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1087 __PACKAGE__->register_method(
1088 method => 'copy_details_barcode',
1090 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1091 sub copy_details_barcode {
1092 my( $self, $conn, $auth, $barcode ) = @_;
1093 my $e = new_editor();
1094 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1095 return $e->event unless $cid;
1096 return copy_details( $self, $conn, $auth, $cid );
1100 __PACKAGE__->register_method(
1101 method => 'copy_details',
1102 api_name => 'open-ils.circ.copy_details.retrieve');
1105 my( $self, $conn, $auth, $copy_id ) = @_;
1106 my $e = new_editor(authtoken=>$auth);
1107 return $e->event unless $e->checkauth;
1109 my $flesh = { flesh => 1 };
1111 my $copy = $e->retrieve_asset_copy(
1117 acp => ['call_number','parts','peer_record_maps','floating'],
1118 acn => ['record','prefix','suffix','label_class']
1121 ]) or return $e->event;
1124 # De-flesh the copy for backwards compatibility
1126 my $vol = $copy->call_number;
1128 $copy->call_number($vol->id);
1129 my $record = $vol->record;
1131 $vol->record($record->id);
1132 $mvr = $U->record_to_mvr($record);
1137 my $hold = $e->search_action_hold_request(
1139 current_copy => $copy_id,
1140 capture_time => { "!=" => undef },
1141 fulfillment_time => undef,
1142 cancel_time => undef,
1146 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1148 my $transit = $e->search_action_transit_copy(
1149 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1151 # find the most recent circulation for the requested copy,
1152 # be it active, completed, or aged.
1153 my $circ = $e->search_action_all_circulation([
1154 { target_copy => $copy_id },
1160 'checkin_workstation',
1163 'recurring_fine_rule'
1166 order_by => { combcirc => 'xact_start desc' },
1174 transit => $transit,
1184 __PACKAGE__->register_method(
1185 method => 'mark_item',
1186 api_name => 'open-ils.circ.mark_item_damaged',
1188 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1189 @param authtoken The login session key
1190 @param copy_id The ID of the copy to mark as damaged
1191 @return 1 on success - Event otherwise.
1194 __PACKAGE__->register_method(
1195 method => 'mark_item',
1196 api_name => 'open-ils.circ.mark_item_missing',
1198 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1199 @param authtoken The login session key
1200 @param copy_id The ID of the copy to mark as missing
1201 @return 1 on success - Event otherwise.
1204 __PACKAGE__->register_method(
1205 method => 'mark_item',
1206 api_name => 'open-ils.circ.mark_item_bindery',
1208 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1209 @param authtoken The login session key
1210 @param copy_id The ID of the copy to mark as bindery
1211 @return 1 on success - Event otherwise.
1214 __PACKAGE__->register_method(
1215 method => 'mark_item',
1216 api_name => 'open-ils.circ.mark_item_on_order',
1218 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1219 @param authtoken The login session key
1220 @param copy_id The ID of the copy to mark as on order
1221 @return 1 on success - Event otherwise.
1224 __PACKAGE__->register_method(
1225 method => 'mark_item',
1226 api_name => 'open-ils.circ.mark_item_ill',
1228 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1229 @param authtoken The login session key
1230 @param copy_id The ID of the copy to mark as inter-library loan
1231 @return 1 on success - Event otherwise.
1234 __PACKAGE__->register_method(
1235 method => 'mark_item',
1236 api_name => 'open-ils.circ.mark_item_cataloging',
1238 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1239 @param authtoken The login session key
1240 @param copy_id The ID of the copy to mark as cataloging
1241 @return 1 on success - Event otherwise.
1244 __PACKAGE__->register_method(
1245 method => 'mark_item',
1246 api_name => 'open-ils.circ.mark_item_reserves',
1248 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1249 @param authtoken The login session key
1250 @param copy_id The ID of the copy to mark as reserves
1251 @return 1 on success - Event otherwise.
1254 __PACKAGE__->register_method(
1255 method => 'mark_item',
1256 api_name => 'open-ils.circ.mark_item_discard',
1258 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1259 @param authtoken The login session key
1260 @param copy_id The ID of the copy to mark as discard
1261 @return 1 on success - Event otherwise.
1266 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1267 my $e = new_editor(authtoken=>$auth, xact =>1);
1268 return $e->die_event unless $e->checkauth;
1271 my $copy = $e->retrieve_asset_copy([
1273 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1274 or return $e->die_event;
1277 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1278 $copy->circ_lib : $copy->call_number->owning_lib;
1280 my $perm = 'MARK_ITEM_MISSING';
1281 my $stat = OILS_COPY_STATUS_MISSING;
1283 if( $self->api_name =~ /damaged/ ) {
1284 $perm = 'MARK_ITEM_DAMAGED';
1285 $stat = OILS_COPY_STATUS_DAMAGED;
1286 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1287 return $evt if $evt;
1289 } elsif ( $self->api_name =~ /bindery/ ) {
1290 $perm = 'MARK_ITEM_BINDERY';
1291 $stat = OILS_COPY_STATUS_BINDERY;
1292 } elsif ( $self->api_name =~ /on_order/ ) {
1293 $perm = 'MARK_ITEM_ON_ORDER';
1294 $stat = OILS_COPY_STATUS_ON_ORDER;
1295 } elsif ( $self->api_name =~ /ill/ ) {
1296 $perm = 'MARK_ITEM_ILL';
1297 $stat = OILS_COPY_STATUS_ILL;
1298 } elsif ( $self->api_name =~ /cataloging/ ) {
1299 $perm = 'MARK_ITEM_CATALOGING';
1300 $stat = OILS_COPY_STATUS_CATALOGING;
1301 } elsif ( $self->api_name =~ /reserves/ ) {
1302 $perm = 'MARK_ITEM_RESERVES';
1303 $stat = OILS_COPY_STATUS_RESERVES;
1304 } elsif ( $self->api_name =~ /discard/ ) {
1305 $perm = 'MARK_ITEM_DISCARD';
1306 $stat = OILS_COPY_STATUS_DISCARD;
1309 # caller may proceed if either perm is allowed
1310 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1312 $copy->status($stat);
1313 $copy->edit_date('now');
1314 $copy->editor($e->requestor->id);
1316 $e->update_asset_copy($copy) or return $e->die_event;
1318 my $holds = $e->search_action_hold_request(
1320 current_copy => $copy->id,
1321 fulfillment_time => undef,
1322 cancel_time => undef,
1328 if( $self->api_name =~ /damaged/ ) {
1329 # now that we've committed the changes, create related A/T events
1330 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1331 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1334 $logger->debug("resetting holds that target the marked copy");
1335 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1340 sub handle_mark_damaged {
1341 my($e, $copy, $owning_lib, $args) = @_;
1343 my $apply = $args->{apply_fines} || '';
1344 return undef if $apply eq 'noapply';
1346 my $new_amount = $args->{override_amount};
1347 my $new_btype = $args->{override_btype};
1348 my $new_note = $args->{override_note};
1350 # grab the last circulation
1351 my $circ = $e->search_action_circulation([
1352 { target_copy => $copy->id},
1354 order_by => {circ => "xact_start DESC"},
1356 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1360 return undef unless $circ;
1362 my $charge_price = $U->ou_ancestor_setting_value(
1363 $owning_lib, 'circ.charge_on_damaged', $e);
1365 my $proc_fee = $U->ou_ancestor_setting_value(
1366 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1368 my $void_overdue = $U->ou_ancestor_setting_value(
1369 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1371 return undef unless $charge_price or $proc_fee;
1373 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1374 my $total = $copy_price + $proc_fee;
1378 if($new_amount and $new_btype) {
1380 # Allow staff to override the amount to charge for a damaged item
1381 # Consider the case where the item is only partially damaged
1382 # This value is meant to take the place of the item price and
1383 # optional processing fee.
1385 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1386 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1387 return $evt if $evt;
1391 if($charge_price and $copy_price) {
1392 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1393 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1394 return $evt if $evt;
1398 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1399 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1400 return $evt if $evt;
1404 # the assumption is that you would not void the overdues unless you
1405 # were also charging for the item and/or applying a processing fee
1407 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1408 return $evt if $evt;
1411 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1412 return $evt if $evt;
1414 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1415 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1417 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1418 return $evt2 if $evt2;
1423 return OpenILS::Event->new('DAMAGE_CHARGE',
1434 # ----------------------------------------------------------------------
1435 __PACKAGE__->register_method(
1436 method => 'mark_item_missing_pieces',
1437 api_name => 'open-ils.circ.mark_item_missing_pieces',
1439 Changes the status of a copy to "damaged" or to a custom status based on the
1440 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1442 @param authtoken The login session key
1443 @param copy_id The ID of the copy to mark as damaged
1444 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1448 sub mark_item_missing_pieces {
1449 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1450 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1451 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1453 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1454 return $e2->die_event unless $e2->checkauth;
1457 my $copy = $e2->retrieve_asset_copy([
1459 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1460 or return $e2->die_event;
1463 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1464 $copy->circ_lib : $copy->call_number->owning_lib;
1466 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1468 #### grab the last circulation
1469 my $circ = $e2->search_action_circulation([
1470 { target_copy => $copy->id},
1472 order_by => {circ => "xact_start DESC"}
1477 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1479 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1482 my $holds = $e2->search_action_hold_request(
1484 current_copy => $copy->id,
1485 fulfillment_time => undef,
1486 cancel_time => undef,
1490 $logger->debug("resetting holds that target the marked copy");
1491 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1494 if (! $e2->commit) {
1495 return $e2->die_event;
1498 my $e = new_editor(authtoken=>$auth, xact =>1);
1499 return $e->die_event unless $e->checkauth;
1501 if (! $circ->checkin_time) { # if circ active, attempt renew
1502 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1503 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1504 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1505 $circ = $res->[0]->{payload}{'circ'};
1506 $circ->target_copy( $copy->id );
1507 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1509 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1514 'copy_id'=>$circ->target_copy,
1515 'patron_id'=>$circ->usr,
1516 'skip_deposit_fee'=>1,
1517 'skip_rental_fee'=>1
1520 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1522 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1523 $e, $copy, $e->requestor, 1 );
1525 if ($hold) { # needed for hold? then due now
1527 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1528 my $due_date = DateTime->now(time_zone => 'local');
1529 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1531 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1535 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1536 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1537 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1538 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1539 $circ = $res->[0]->{payload}{'circ'};
1541 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1547 ### Update the item status
1549 my $custom_stat = $U->ou_ancestor_setting_value(
1550 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1551 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1553 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1554 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1556 $copy->status($stat);
1557 $copy->edit_date('now');
1558 $copy->editor($e->requestor->id);
1560 $e->update_asset_copy($copy) or return $e->die_event;
1564 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1565 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1567 return OpenILS::Event->new('SUCCESS',
1571 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1572 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1577 return $e->die_event;
1585 # ----------------------------------------------------------------------
1586 __PACKAGE__->register_method(
1587 method => 'magic_fetch',
1588 api_name => 'open-ils.agent.fetch'
1591 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1594 my( $self, $conn, $auth, $args ) = @_;
1595 my $e = new_editor( authtoken => $auth );
1596 return $e->event unless $e->checkauth;
1598 my $hint = $$args{hint};
1599 my $id = $$args{id};
1601 # Is the call allowed to fetch this type of object?
1602 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1604 # Find the class the implements the given hint
1605 my ($class) = grep {
1606 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1608 $class =~ s/Fieldmapper:://og;
1609 $class =~ s/::/_/og;
1610 my $method = "retrieve_$class";
1612 my $obj = $e->$method($id) or return $e->event;
1615 # ----------------------------------------------------------------------
1618 __PACKAGE__->register_method(
1619 method => "fleshed_circ_retrieve",
1621 api_name => "open-ils.circ.fleshed.retrieve",);
1623 sub fleshed_circ_retrieve {
1624 my( $self, $client, $id ) = @_;
1625 my $e = new_editor();
1626 my $circ = $e->retrieve_action_circulation(
1632 circ => [ qw/ target_copy / ],
1633 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1634 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1635 acn => [ qw/ record / ],
1639 ) or return $e->event;
1641 my $copy = $circ->target_copy;
1642 my $vol = $copy->call_number;
1643 my $rec = $circ->target_copy->call_number->record;
1645 $vol->record($rec->id);
1646 $copy->call_number($vol->id);
1647 $circ->target_copy($copy->id);
1651 if( $rec->id == OILS_PRECAT_RECORD ) {
1655 $mvr = $U->record_to_mvr($rec);
1656 $rec->marc(''); # drop the bulky marc data
1670 __PACKAGE__->register_method(
1671 method => "test_batch_circ_events",
1672 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1675 # method for testing the behavior of a given event definition
1676 sub test_batch_circ_events {
1677 my($self, $conn, $auth, $event_def, $barcode) = @_;
1679 my $e = new_editor(authtoken => $auth);
1680 return $e->event unless $e->checkauth;
1681 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1683 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1684 or return $e->event;
1686 my $circ = $e->search_action_circulation(
1687 {target_copy => $copy->id, checkin_time => undef})->[0]
1688 or return $e->event;
1690 return undef unless $circ;
1692 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1696 __PACKAGE__->register_method(
1697 method => "fire_circ_events",
1698 api_name => "open-ils.circ.fire_circ_trigger_events",
1700 General event def runner for circ objects. If no event def ID
1701 is provided, the hook will be used to find the best event_def
1702 match based on the context org unit
1706 __PACKAGE__->register_method(
1707 method => "fire_circ_events",
1708 api_name => "open-ils.circ.fire_hold_trigger_events",
1710 General event def runner for hold objects. If no event def ID
1711 is provided, the hook will be used to find the best event_def
1712 match based on the context org unit
1716 __PACKAGE__->register_method(
1717 method => "fire_circ_events",
1718 api_name => "open-ils.circ.fire_user_trigger_events",
1720 General event def runner for user objects. If no event def ID
1721 is provided, the hook will be used to find the best event_def
1722 match based on the context org unit
1727 sub fire_circ_events {
1728 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1730 my $e = new_editor(authtoken => $auth, xact => 1);
1731 return $e->event unless $e->checkauth;
1735 if($self->api_name =~ /hold/) {
1736 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1737 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1738 } elsif($self->api_name =~ /user/) {
1739 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1740 $targets = $e->batch_retrieve_actor_user($target_ids);
1742 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1743 $targets = $e->batch_retrieve_action_circulation($target_ids);
1745 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1746 # simply making this method authoritative because of weirdness
1747 # with transaction handling in A/T code that causes rollback
1748 # failure down the line if handling many targets
1750 return undef unless @$targets;
1751 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1754 __PACKAGE__->register_method(
1755 method => "user_payments_list",
1756 api_name => "open-ils.circ.user_payments.filtered.batch",
1759 desc => q/Returns a fleshed, date-limited set of all payments a user
1760 has made. By default, ordered by payment date. Optionally
1761 ordered by other columns in the top-level "mp" object/,
1763 {desc => 'Authentication token', type => 'string'},
1764 {desc => 'User ID', type => 'number'},
1765 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1767 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1768 and the related fully-realized payment object (e.g money.cash_payment)/}
1772 sub user_payments_list {
1773 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1775 my $e = new_editor(authtoken => $auth);
1776 return $e->event unless $e->checkauth;
1778 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1779 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1781 $order_by ||= ['payment_ts'];
1783 # all payments by user, between start_date and end_date
1784 my $payments = $e->json_query({
1785 select => {mp => ['id']},
1789 fkey => 'xact', field => 'id'}
1793 '+mbt' => {usr => $user_id},
1794 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1796 order_by => {mp => $order_by}
1799 for my $payment_id (@$payments) {
1800 my $payment = $e->retrieve_money_payment([
1808 'credit_card_payment',
1823 $conn->respond($payment);
1830 __PACKAGE__->register_method(
1831 method => "retrieve_circ_chain",
1832 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1835 desc => q/Given a circulation, this returns all circulation objects
1836 that are part of the same chain of renewals./,
1838 {desc => 'Authentication token', type => 'string'},
1839 {desc => 'Circ ID', type => 'number'},
1841 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1845 __PACKAGE__->register_method(
1846 method => "retrieve_circ_chain",
1847 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1849 desc => q/Given a circulation, this returns a summary of the circulation objects
1850 that are part of the same chain of renewals./,
1852 {desc => 'Authentication token', type => 'string'},
1853 {desc => 'Circ ID', type => 'number'},
1855 return => {desc => q/Circulation Chain Summary/}
1859 sub retrieve_circ_chain {
1860 my($self, $conn, $auth, $circ_id) = @_;
1862 my $e = new_editor(authtoken => $auth);
1863 return $e->event unless $e->checkauth;
1864 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1866 if($self->api_name =~ /summary/) {
1867 return $U->create_circ_chain_summary($e, $circ_id);
1871 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
1873 for my $circ_info (@$chain) {
1874 my $circ = Fieldmapper::action::all_circulation->new;
1875 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1876 $conn->respond($circ);
1883 __PACKAGE__->register_method(
1884 method => "retrieve_prev_circ_chain",
1885 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1888 desc => q/Given a circulation, this returns all circulation objects
1889 that are part of the previous chain of renewals./,
1891 {desc => 'Authentication token', type => 'string'},
1892 {desc => 'Circ ID', type => 'number'},
1894 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1898 __PACKAGE__->register_method(
1899 method => "retrieve_prev_circ_chain",
1900 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1902 desc => q/Given a circulation, this returns a summary of the circulation objects
1903 that are part of the previous chain of renewals./,
1905 {desc => 'Authentication token', type => 'string'},
1906 {desc => 'Circ ID', type => 'number'},
1908 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1912 sub retrieve_prev_circ_chain {
1913 my($self, $conn, $auth, $circ_id) = @_;
1915 my $e = new_editor(authtoken => $auth);
1916 return $e->event unless $e->checkauth;
1917 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1920 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
1922 my $prev_circ = $e->search_action_all_circulation([
1923 { target_copy => $first_circ->{target_copy},
1924 xact_start => {'<' => $first_circ->{xact_start}}
1933 order_by => { combcirc => 'xact_start desc' },
1938 return undef unless $prev_circ;
1940 my $chain_usr = $prev_circ->usr; # note: may be undef
1942 if ($self->api_name =~ /summary/) {
1943 my $sum = $e->json_query({
1945 'action.summarize_all_circ_chain',
1950 my $summary = Fieldmapper::action::circ_chain_summary->new;
1951 $summary->$_($sum->{$_}) for keys %$sum;
1953 return {summary => $summary, usr => $chain_usr};
1957 my $chain = $e->json_query(
1958 {from => ['action.all_circ_chain', $prev_circ->id]});
1960 for my $circ_info (@$chain) {
1961 my $circ = Fieldmapper::action::all_circulation->new;
1962 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1963 $conn->respond($circ);
1970 __PACKAGE__->register_method(
1971 method => "get_copy_due_date",
1972 api_name => "open-ils.circ.copy.due_date.retrieve",
1975 Given a copy ID, returns the due date for the copy if it's
1976 currently circulating. Otherwise, returns null. Note, this is a public
1977 method requiring no authentication. Only the due date is exposed.
1980 {desc => 'Copy ID', type => 'number'}
1982 return => {desc => q/
1983 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1988 sub get_copy_due_date {
1989 my($self, $conn, $copy_id) = @_;
1990 my $e = new_editor();
1992 my $circ = $e->json_query({
1993 select => {circ => ['due_date']},
1996 target_copy => $copy_id,
1997 checkin_time => undef,
1999 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2000 {stop_fines => undef}
2004 })->[0] or return undef;
2006 return $circ->{due_date};
2013 # {"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}}