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(
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([
798 {target_copy => $copyid},
799 {limit => $count, order_by => { combcirc => "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([
1159 { target_copy => $copy_id },
1165 'checkin_workstation',
1168 'recurring_fine_rule'
1171 order_by => { combcirc => '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 ) = @_;
1272 my $e = new_editor(authtoken=>$auth, xact =>1);
1273 return $e->die_event unless $e->checkauth;
1276 my $copy = $e->retrieve_asset_copy([
1278 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1279 or return $e->die_event;
1282 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1283 $copy->circ_lib : $copy->call_number->owning_lib;
1285 my $perm = 'MARK_ITEM_MISSING';
1286 my $stat = OILS_COPY_STATUS_MISSING;
1288 if( $self->api_name =~ /damaged/ ) {
1289 $perm = 'MARK_ITEM_DAMAGED';
1290 $stat = OILS_COPY_STATUS_DAMAGED;
1291 my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1292 return $evt if $evt;
1294 } elsif ( $self->api_name =~ /bindery/ ) {
1295 $perm = 'MARK_ITEM_BINDERY';
1296 $stat = OILS_COPY_STATUS_BINDERY;
1297 } elsif ( $self->api_name =~ /on_order/ ) {
1298 $perm = 'MARK_ITEM_ON_ORDER';
1299 $stat = OILS_COPY_STATUS_ON_ORDER;
1300 } elsif ( $self->api_name =~ /ill/ ) {
1301 $perm = 'MARK_ITEM_ILL';
1302 $stat = OILS_COPY_STATUS_ILL;
1303 } elsif ( $self->api_name =~ /cataloging/ ) {
1304 $perm = 'MARK_ITEM_CATALOGING';
1305 $stat = OILS_COPY_STATUS_CATALOGING;
1306 } elsif ( $self->api_name =~ /reserves/ ) {
1307 $perm = 'MARK_ITEM_RESERVES';
1308 $stat = OILS_COPY_STATUS_RESERVES;
1309 } elsif ( $self->api_name =~ /discard/ ) {
1310 $perm = 'MARK_ITEM_DISCARD';
1311 $stat = OILS_COPY_STATUS_DISCARD;
1314 # caller may proceed if either perm is allowed
1315 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1317 $copy->status($stat);
1318 $copy->edit_date('now');
1319 $copy->editor($e->requestor->id);
1321 $e->update_asset_copy($copy) or return $e->die_event;
1323 my $holds = $e->search_action_hold_request(
1325 current_copy => $copy->id,
1326 fulfillment_time => undef,
1327 cancel_time => undef,
1333 if( $self->api_name =~ /damaged/ ) {
1334 # now that we've committed the changes, create related A/T events
1335 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1336 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1339 $logger->debug("resetting holds that target the marked copy");
1340 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1345 sub handle_mark_damaged {
1346 my($e, $copy, $owning_lib, $args) = @_;
1348 my $apply = $args->{apply_fines} || '';
1349 return undef if $apply eq 'noapply';
1351 my $new_amount = $args->{override_amount};
1352 my $new_btype = $args->{override_btype};
1353 my $new_note = $args->{override_note};
1355 # grab the last circulation
1356 my $circ = $e->search_action_circulation([
1357 { target_copy => $copy->id},
1359 order_by => {circ => "xact_start DESC"},
1361 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1365 return undef unless $circ;
1367 my $charge_price = $U->ou_ancestor_setting_value(
1368 $owning_lib, 'circ.charge_on_damaged', $e);
1370 my $proc_fee = $U->ou_ancestor_setting_value(
1371 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1373 my $void_overdue = $U->ou_ancestor_setting_value(
1374 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1376 return undef unless $charge_price or $proc_fee;
1378 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1379 my $total = $copy_price + $proc_fee;
1383 if($new_amount and $new_btype) {
1385 # Allow staff to override the amount to charge for a damaged item
1386 # Consider the case where the item is only partially damaged
1387 # This value is meant to take the place of the item price and
1388 # optional processing fee.
1390 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1391 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1392 return $evt if $evt;
1396 if($charge_price and $copy_price) {
1397 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1398 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1399 return $evt if $evt;
1403 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1404 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1405 return $evt if $evt;
1409 # the assumption is that you would not void the overdues unless you
1410 # were also charging for the item and/or applying a processing fee
1412 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1413 return $evt if $evt;
1416 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1417 return $evt if $evt;
1419 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1420 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1422 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1423 return $evt2 if $evt2;
1428 return OpenILS::Event->new('DAMAGE_CHARGE',
1439 # ----------------------------------------------------------------------
1440 __PACKAGE__->register_method(
1441 method => 'mark_item_missing_pieces',
1442 api_name => 'open-ils.circ.mark_item_missing_pieces',
1444 Changes the status of a copy to "damaged" or to a custom status based on the
1445 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1447 @param authtoken The login session key
1448 @param copy_id The ID of the copy to mark as damaged
1449 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1453 sub mark_item_missing_pieces {
1454 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1455 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1456 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1458 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1459 return $e2->die_event unless $e2->checkauth;
1462 my $copy = $e2->retrieve_asset_copy([
1464 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1465 or return $e2->die_event;
1468 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1469 $copy->circ_lib : $copy->call_number->owning_lib;
1471 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1473 #### grab the last circulation
1474 my $circ = $e2->search_action_circulation([
1475 { target_copy => $copy->id},
1477 order_by => {circ => "xact_start DESC"}
1482 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1484 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1487 my $holds = $e2->search_action_hold_request(
1489 current_copy => $copy->id,
1490 fulfillment_time => undef,
1491 cancel_time => undef,
1495 $logger->debug("resetting holds that target the marked copy");
1496 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1499 if (! $e2->commit) {
1500 return $e2->die_event;
1503 my $e = new_editor(authtoken=>$auth, xact =>1);
1504 return $e->die_event unless $e->checkauth;
1506 if (! $circ->checkin_time) { # if circ active, attempt renew
1507 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1508 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1509 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1510 $circ = $res->[0]->{payload}{'circ'};
1511 $circ->target_copy( $copy->id );
1512 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1514 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1519 'copy_id'=>$circ->target_copy,
1520 'patron_id'=>$circ->usr,
1521 'skip_deposit_fee'=>1,
1522 'skip_rental_fee'=>1
1525 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1527 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1528 $e, $copy, $e->requestor, 1 );
1530 if ($hold) { # needed for hold? then due now
1532 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1533 my $due_date = DateTime->now(time_zone => 'local');
1534 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1536 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1540 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1541 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1542 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1543 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1544 $circ = $res->[0]->{payload}{'circ'};
1546 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1552 ### Update the item status
1554 my $custom_stat = $U->ou_ancestor_setting_value(
1555 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1556 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1558 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1559 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1561 $copy->status($stat);
1562 $copy->edit_date('now');
1563 $copy->editor($e->requestor->id);
1565 $e->update_asset_copy($copy) or return $e->die_event;
1569 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1570 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1572 return OpenILS::Event->new('SUCCESS',
1576 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1577 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1582 return $e->die_event;
1590 # ----------------------------------------------------------------------
1591 __PACKAGE__->register_method(
1592 method => 'magic_fetch',
1593 api_name => 'open-ils.agent.fetch'
1596 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1599 my( $self, $conn, $auth, $args ) = @_;
1600 my $e = new_editor( authtoken => $auth );
1601 return $e->event unless $e->checkauth;
1603 my $hint = $$args{hint};
1604 my $id = $$args{id};
1606 # Is the call allowed to fetch this type of object?
1607 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1609 # Find the class the implements the given hint
1610 my ($class) = grep {
1611 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1613 $class =~ s/Fieldmapper:://og;
1614 $class =~ s/::/_/og;
1615 my $method = "retrieve_$class";
1617 my $obj = $e->$method($id) or return $e->event;
1620 # ----------------------------------------------------------------------
1623 __PACKAGE__->register_method(
1624 method => "fleshed_circ_retrieve",
1626 api_name => "open-ils.circ.fleshed.retrieve",);
1628 sub fleshed_circ_retrieve {
1629 my( $self, $client, $id ) = @_;
1630 my $e = new_editor();
1631 my $circ = $e->retrieve_action_circulation(
1637 circ => [ qw/ target_copy / ],
1638 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1639 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1640 acn => [ qw/ record / ],
1644 ) or return $e->event;
1646 my $copy = $circ->target_copy;
1647 my $vol = $copy->call_number;
1648 my $rec = $circ->target_copy->call_number->record;
1650 $vol->record($rec->id);
1651 $copy->call_number($vol->id);
1652 $circ->target_copy($copy->id);
1656 if( $rec->id == OILS_PRECAT_RECORD ) {
1660 $mvr = $U->record_to_mvr($rec);
1661 $rec->marc(''); # drop the bulky marc data
1675 __PACKAGE__->register_method(
1676 method => "test_batch_circ_events",
1677 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1680 # method for testing the behavior of a given event definition
1681 sub test_batch_circ_events {
1682 my($self, $conn, $auth, $event_def, $barcode) = @_;
1684 my $e = new_editor(authtoken => $auth);
1685 return $e->event unless $e->checkauth;
1686 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1688 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1689 or return $e->event;
1691 my $circ = $e->search_action_circulation(
1692 {target_copy => $copy->id, checkin_time => undef})->[0]
1693 or return $e->event;
1695 return undef unless $circ;
1697 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1701 __PACKAGE__->register_method(
1702 method => "fire_circ_events",
1703 api_name => "open-ils.circ.fire_circ_trigger_events",
1705 General event def runner for circ objects. If no event def ID
1706 is provided, the hook will be used to find the best event_def
1707 match based on the context org unit
1711 __PACKAGE__->register_method(
1712 method => "fire_circ_events",
1713 api_name => "open-ils.circ.fire_hold_trigger_events",
1715 General event def runner for hold objects. If no event def ID
1716 is provided, the hook will be used to find the best event_def
1717 match based on the context org unit
1721 __PACKAGE__->register_method(
1722 method => "fire_circ_events",
1723 api_name => "open-ils.circ.fire_user_trigger_events",
1725 General event def runner for user objects. If no event def ID
1726 is provided, the hook will be used to find the best event_def
1727 match based on the context org unit
1732 sub fire_circ_events {
1733 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1735 my $e = new_editor(authtoken => $auth, xact => 1);
1736 return $e->event unless $e->checkauth;
1740 if($self->api_name =~ /hold/) {
1741 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1742 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1743 } elsif($self->api_name =~ /user/) {
1744 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1745 $targets = $e->batch_retrieve_actor_user($target_ids);
1747 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1748 $targets = $e->batch_retrieve_action_circulation($target_ids);
1750 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1751 # simply making this method authoritative because of weirdness
1752 # with transaction handling in A/T code that causes rollback
1753 # failure down the line if handling many targets
1755 return undef unless @$targets;
1756 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1759 __PACKAGE__->register_method(
1760 method => "user_payments_list",
1761 api_name => "open-ils.circ.user_payments.filtered.batch",
1764 desc => q/Returns a fleshed, date-limited set of all payments a user
1765 has made. By default, ordered by payment date. Optionally
1766 ordered by other columns in the top-level "mp" object/,
1768 {desc => 'Authentication token', type => 'string'},
1769 {desc => 'User ID', type => 'number'},
1770 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1772 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1773 and the related fully-realized payment object (e.g money.cash_payment)/}
1777 sub user_payments_list {
1778 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1780 my $e = new_editor(authtoken => $auth);
1781 return $e->event unless $e->checkauth;
1783 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1784 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1786 $order_by ||= ['payment_ts'];
1788 # all payments by user, between start_date and end_date
1789 my $payments = $e->json_query({
1790 select => {mp => ['id']},
1794 fkey => 'xact', field => 'id'}
1798 '+mbt' => {usr => $user_id},
1799 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1801 order_by => {mp => $order_by}
1804 for my $payment_id (@$payments) {
1805 my $payment = $e->retrieve_money_payment([
1813 'credit_card_payment',
1828 $conn->respond($payment);
1835 __PACKAGE__->register_method(
1836 method => "retrieve_circ_chain",
1837 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1840 desc => q/Given a circulation, this returns all circulation objects
1841 that are part of the same chain of renewals./,
1843 {desc => 'Authentication token', type => 'string'},
1844 {desc => 'Circ ID', type => 'number'},
1846 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1850 __PACKAGE__->register_method(
1851 method => "retrieve_circ_chain",
1852 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1854 desc => q/Given a circulation, this returns a summary of the circulation objects
1855 that are part of the same chain of renewals./,
1857 {desc => 'Authentication token', type => 'string'},
1858 {desc => 'Circ ID', type => 'number'},
1860 return => {desc => q/Circulation Chain Summary/}
1864 sub retrieve_circ_chain {
1865 my($self, $conn, $auth, $circ_id) = @_;
1867 my $e = new_editor(authtoken => $auth);
1868 return $e->event unless $e->checkauth;
1869 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1871 if($self->api_name =~ /summary/) {
1872 return $U->create_circ_chain_summary($e, $circ_id);
1876 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
1878 for my $circ_info (@$chain) {
1879 my $circ = Fieldmapper::action::all_circulation->new;
1880 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1881 $conn->respond($circ);
1888 __PACKAGE__->register_method(
1889 method => "retrieve_prev_circ_chain",
1890 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1893 desc => q/Given a circulation, this returns all circulation objects
1894 that are part of the previous chain of renewals./,
1896 {desc => 'Authentication token', type => 'string'},
1897 {desc => 'Circ ID', type => 'number'},
1899 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1903 __PACKAGE__->register_method(
1904 method => "retrieve_prev_circ_chain",
1905 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1907 desc => q/Given a circulation, this returns a summary of the circulation objects
1908 that are part of the previous chain of renewals./,
1910 {desc => 'Authentication token', type => 'string'},
1911 {desc => 'Circ ID', type => 'number'},
1913 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1917 sub retrieve_prev_circ_chain {
1918 my($self, $conn, $auth, $circ_id) = @_;
1920 my $e = new_editor(authtoken => $auth);
1921 return $e->event unless $e->checkauth;
1922 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1925 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
1927 my $prev_circ = $e->search_action_all_circulation([
1928 { target_copy => $first_circ->{target_copy},
1929 xact_start => {'<' => $first_circ->{xact_start}}
1938 order_by => { combcirc => 'xact_start desc' },
1943 return undef unless $prev_circ;
1945 my $chain_usr = $prev_circ->usr; # note: may be undef
1947 if ($self->api_name =~ /summary/) {
1948 my $sum = $e->json_query({
1950 'action.summarize_all_circ_chain',
1955 my $summary = Fieldmapper::action::circ_chain_summary->new;
1956 $summary->$_($sum->{$_}) for keys %$sum;
1958 return {summary => $summary, usr => $chain_usr};
1962 my $chain = $e->json_query(
1963 {from => ['action.all_circ_chain', $prev_circ->id]});
1965 for my $circ_info (@$chain) {
1966 my $circ = Fieldmapper::action::all_circulation->new;
1967 $circ->$_($circ_info->{$_}) for keys %$circ_info;
1968 $conn->respond($circ);
1975 __PACKAGE__->register_method(
1976 method => "get_copy_due_date",
1977 api_name => "open-ils.circ.copy.due_date.retrieve",
1980 Given a copy ID, returns the due date for the copy if it's
1981 currently circulating. Otherwise, returns null. Note, this is a public
1982 method requiring no authentication. Only the due date is exposed.
1985 {desc => 'Copy ID', type => 'number'}
1987 return => {desc => q/
1988 Due date (ISO date stamp) if the copy is circulating, null otherwise.
1993 sub get_copy_due_date {
1994 my($self, $conn, $copy_id) = @_;
1995 my $e = new_editor();
1997 my $circ = $e->json_query({
1998 select => {circ => ['due_date']},
2001 target_copy => $copy_id,
2002 checkin_time => undef,
2004 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2005 {stop_fines => undef}
2009 })->[0] or return undef;
2011 return $circ->{due_date};
2018 # {"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}}