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 OpenILS::Utils::DateTime qw/:datetime/;
24 use OpenSRF::AppSession;
25 use OpenILS::Utils::ModsParser;
27 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::Fieldmapper;
30 use OpenILS::Utils::CStoreEditor q/:funcs/;
31 use OpenILS::Const qw/:const/;
32 use OpenSRF::Utils::SettingsClient;
33 use OpenILS::Application::Cat::AssetCommon;
35 my $apputils = "OpenILS::Application::AppUtils";
38 my $holdcode = "OpenILS::Application::Circ::Holds";
40 # ------------------------------------------------------------------------
41 # Top level Circ package;
42 # ------------------------------------------------------------------------
46 OpenILS::Application::Circ::Circulate->initialize();
50 __PACKAGE__->register_method(
51 method => 'retrieve_circ',
53 api_name => 'open-ils.circ.retrieve',
55 Retrieve a circ object by id
56 @param authtoken Login session key
57 @pararm circid The id of the circ object
58 @param all_circ Returns an action.all_circulation_slim object instead
59 of an action.circulation object to pick up aged circs.
64 my( $s, $c, $a, $i, $all_circ ) = @_;
65 my $e = new_editor(authtoken => $a);
66 return $e->event unless $e->checkauth;
67 my $method = $all_circ ?
68 'retrieve_action_all_circulation_slim' :
69 'retrieve_action_circulation';
70 my $circ = $e->$method($i) or return $e->event;
71 if( $e->requestor->id ne ($circ->usr || '') ) {
72 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
78 __PACKAGE__->register_method(
79 method => 'fetch_circ_mods',
80 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
82 my($self, $conn, $args) = @_;
83 my $mods = new_editor()->retrieve_all_config_circ_modifier;
84 return [ map {$_->code} @$mods ] unless $$args{full};
88 __PACKAGE__->register_method(
89 method => 'ranged_billing_types',
90 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
92 sub ranged_billing_types {
93 my($self, $conn, $auth, $org_id, $depth) = @_;
94 my $e = new_editor(authtoken => $auth);
95 return $e->event unless $e->checkauth;
96 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
97 return $e->search_config_billing_type(
98 {owner => $U->get_org_full_path($org_id, $depth)});
103 # ------------------------------------------------------------------------
104 # Returns an array of {circ, record} hashes checked out by the user.
105 # ------------------------------------------------------------------------
106 __PACKAGE__->register_method(
107 method => "checkouts_by_user",
108 api_name => "open-ils.circ.actor.user.checked_out",
110 NOTES => <<" NOTES");
111 Returns a list of open circulations as a pile of objects. Each object
112 contains the relevant copy, circ, and record
115 sub checkouts_by_user {
116 my($self, $client, $auth, $user_id) = @_;
118 my $e = new_editor(authtoken=>$auth);
119 return $e->event unless $e->checkauth;
121 my $circ_ids = $e->search_action_circulation(
123 checkin_time => undef,
125 {stop_fines => undef},
126 {stop_fines => ['MAXFINES','LONGOVERDUE']}
132 for my $id (@$circ_ids) {
133 my $circ = $e->retrieve_action_circulation([
137 circ => ['target_copy'],
138 acp => ['call_number'],
144 # un-flesh for consistency
145 my $c = $circ->target_copy;
146 $circ->target_copy($c->id);
148 my $cn = $c->call_number;
149 $c->call_number($cn->id);
157 record => $U->record_to_mvr($t)
167 __PACKAGE__->register_method(
168 method => "checkouts_by_user_slim",
169 api_name => "open-ils.circ.actor.user.checked_out.slim",
170 NOTES => <<" NOTES");
171 Returns a list of open circulation objects
175 sub checkouts_by_user_slim {
176 my( $self, $client, $user_session, $user_id ) = @_;
178 my( $requestor, $target, $copy, $record, $evt );
180 ( $requestor, $target, $evt ) =
181 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
184 $logger->debug( 'User ' . $requestor->id .
185 " retrieving checked out items for user " . $target->id );
187 # XXX Make the call correct..
188 return $apputils->simplereq(
190 "open-ils.cstore.direct.action.open_circulation.search.atomic",
191 { usr => $target->id, checkin_time => undef } );
192 # { usr => $target->id } );
196 __PACKAGE__->register_method(
197 method => "checkouts_by_user_opac",
198 api_name => "open-ils.circ.actor.user.checked_out.opac",);
201 sub checkouts_by_user_opac {
202 my( $self, $client, $auth, $user_id ) = @_;
204 my $e = new_editor( authtoken => $auth );
205 return $e->event unless $e->checkauth;
206 $user_id ||= $e->requestor->id;
207 return $e->event unless
208 my $patron = $e->retrieve_actor_user($user_id);
211 my $search = {usr => $user_id, stop_fines => undef};
213 if( $user_id ne $e->requestor->id ) {
214 $data = $e->search_action_circulation(
215 $search, {checkperm=>1, permorg=>$patron->home_ou})
219 $data = $e->search_action_circulation($search);
226 __PACKAGE__->register_method(
227 method => "title_from_transaction",
228 api_name => "open-ils.circ.circ_transaction.find_title",
229 NOTES => <<" NOTES");
230 Returns a mods object for the title that is linked to from the
231 copy from the hold that created the given transaction
234 sub title_from_transaction {
235 my( $self, $client, $login_session, $transactionid ) = @_;
237 my( $user, $circ, $title, $evt );
239 ( $user, $evt ) = $apputils->checkses( $login_session );
242 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
245 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
248 return $apputils->record_to_mvr($title);
251 __PACKAGE__->register_method(
252 method => "staff_age_to_lost",
253 api_name => "open-ils.circ.circulation.age_to_lost",
256 This fires a circ.staff_age_to_lost Action-Trigger event against all
257 overdue circulations in scope of the specified context library and
258 user profile, which effectively marks the associated items as Lost.
259 This is likely to be done at the end of a semester in an academic
262 @param args : circ_lib, user_profile
266 sub staff_age_to_lost {
267 my( $self, $conn, $auth, $args ) = @_;
268 my $e = new_editor(authtoken=>$auth);
269 return $e->event unless $e->checkauth;
270 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
272 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
273 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
275 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
277 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
278 my $hook = 'circ.staff_age_to_lost';
279 my $context_org = 'circ_lib';
280 my $opt_granularity = undef;
282 "checkin_time" => undef,
283 "due_date" => { "<" => "now" },
285 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
286 { "stop_fines" => undef }
290 "select" => {"au" => ["id"]},
293 "profile" => $profiles,
294 "id" => { "=" => {"+circ" => "usr"} }
298 "select" => {"aou" => ["id"]},
302 {"id" => { "=" => {"+circ" => "circ_lib"} }},
309 my $req_timeout = 10800;
310 my $chunk_size = 100;
313 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
314 my @event_ids; my @chunked_ids;
315 while (my $resp = $req->recv(timeout => $req_timeout)) {
316 push(@event_ids, $resp->content);
317 push(@chunked_ids, $resp->content);
318 if (scalar(@chunked_ids) > $chunk_size) {
319 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
323 if (scalar(@chunked_ids) > 0) {
324 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
328 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
329 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
330 } elsif($req->complete) {
331 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
332 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
334 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
335 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
342 __PACKAGE__->register_method(
343 method => "new_set_circ_lost",
344 api_name => "open-ils.circ.circulation.set_lost",
346 Sets the copy and related open circulation to lost
348 @param args : barcode
353 # ---------------------------------------------------------------------
354 # Sets a circulation to lost. updates copy status to lost
355 # applies copy and/or prcoessing fees depending on org settings
356 # ---------------------------------------------------------------------
357 sub new_set_circ_lost {
358 my( $self, $conn, $auth, $args ) = @_;
360 my $e = new_editor(authtoken=>$auth, xact=>1);
361 return $e->die_event unless $e->checkauth;
363 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
364 or return $e->die_event;
366 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
373 __PACKAGE__->register_method(
374 method => "update_latest_inventory",
375 api_name => "open-ils.circ.circulation.update_latest_inventory");
377 sub update_latest_inventory {
378 my( $self, $conn, $auth, $args ) = @_;
379 my $e = new_editor(authtoken=>$auth, xact=>1);
380 return $e->die_event unless $e->checkauth;
382 my $copies = $$args{copy_list};
383 foreach my $copyid (@$copies) {
384 my $copy = $e->retrieve_asset_copy($copyid);
385 my $alci = $e->search_asset_latest_inventory({copy => $copyid})->[0];
388 $alci->inventory_date('now');
389 $alci->inventory_workstation($e->requestor->wsid);
390 $e->update_asset_latest_inventory($alci) or return $e->die_event;
392 my $alci = Fieldmapper::asset::latest_inventory->new;
393 $alci->inventory_date('now');
394 $alci->inventory_workstation($e->requestor->wsid);
395 $alci->copy($copy->id);
396 $e->create_asset_latest_inventory($alci) or return $e->die_event;
399 $copy->latest_inventory($alci);
405 __PACKAGE__->register_method(
406 method => "set_circ_claims_returned",
407 api_name => "open-ils.circ.circulation.set_claims_returned",
409 desc => q/Sets the circ for a given item as claims returned
410 If a backdate is provided, overdue fines will be voided
411 back to the backdate/,
413 {desc => 'Authentication token', type => 'string'},
414 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
416 return => {desc => q/1 on success, failure event on error, and
417 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
418 configured claims return maximum/}
422 __PACKAGE__->register_method(
423 method => "set_circ_claims_returned",
424 api_name => "open-ils.circ.circulation.set_claims_returned.override",
426 desc => q/This adds support for overrideing the configured max
427 claims returned amount.
428 @see open-ils.circ.circulation.set_claims_returned./,
432 sub set_circ_claims_returned {
433 my( $self, $conn, $auth, $args, $oargs ) = @_;
435 my $e = new_editor(authtoken=>$auth, xact=>1);
436 return $e->die_event unless $e->checkauth;
438 $oargs = { all => 1 } unless defined $oargs;
440 my $barcode = $$args{barcode};
441 my $backdate = $$args{backdate};
443 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
444 or return $e->die_event;
446 my $circ = $e->search_action_circulation(
447 {checkin_time => undef, target_copy => $copy->id})->[0]
448 or return $e->die_event;
450 $backdate = $circ->due_date if $$args{use_due_date};
452 $logger->info("marking circ for item $barcode as claims returned".
453 (($backdate) ? " with backdate $backdate" : ''));
455 my $patron = $e->retrieve_actor_user($circ->usr);
456 my $max_count = $U->ou_ancestor_setting_value(
457 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
459 # If the patron has too instances of many claims returned,
460 # require an override to continue. A configured max of
461 # 0 means all attempts require an override
462 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
464 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
466 # see if we're allowed to override
467 return $e->die_event unless
468 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
472 # exit early and return the max claims return event
474 return OpenILS::Event->new(
475 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
477 patron_count => $patron->claims_returned_count,
478 max_count => $max_count
484 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
485 or return $e->die_event;
487 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
488 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
491 $backdate = clean_ISO8601($backdate);
493 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
494 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
495 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
497 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
498 $backdate = clean_ISO8601($backdate);
500 # make it look like the circ stopped at the cliams returned time
501 $circ->stop_fines_time($backdate);
502 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
506 $e->update_action_circulation($circ) or return $e->die_event;
508 # see if there is a configured post-claims-return copy status
509 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
510 $copy->status($stat);
511 $copy->edit_date('now');
512 $copy->editor($e->requestor->id);
513 $e->update_asset_copy($copy) or return $e->die_event;
516 # Check if the copy circ lib wants lost fees voided on claims
518 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
519 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
530 # Check if the copy circ lib wants lost processing fees voided on
532 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
533 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
544 # Check if the copy circ lib wants longoverdue fees voided on claims
546 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
547 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
558 # Check if the copy circ lib wants longoverdue processing fees voided on
560 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
561 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
572 # Now that all data has been munged, do a no-op update of
573 # the patron to force a change of the last_xact_id value.
574 $e->update_actor_user($e->retrieve_actor_user($circ->usr))
575 or return $e->die_event;
582 __PACKAGE__->register_method(
583 method => "post_checkin_backdate_circ",
584 api_name => "open-ils.circ.post_checkin_backdate",
586 desc => q/Back-date an already checked in circulation/,
588 {desc => 'Authentication token', type => 'string'},
589 {desc => 'Circ ID', type => 'number'},
590 {desc => 'ISO8601 backdate', type => 'string'},
592 return => {desc => q/1 on success, failure event on error/}
596 __PACKAGE__->register_method(
597 method => "post_checkin_backdate_circ",
598 api_name => "open-ils.circ.post_checkin_backdate.batch",
601 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
603 {desc => 'Authentication token', type => 'string'},
604 {desc => 'List of Circ ID', type => 'array'},
605 {desc => 'ISO8601 backdate', type => 'string'},
607 return => {desc => q/Set of: 1 on success, failure event on error/}
612 sub post_checkin_backdate_circ {
613 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
614 my $e = new_editor(authtoken=>$auth);
615 return $e->die_event unless $e->checkauth;
616 if($self->api_name =~ /batch/) {
617 foreach my $c (@$circ_id) {
618 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
621 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
629 sub post_checkin_backdate_circ_impl {
630 my($e, $circ_id, $backdate) = @_;
634 my $circ = $e->retrieve_action_circulation($circ_id)
635 or return $e->die_event;
637 # anyone with checkin perms can backdate (more restrictive?)
638 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
640 # don't allow back-dating an open circulation
641 return OpenILS::Event->new('BAD_PARAMS') unless
642 $backdate and $circ->checkin_time;
644 # update the checkin and stop_fines times to reflect the new backdate
645 $circ->stop_fines_time(clean_ISO8601($backdate));
646 $circ->checkin_time(clean_ISO8601($backdate));
647 $e->update_action_circulation($circ) or return $e->die_event;
649 # now void the overdues "erased" by the back-dating
650 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
653 # If the circ was closed before and the balance owned !=0, re-open the transaction
654 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
663 __PACKAGE__->register_method (
664 method => 'set_circ_due_date',
665 api_name => 'open-ils.circ.circulation.due_date.update',
667 Updates the due_date on the given circ
669 @param circid The id of the circ to update
670 @param date The timestamp of the new due date
674 sub set_circ_due_date {
675 my( $self, $conn, $auth, $circ_id, $date ) = @_;
677 my $e = new_editor(xact=>1, authtoken=>$auth);
678 return $e->die_event unless $e->checkauth;
679 my $circ = $e->retrieve_action_circulation($circ_id)
680 or return $e->die_event;
682 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
683 $date = clean_ISO8601($date);
685 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
686 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
687 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
689 # since the new date may be coming in as UTC, convert it
690 # to the same time zone as the original due date so that
691 # ->ymd is more likely to yield the expected results
692 $new_date->set_time_zone($original_date->time_zone());
693 $date = clean_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
696 $circ->due_date($date);
697 $e->update_action_circulation($circ) or return $e->die_event;
704 __PACKAGE__->register_method(
705 method => "create_in_house_use",
706 api_name => 'open-ils.circ.in_house_use.create',
708 Creates an in-house use action.
709 @param $authtoken The login session key
710 @param params A hash of params including
711 'location' The org unit id where the in-house use occurs
712 'copyid' The copy in question
713 'count' The number of in-house uses to apply to this copy
714 @return An array of id's representing the id's of the newly created
715 in-house use objects or an event on an error
718 __PACKAGE__->register_method(
719 method => "create_in_house_use",
720 api_name => 'open-ils.circ.non_cat_in_house_use.create',
724 sub create_in_house_use {
725 my( $self, $client, $auth, $params ) = @_;
728 my $org = $params->{location};
729 my $copyid = $params->{copyid};
730 my $count = $params->{count} || 1;
731 my $nc_type = $params->{non_cat_type};
732 my $use_time = $params->{use_time} || 'now';
734 my $e = new_editor(xact=>1,authtoken=>$auth);
735 return $e->event unless $e->checkauth;
736 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
738 my $non_cat = 1 if $self->api_name =~ /non_cat/;
742 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
744 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
750 if( $use_time ne 'now' ) {
751 $use_time = clean_ISO8601($use_time);
752 $logger->debug("in_house_use setting use time to $use_time");
763 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
764 $ihu->item_type($nc_type);
765 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
766 $cmeth = "create_action_non_cat_in_house_use";
769 $ihu = Fieldmapper::action::in_house_use->new;
771 $method = 'open-ils.storage.direct.action.in_house_use.create';
772 $cmeth = "create_action_in_house_use";
775 $ihu->staff($e->requestor->id);
776 $ihu->workstation($e->requestor->wsid);
777 $ihu->org_unit($org);
778 $ihu->use_time($use_time);
780 $ihu = $e->$cmeth($ihu) or return $e->event;
781 push( @ids, $ihu->id );
792 __PACKAGE__->register_method(
793 method => "view_circs",
794 api_name => "open-ils.circ.copy_checkout_history.retrieve",
796 Retrieves the last X circs for a given copy
797 @param authtoken The login session key
798 @param copyid The copy to check
799 @param count How far to go back in the item history
800 @return An array of circ ids
803 # ----------------------------------------------------------------------
804 # Returns $count most recent circs. If count exceeds the configured
805 # max, use the configured max instead
806 # ----------------------------------------------------------------------
808 my( $self, $client, $authtoken, $copyid, $count ) = @_;
810 my $e = new_editor(authtoken => $authtoken);
811 return $e->event unless $e->checkauth;
813 my $copy = $e->retrieve_asset_copy([
816 flesh_fields => {acp => ['call_number']}
818 ]) or return $e->event;
820 return $e->event unless $e->allowed(
821 'VIEW_COPY_CHECKOUT_HISTORY',
822 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
823 $copy->circ_lib : $copy->call_number->owning_lib);
825 my $max_history = $U->ou_ancestor_setting_value(
826 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
828 if(defined $max_history) {
829 $count = $max_history unless defined $count and $count < $max_history;
831 $count = 4 unless defined $count;
834 return $e->search_action_all_circulation_slim([
835 {target_copy => $copyid},
836 {limit => $count, order_by => { aacs => "xact_start DESC" }}
841 __PACKAGE__->register_method(
842 method => "circ_count",
843 api_name => "open-ils.circ.circulation.count",
845 Returns the number of times the item has circulated
846 @param copyid The copy to check
850 my( $self, $client, $copyid ) = @_;
852 my $count = new_editor()->json_query({
861 where => {'+circbyyr' => {copy => $copyid}}
873 __PACKAGE__->register_method(
874 method => 'fetch_notes',
876 api_name => 'open-ils.circ.copy_note.retrieve.all',
878 Returns an array of copy note objects.
879 @param args A named hash of parameters including:
880 authtoken : Required if viewing non-public notes
881 itemid : The id of the item whose notes we want to retrieve
882 pub : True if all the caller wants are public notes
883 @return An array of note objects
886 __PACKAGE__->register_method(
887 method => 'fetch_notes',
888 api_name => 'open-ils.circ.call_number_note.retrieve.all',
889 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
891 __PACKAGE__->register_method(
892 method => 'fetch_notes',
893 api_name => 'open-ils.circ.title_note.retrieve.all',
894 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
897 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
899 my( $self, $connection, $args ) = @_;
901 my $id = $$args{itemid};
902 my $authtoken = $$args{authtoken};
905 if( $self->api_name =~ /copy/ ) {
907 return $U->cstorereq(
908 'open-ils.cstore.direct.asset.copy_note.search.atomic',
909 { owning_copy => $id, pub => 't' } );
911 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
913 return $U->cstorereq(
914 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
917 } elsif( $self->api_name =~ /call_number/ ) {
919 return $U->cstorereq(
920 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
921 { call_number => $id, pub => 't' } );
923 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
925 return $U->cstorereq(
926 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
929 } elsif( $self->api_name =~ /title/ ) {
931 return $U->cstorereq(
932 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
933 { record => $id, pub => 't' } );
935 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
937 return $U->cstorereq(
938 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
945 __PACKAGE__->register_method(
946 method => 'has_notes',
947 api_name => 'open-ils.circ.copy.has_notes');
948 __PACKAGE__->register_method(
949 method => 'has_notes',
950 api_name => 'open-ils.circ.call_number.has_notes');
951 __PACKAGE__->register_method(
952 method => 'has_notes',
953 api_name => 'open-ils.circ.title.has_notes');
957 my( $self, $conn, $authtoken, $id ) = @_;
958 my $editor = new_editor(authtoken => $authtoken);
959 return $editor->event unless $editor->checkauth;
961 my $n = $editor->search_asset_copy_note(
962 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
964 $n = $editor->search_asset_call_number_note(
965 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
967 $n = $editor->search_biblio_record_note(
968 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
975 __PACKAGE__->register_method(
976 method => 'create_copy_note',
977 api_name => 'open-ils.circ.copy_note.create',
979 Creates a new copy note
980 @param authtoken The login session key
981 @param note The note object to create
982 @return The id of the new note object
985 sub create_copy_note {
986 my( $self, $connection, $authtoken, $note ) = @_;
988 my $e = new_editor(xact=>1, authtoken=>$authtoken);
989 return $e->event unless $e->checkauth;
990 my $copy = $e->retrieve_asset_copy(
994 flesh_fields => { 'acp' => ['call_number'] }
999 return $e->event unless
1000 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
1002 $note->create_date('now');
1003 $note->creator($e->requestor->id);
1004 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1007 $e->create_asset_copy_note($note) or return $e->event;
1013 __PACKAGE__->register_method(
1014 method => 'delete_copy_note',
1015 api_name => 'open-ils.circ.copy_note.delete',
1017 Deletes an existing copy note
1018 @param authtoken The login session key
1019 @param noteid The id of the note to delete
1020 @return 1 on success - Event otherwise.
1022 sub delete_copy_note {
1023 my( $self, $conn, $authtoken, $noteid ) = @_;
1025 my $e = new_editor(xact=>1, authtoken=>$authtoken);
1026 return $e->die_event unless $e->checkauth;
1028 my $note = $e->retrieve_asset_copy_note([
1032 'acpn' => [ 'owning_copy' ],
1033 'acp' => [ 'call_number' ],
1036 ]) or return $e->die_event;
1038 if( $note->creator ne $e->requestor->id ) {
1039 return $e->die_event unless
1040 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1043 $e->delete_asset_copy_note($note) or return $e->die_event;
1047 __PACKAGE__->register_method(
1048 method => 'fetch_course_materials',
1050 api_name => 'open-ils.circ.course_materials.retrieve',
1052 Returns an array of course materials.
1053 @params args : Supplied object to filter search.
1056 __PACKAGE__->register_method(
1057 method => 'fetch_course_materials',
1059 api_name => 'open-ils.circ.course_materials.retrieve.fleshed',
1061 Returns an array of course materials, each fleshed out with information
1062 from the item and the course_material object.
1063 @params args : Supplied object to filter search.
1066 sub fetch_course_materials {
1067 my ($self, $conn, $args) = @_;
1068 my $e = new_editor();
1072 $materials->{list} = $e->search_asset_course_module_course_materials($args);
1073 return $materials->{list} unless ($self->api_name =~ /\.fleshed/);
1075 # If we want it fleshed out...
1076 for my $course_material (@{$materials->{list}}) {
1078 $material->{id} = $course_material->id;
1079 $material->{relationship} = $course_material->relationship;
1080 $material->{record} = $course_material->record;
1081 my $copy = $e->retrieve_asset_copy([
1082 $course_material->item, {
1083 flesh => 3, flesh_fields => {
1084 'acp' => ['call_number'],
1090 $material->{item_data} = $copy;
1091 $material->{volume_data} = $copy->call_number;
1092 $material->{record_data} = $copy->call_number->record;
1093 $items{$course_material->item} = $material;
1097 for my $item (values %items) {
1098 my $final_item = {};
1099 my $mvr = $U->record_to_mvr($item->{record_data});
1100 $final_item->{id} = $item->{id};
1101 $final_item->{relationship} = $item->{relationship};
1102 $final_item->{record} = $item->{record};
1103 $final_item->{barcode} = $item->{item_data}->barcode;
1104 $final_item->{circ_lib} = $item->{item_data}->circ_lib;
1105 $final_item->{title} = $mvr->title;
1106 $final_item->{call_number} = $item->{volume_data}->label;
1107 $final_item->{location} = $e->retrieve_asset_copy_location(
1108 $item->{item_data}->location
1110 $final_item->{status} = $e->retrieve_config_copy_status(
1111 $item->{item_data}->status
1114 push @$targets, $final_item;
1120 __PACKAGE__->register_method(
1121 method => 'fetch_courses',
1123 api_name => 'open-ils.circ.courses.retrieve',
1125 Returns an array of course materials.
1126 @params course_id: The id of the course we want to retrieve
1130 my ($self, $conn, @course_ids) = @_;
1131 my $e = new_editor();
1133 return unless @course_ids;
1135 foreach my $course_id (@course_ids) {
1136 my $target = $e->retrieve_asset_course_module_course($course_id);
1137 push @$targets, $target;
1143 __PACKAGE__->register_method(
1144 method => 'fetch_course_users',
1146 api_name => 'open-ils.circ.course_users.retrieve',
1148 Returns an array of course users.
1149 @params course_id: The id of the course we want to retrieve from
1151 __PACKAGE__->register_method(
1152 method => 'fetch_course_users',
1154 api_name => 'open-ils.circ.course_users.retrieve.staff',
1156 Returns an array of course users.
1157 @params course_id: The id of the course we want to retrieve from
1160 sub fetch_course_users {
1161 my ($self, $conn, $course_id) = @_;
1162 my $e = new_editor();
1167 $filter->{course} = $course_id;
1168 $filter->{is_public} = 't'
1169 unless ($self->api_name =~ /\.staff/) and $e->allowed('MANAGE_RESERVES');
1172 $users->{list} = $e->search_asset_course_module_course_users($filter, {order_by => {acmcu => 'id'}});
1173 for my $course_user (@{$users->{list}}) {
1175 $patron->{id} = $course_user->id;
1176 $patron->{usr_role} = $course_user->usr_role;
1177 $patron->{patron_data} = $e->retrieve_actor_user($course_user->usr);
1178 $patrons{$course_user->usr} = $patron;
1182 for my $user (values %patrons) {
1183 my $final_user = {};
1184 $final_user->{id} = $user->{id};
1185 $final_user->{usr_role} = $user->{usr_role};
1186 $final_user->{patron_id} = $user->{patron_data}->id;
1187 $final_user->{first_given_name} = $user->{patron_data}->first_given_name;
1188 $final_user->{second_given_name} = $user->{patron_data}->second_given_name;
1189 $final_user->{family_name} = $user->{patron_data}->family_name;
1190 $final_user->{pref_first_given_name} = $user->{patron_data}->pref_first_given_name;
1191 $final_user->{pref_family_name} = $user->{patron_data}->pref_family_name;
1192 $final_user->{pref_second_given_name} = $user->{patron_data}->pref_second_given_name;
1193 $final_user->{pref_suffix} = $user->{patron_data}->pref_suffix;
1194 $final_user->{pref_prefix} = $user->{patron_data}->pref_prefix;
1196 push @$targets, $final_user;
1203 __PACKAGE__->register_method(
1204 method => 'fetch_copy_tags',
1206 api_name => 'open-ils.circ.copy_tags.retrieve',
1208 Returns an array of publicly-visible copy tag objects.
1209 @param args A named hash of parameters including:
1210 copy_id : The id of the item whose notes we want to retrieve
1211 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1212 scope : top of org subtree whose copy tags we want to see
1213 depth : how far down to look for copy tags (optional)
1214 @return An array of copy tag objects
1216 __PACKAGE__->register_method(
1217 method => 'fetch_copy_tags',
1219 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1221 Returns an array of all copy tag objects.
1222 @param args A named hash of parameters including:
1223 authtoken : Required to view non-public notes
1224 copy_id : The id of the item whose notes we want to retrieve (optional)
1225 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1226 scope : top of org subtree whose copy tags we want to see
1227 depth : how far down to look for copy tags (optional)
1228 @return An array of copy tag objects
1231 sub fetch_copy_tags {
1232 my ($self, $conn, $args) = @_;
1234 my $org = $args->{scope};
1235 my $depth = $args->{depth};
1239 if ($self->api_name =~ /\.staff/) {
1240 my $authtoken = $args->{authtoken};
1241 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1242 $e = new_editor(authtoken => $args->{authtoken});
1243 return $e->event unless $e->checkauth;
1244 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1247 $filter->{pub} = 't';
1249 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1250 $filter->{'+acptcm'} = {
1251 copy => $args->{copy_id}
1254 # filter by owner of copy tag and depth
1255 $filter->{owner} = {
1257 select => {aou => [{
1259 transform => 'actor.org_unit_descendants',
1260 result_field => 'id',
1261 (defined($depth) ? ( params => [$depth] ) : ()),
1264 where => {id => $org}
1268 return $e->search_asset_copy_tag([$filter, {
1269 join => { acptcm => {} },
1271 flesh_fields => { acpt => ['tag_type'] }
1276 __PACKAGE__->register_method(
1277 method => 'age_hold_rules',
1278 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1281 sub age_hold_rules {
1282 my( $self, $conn ) = @_;
1283 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1288 __PACKAGE__->register_method(
1289 method => 'copy_details_barcode',
1291 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1292 sub copy_details_barcode {
1293 my( $self, $conn, $auth, $barcode ) = @_;
1294 my $e = new_editor();
1295 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1296 return $e->event unless $cid;
1297 return copy_details( $self, $conn, $auth, $cid );
1301 __PACKAGE__->register_method(
1302 method => 'copy_details',
1303 api_name => 'open-ils.circ.copy_details.retrieve');
1306 my( $self, $conn, $auth, $copy_id ) = @_;
1307 my $e = new_editor(authtoken=>$auth);
1308 return $e->event unless $e->checkauth;
1310 my $flesh = { flesh => 1 };
1312 my $copy = $e->retrieve_asset_copy(
1318 acp => ['call_number','parts','peer_record_maps','floating'],
1319 acn => ['record','prefix','suffix','label_class']
1322 ]) or return $e->event;
1325 # De-flesh the copy for backwards compatibility
1327 my $vol = $copy->call_number;
1329 $copy->call_number($vol->id);
1330 my $record = $vol->record;
1332 $vol->record($record->id);
1333 $mvr = $U->record_to_mvr($record);
1338 my $hold = $e->search_action_hold_request(
1340 current_copy => $copy_id,
1341 capture_time => { "!=" => undef },
1342 fulfillment_time => undef,
1343 cancel_time => undef,
1347 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1349 my $transit = $e->search_action_transit_copy(
1350 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1352 # find the most recent circulation for the requested copy,
1353 # be it active, completed, or aged.
1354 my $circ = $e->search_action_all_circulation_slim([
1355 { target_copy => $copy_id },
1361 'checkin_workstation',
1364 'recurring_fine_rule'
1367 order_by => { aacs => 'xact_start desc' },
1375 transit => $transit,
1385 __PACKAGE__->register_method(
1386 method => 'mark_item',
1387 api_name => 'open-ils.circ.mark_item_damaged',
1389 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1390 @param authtoken The login session key
1391 @param copy_id The ID of the copy to mark as damaged
1392 @return 1 on success - Event otherwise.
1395 __PACKAGE__->register_method(
1396 method => 'mark_item',
1397 api_name => 'open-ils.circ.mark_item_missing',
1399 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1400 @param authtoken The login session key
1401 @param copy_id The ID of the copy to mark as missing
1402 @return 1 on success - Event otherwise.
1405 __PACKAGE__->register_method(
1406 method => 'mark_item',
1407 api_name => 'open-ils.circ.mark_item_bindery',
1409 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1410 @param authtoken The login session key
1411 @param copy_id The ID of the copy to mark as bindery
1412 @return 1 on success - Event otherwise.
1415 __PACKAGE__->register_method(
1416 method => 'mark_item',
1417 api_name => 'open-ils.circ.mark_item_on_order',
1419 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1420 @param authtoken The login session key
1421 @param copy_id The ID of the copy to mark as on order
1422 @return 1 on success - Event otherwise.
1425 __PACKAGE__->register_method(
1426 method => 'mark_item',
1427 api_name => 'open-ils.circ.mark_item_ill',
1429 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1430 @param authtoken The login session key
1431 @param copy_id The ID of the copy to mark as inter-library loan
1432 @return 1 on success - Event otherwise.
1435 __PACKAGE__->register_method(
1436 method => 'mark_item',
1437 api_name => 'open-ils.circ.mark_item_cataloging',
1439 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1440 @param authtoken The login session key
1441 @param copy_id The ID of the copy to mark as cataloging
1442 @return 1 on success - Event otherwise.
1445 __PACKAGE__->register_method(
1446 method => 'mark_item',
1447 api_name => 'open-ils.circ.mark_item_reserves',
1449 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1450 @param authtoken The login session key
1451 @param copy_id The ID of the copy to mark as reserves
1452 @return 1 on success - Event otherwise.
1455 __PACKAGE__->register_method(
1456 method => 'mark_item',
1457 api_name => 'open-ils.circ.mark_item_discard',
1459 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1460 @param authtoken The login session key
1461 @param copy_id The ID of the copy to mark as discard
1462 @return 1 on success - Event otherwise.
1467 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1470 my $e = new_editor(authtoken=>$auth);
1471 return $e->die_event unless $e->checkauth;
1472 my $copy = $e->retrieve_asset_copy([
1474 {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
1475 or return $e->die_event;
1478 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1479 $copy->circ_lib : $copy->call_number->owning_lib;
1481 my $evt; # For later.
1482 my $perm = 'MARK_ITEM_MISSING';
1483 my $stat = OILS_COPY_STATUS_MISSING;
1485 if( $self->api_name =~ /damaged/ ) {
1486 $perm = 'MARK_ITEM_DAMAGED';
1487 $stat = OILS_COPY_STATUS_DAMAGED;
1488 } elsif ( $self->api_name =~ /bindery/ ) {
1489 $perm = 'MARK_ITEM_BINDERY';
1490 $stat = OILS_COPY_STATUS_BINDERY;
1491 } elsif ( $self->api_name =~ /on_order/ ) {
1492 $perm = 'MARK_ITEM_ON_ORDER';
1493 $stat = OILS_COPY_STATUS_ON_ORDER;
1494 } elsif ( $self->api_name =~ /ill/ ) {
1495 $perm = 'MARK_ITEM_ILL';
1496 $stat = OILS_COPY_STATUS_ILL;
1497 } elsif ( $self->api_name =~ /cataloging/ ) {
1498 $perm = 'MARK_ITEM_CATALOGING';
1499 $stat = OILS_COPY_STATUS_CATALOGING;
1500 } elsif ( $self->api_name =~ /reserves/ ) {
1501 $perm = 'MARK_ITEM_RESERVES';
1502 $stat = OILS_COPY_STATUS_RESERVES;
1503 } elsif ( $self->api_name =~ /discard/ ) {
1504 $perm = 'MARK_ITEM_DISCARD';
1505 $stat = OILS_COPY_STATUS_DISCARD;
1508 # caller may proceed if either perm is allowed
1509 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1511 # Copy status checks.
1512 if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
1513 # Items must be checked in before any attempt is made to change its status.
1514 if ($args->{handle_checkin}) {
1515 $evt = try_checkin($auth, $copy_id);
1517 $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
1519 } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
1520 # Items in transit need to have the transit aborted before being marked.
1521 if ($args->{handle_transit}) {
1522 $evt = try_abort_transit($auth, $copy_id);
1524 $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
1526 } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
1527 # Items with restrict_copy_delete status require the
1528 # COPY_DELETE_WARNING.override permission to be marked for
1530 if ($args->{handle_copy_delete_warning}) {
1531 $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
1533 $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
1536 return $evt if $evt;
1538 # Retrieving holds for later use.
1539 my $holds = $e->search_action_hold_request([
1541 current_copy => $copy->id,
1542 fulfillment_time => undef,
1543 cancel_time => undef,
1545 {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
1548 # Throw event if attempting to mark discard the only copy to fill a hold.
1549 if ($self->api_name =~ /discard/) {
1550 if (!$args->{handle_last_hold_copy}) {
1551 for my $hold (@$holds) {
1552 my $eligible = $hold->eligible_copies();
1553 if (!defined($eligible) || scalar(@{$eligible}) < 2) {
1554 $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
1560 return $evt if $evt;
1562 # Things below here require a transaction and there is nothing left to interfere with it.
1565 # Handle extra mark damaged charges, etc.
1566 if ($self->api_name =~ /damaged/) {
1567 $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1568 return $evt if $evt;
1572 $copy->status($stat);
1573 $copy->edit_date('now');
1574 $copy->editor($e->requestor->id);
1576 $e->update_asset_copy($copy) or return $e->die_event;
1580 if( $self->api_name =~ /damaged/ ) {
1581 # now that we've committed the changes, create related A/T events
1582 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1583 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1586 $logger->debug("resetting holds that target the marked copy");
1587 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1593 my($auth, $copy_id) = @_;
1595 my $checkin = $U->simplereq(
1597 'open-ils.circ.checkin.override',
1599 copy_id => $copy_id,
1603 if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1605 my $evt_code = $checkin->[0]->{textcode};
1606 $logger->info("try_checkin() received event: $evt_code");
1608 if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1609 $logger->info('try_checkin() successful checkin');
1612 $logger->warn('try_checkin() un-successful checkin');
1617 sub try_abort_transit {
1618 my ($auth, $copy_id) = @_;
1620 my $abort = $U->simplereq(
1622 'open-ils.circ.transit.abort',
1623 $auth, {copyid => $copy_id}
1625 # Above returns 1 or an event.
1626 return $abort if (ref $abort);
1630 sub handle_mark_damaged {
1631 my($e, $copy, $owning_lib, $args) = @_;
1633 my $apply = $args->{apply_fines} || '';
1634 return undef if $apply eq 'noapply';
1636 my $new_amount = $args->{override_amount};
1637 my $new_btype = $args->{override_btype};
1638 my $new_note = $args->{override_note};
1640 # grab the last circulation
1641 my $circ = $e->search_action_circulation([
1642 { target_copy => $copy->id},
1644 order_by => {circ => "xact_start DESC"},
1646 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1650 return undef unless $circ;
1652 my $charge_price = $U->ou_ancestor_setting_value(
1653 $owning_lib, 'circ.charge_on_damaged', $e);
1655 my $proc_fee = $U->ou_ancestor_setting_value(
1656 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1658 my $void_overdue = $U->ou_ancestor_setting_value(
1659 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1661 return undef unless $charge_price or $proc_fee;
1663 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1664 my $total = $copy_price + $proc_fee;
1668 if($new_amount and $new_btype) {
1670 # Allow staff to override the amount to charge for a damaged item
1671 # Consider the case where the item is only partially damaged
1672 # This value is meant to take the place of the item price and
1673 # optional processing fee.
1675 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1676 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1677 return $evt if $evt;
1681 if($charge_price and $copy_price) {
1682 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1683 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1684 return $evt if $evt;
1688 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1689 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1690 return $evt if $evt;
1694 # the assumption is that you would not void the overdues unless you
1695 # were also charging for the item and/or applying a processing fee
1697 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1698 return $evt if $evt;
1701 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1702 return $evt if $evt;
1704 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1705 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1707 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1708 return $evt2 if $evt2;
1711 return OpenILS::Event->new('DAMAGE_CHARGE',
1722 # ----------------------------------------------------------------------
1723 __PACKAGE__->register_method(
1724 method => 'mark_item_missing_pieces',
1725 api_name => 'open-ils.circ.mark_item_missing_pieces',
1727 Changes the status of a copy to "damaged" or to a custom status based on the
1728 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1730 @param authtoken The login session key
1731 @param copy_id The ID of the copy to mark as damaged
1732 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1736 sub mark_item_missing_pieces {
1737 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1738 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1739 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1741 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1742 return $e2->die_event unless $e2->checkauth;
1745 my $copy = $e2->retrieve_asset_copy([
1747 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1748 or return $e2->die_event;
1751 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1752 $copy->circ_lib : $copy->call_number->owning_lib;
1754 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1756 #### grab the last circulation
1757 my $circ = $e2->search_action_circulation([
1758 { target_copy => $copy->id},
1760 order_by => {circ => "xact_start DESC"}
1765 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1767 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1770 my $holds = $e2->search_action_hold_request(
1772 current_copy => $copy->id,
1773 fulfillment_time => undef,
1774 cancel_time => undef,
1778 $logger->debug("resetting holds that target the marked copy");
1779 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1782 if (! $e2->commit) {
1783 return $e2->die_event;
1786 my $e = new_editor(authtoken=>$auth, xact =>1);
1787 return $e->die_event unless $e->checkauth;
1789 if (! $circ->checkin_time) { # if circ active, attempt renew
1790 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1791 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1792 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1793 $circ = $res->[0]->{payload}{'circ'};
1794 $circ->target_copy( $copy->id );
1795 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1797 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1802 'copy_id'=>$circ->target_copy,
1803 'patron_id'=>$circ->usr,
1804 'skip_deposit_fee'=>1,
1805 'skip_rental_fee'=>1
1808 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1810 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1811 $e, $copy, $e->requestor, 1 );
1813 if ($hold) { # needed for hold? then due now
1815 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1816 my $due_date = DateTime->now(time_zone => 'local');
1817 $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
1819 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1823 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1824 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1825 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1826 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1827 $circ = $res->[0]->{payload}{'circ'};
1829 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1835 ### Update the item status
1837 my $custom_stat = $U->ou_ancestor_setting_value(
1838 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1839 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1841 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1842 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1844 $copy->status($stat);
1845 $copy->edit_date('now');
1846 $copy->editor($e->requestor->id);
1848 $e->update_asset_copy($copy) or return $e->die_event;
1852 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1853 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1855 return OpenILS::Event->new('SUCCESS',
1859 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1860 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1865 return $e->die_event;
1873 # ----------------------------------------------------------------------
1874 __PACKAGE__->register_method(
1875 method => 'magic_fetch',
1876 api_name => 'open-ils.agent.fetch'
1879 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1882 my( $self, $conn, $auth, $args ) = @_;
1883 my $e = new_editor( authtoken => $auth );
1884 return $e->event unless $e->checkauth;
1886 my $hint = $$args{hint};
1887 my $id = $$args{id};
1889 # Is the call allowed to fetch this type of object?
1890 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1892 # Find the class the implements the given hint
1893 my ($class) = grep {
1894 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1896 $class =~ s/Fieldmapper:://og;
1897 $class =~ s/::/_/og;
1898 my $method = "retrieve_$class";
1900 my $obj = $e->$method($id) or return $e->event;
1903 # ----------------------------------------------------------------------
1906 __PACKAGE__->register_method(
1907 method => "fleshed_circ_retrieve",
1909 api_name => "open-ils.circ.fleshed.retrieve",);
1911 sub fleshed_circ_retrieve {
1912 my( $self, $client, $id ) = @_;
1913 my $e = new_editor();
1914 my $circ = $e->retrieve_action_circulation(
1920 circ => [ qw/ target_copy / ],
1921 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1922 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1923 acn => [ qw/ record / ],
1927 ) or return $e->event;
1929 my $copy = $circ->target_copy;
1930 my $vol = $copy->call_number;
1931 my $rec = $circ->target_copy->call_number->record;
1933 $vol->record($rec->id);
1934 $copy->call_number($vol->id);
1935 $circ->target_copy($copy->id);
1939 if( $rec->id == OILS_PRECAT_RECORD ) {
1943 $mvr = $U->record_to_mvr($rec);
1944 $rec->marc(''); # drop the bulky marc data
1958 __PACKAGE__->register_method(
1959 method => "test_batch_circ_events",
1960 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1963 # method for testing the behavior of a given event definition
1964 sub test_batch_circ_events {
1965 my($self, $conn, $auth, $event_def, $barcode) = @_;
1967 my $e = new_editor(authtoken => $auth);
1968 return $e->event unless $e->checkauth;
1969 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1971 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1972 or return $e->event;
1974 my $circ = $e->search_action_circulation(
1975 {target_copy => $copy->id, checkin_time => undef})->[0]
1976 or return $e->event;
1978 return undef unless $circ;
1980 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1984 __PACKAGE__->register_method(
1985 method => "fire_circ_events",
1986 api_name => "open-ils.circ.fire_circ_trigger_events",
1988 General event def runner for circ objects. If no event def ID
1989 is provided, the hook will be used to find the best event_def
1990 match based on the context org unit
1994 __PACKAGE__->register_method(
1995 method => "fire_circ_events",
1996 api_name => "open-ils.circ.fire_hold_trigger_events",
1998 General event def runner for hold objects. If no event def ID
1999 is provided, the hook will be used to find the best event_def
2000 match based on the context org unit
2004 __PACKAGE__->register_method(
2005 method => "fire_circ_events",
2006 api_name => "open-ils.circ.fire_user_trigger_events",
2008 General event def runner for user objects. If no event def ID
2009 is provided, the hook will be used to find the best event_def
2010 match based on the context org unit
2015 sub fire_circ_events {
2016 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
2018 my $e = new_editor(authtoken => $auth, xact => 1);
2019 return $e->event unless $e->checkauth;
2023 if($self->api_name =~ /hold/) {
2024 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
2025 $targets = $e->batch_retrieve_action_hold_request($target_ids);
2026 } elsif($self->api_name =~ /user/) {
2027 return $e->event unless $e->allowed('VIEW_USER', $org_id);
2028 $targets = $e->batch_retrieve_actor_user($target_ids);
2030 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
2031 $targets = $e->batch_retrieve_action_circulation($target_ids);
2033 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
2034 # simply making this method authoritative because of weirdness
2035 # with transaction handling in A/T code that causes rollback
2036 # failure down the line if handling many targets
2038 return undef unless @$targets;
2039 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
2042 __PACKAGE__->register_method(
2043 method => "user_payments_list",
2044 api_name => "open-ils.circ.user_payments.filtered.batch",
2047 desc => q/Returns a fleshed, date-limited set of all payments a user
2048 has made. By default, ordered by payment date. Optionally
2049 ordered by other columns in the top-level "mp" object/,
2051 {desc => 'Authentication token', type => 'string'},
2052 {desc => 'User ID', type => 'number'},
2053 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
2055 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
2056 and the related fully-realized payment object (e.g money.cash_payment)/}
2060 sub user_payments_list {
2061 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
2063 my $e = new_editor(authtoken => $auth);
2064 return $e->event unless $e->checkauth;
2066 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
2067 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
2069 $order_by ||= ['payment_ts'];
2071 # all payments by user, between start_date and end_date
2072 my $payments = $e->json_query({
2073 select => {mp => ['id']},
2077 fkey => 'xact', field => 'id'}
2081 '+mbt' => {usr => $user_id},
2082 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
2084 order_by => {mp => $order_by}
2087 for my $payment_id (@$payments) {
2088 my $payment = $e->retrieve_money_payment([
2096 'credit_card_payment',
2097 'debit_card_payment',
2112 $conn->respond($payment);
2119 __PACKAGE__->register_method(
2120 method => "retrieve_circ_chain",
2121 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
2124 desc => q/Given a circulation, this returns all circulation objects
2125 that are part of the same chain of renewals./,
2127 {desc => 'Authentication token', type => 'string'},
2128 {desc => 'Circ ID', type => 'number'},
2130 return => {desc => q/List of circ objects, orderd by oldest circ first/}
2134 __PACKAGE__->register_method(
2135 method => "retrieve_circ_chain",
2136 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
2138 desc => q/Given a circulation, this returns a summary of the circulation objects
2139 that are part of the same chain of renewals./,
2141 {desc => 'Authentication token', type => 'string'},
2142 {desc => 'Circ ID', type => 'number'},
2144 return => {desc => q/Circulation Chain Summary/}
2148 sub retrieve_circ_chain {
2149 my($self, $conn, $auth, $circ_id) = @_;
2151 my $e = new_editor(authtoken => $auth);
2152 return $e->event unless $e->checkauth;
2153 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2155 if($self->api_name =~ /summary/) {
2156 return $U->create_circ_chain_summary($e, $circ_id);
2160 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
2162 for my $circ_info (@$chain) {
2163 my $circ = Fieldmapper::action::all_circulation_slim->new;
2164 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2165 $conn->respond($circ);
2172 __PACKAGE__->register_method(
2173 method => "retrieve_prev_circ_chain",
2174 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
2177 desc => q/Given a circulation, this returns all circulation objects
2178 that are part of the previous chain of renewals./,
2180 {desc => 'Authentication token', type => 'string'},
2181 {desc => 'Circ ID', type => 'number'},
2183 return => {desc => q/List of circ objects, orderd by oldest circ first/}
2187 __PACKAGE__->register_method(
2188 method => "retrieve_prev_circ_chain",
2189 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
2191 desc => q/Given a circulation, this returns a summary of the circulation objects
2192 that are part of the previous chain of renewals./,
2194 {desc => 'Authentication token', type => 'string'},
2195 {desc => 'Circ ID', type => 'number'},
2197 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
2201 sub retrieve_prev_circ_chain {
2202 my($self, $conn, $auth, $circ_id) = @_;
2204 my $e = new_editor(authtoken => $auth);
2205 return $e->event unless $e->checkauth;
2206 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2209 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
2211 my $prev_circ = $e->search_action_all_circulation_slim([
2212 { target_copy => $first_circ->{target_copy},
2213 xact_start => {'<' => $first_circ->{xact_start}}
2222 order_by => { aacs => 'xact_start desc' },
2227 return undef unless $prev_circ;
2229 my $chain_usr = $prev_circ->usr; # note: may be undef
2231 if ($self->api_name =~ /summary/) {
2232 my $sum = $e->json_query({
2234 'action.summarize_all_circ_chain',
2239 my $summary = Fieldmapper::action::circ_chain_summary->new;
2240 $summary->$_($sum->{$_}) for keys %$sum;
2242 return {summary => $summary, usr => $chain_usr};
2246 my $chain = $e->json_query(
2247 {from => ['action.all_circ_chain', $prev_circ->id]});
2249 for my $circ_info (@$chain) {
2250 my $circ = Fieldmapper::action::all_circulation_slim->new;
2251 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2252 $conn->respond($circ);
2259 __PACKAGE__->register_method(
2260 method => "get_copy_due_date",
2261 api_name => "open-ils.circ.copy.due_date.retrieve",
2264 Given a copy ID, returns the due date for the copy if it's
2265 currently circulating. Otherwise, returns null. Note, this is a public
2266 method requiring no authentication. Only the due date is exposed.
2269 {desc => 'Copy ID', type => 'number'}
2271 return => {desc => q/
2272 Due date (ISO date stamp) if the copy is circulating, null otherwise.
2277 sub get_copy_due_date {
2278 my($self, $conn, $copy_id) = @_;
2279 my $e = new_editor();
2281 my $circ = $e->json_query({
2282 select => {circ => ['due_date']},
2285 target_copy => $copy_id,
2286 checkin_time => undef,
2288 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2289 {stop_fines => undef}
2293 })->[0] or return undef;
2295 return $circ->{due_date};
2302 # {"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}}