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 __PACKAGE__->register_method(
1067 method => 'fetch_courses',
1069 api_name => 'open-ils.circ.courses.retrieve',
1071 Returns an array of course materials.
1072 @params course_id: The id of the course we want to retrieve
1075 sub fetch_course_materials {
1076 my ($self, $conn, $args) = @_;
1077 my $e = new_editor();
1081 $materials->{list} = $e->search_asset_course_module_course_materials($args);
1082 return $materials->{list} unless ($self->api_name =~ /\.fleshed/);
1084 # If we want it fleshed out...
1085 for my $course_material (@{$materials->{list}}) {
1087 $material->{id} = $course_material->id;
1088 $material->{relationship} = $course_material->relationship;
1089 $material->{record} = $course_material->record;
1090 my $copy = $e->retrieve_asset_copy([
1091 $course_material->item, {
1092 flesh => 3, flesh_fields => {
1093 'acp' => ['call_number'],
1099 $material->{item_data} = $copy;
1100 $material->{volume_data} = $copy->call_number;
1101 $material->{record_data} = $copy->call_number->record;
1102 $items{$course_material->item} = $material;
1106 for my $item (values %items) {
1107 my $final_item = {};
1108 my $mvr = $U->record_to_mvr($item->{record_data});
1109 $final_item->{id} = $item->{id};
1110 $final_item->{relationship} = $item->{relationship};
1111 $final_item->{record} = $item->{record};
1112 $final_item->{barcode} = $item->{item_data}->barcode;
1113 $final_item->{circ_lib} = $item->{item_data}->circ_lib;
1114 $final_item->{title} = $mvr->title;
1115 $final_item->{call_number} = $item->{volume_data}->label;
1116 $final_item->{location} = $e->retrieve_asset_copy_location(
1117 $item->{item_data}->location
1119 $final_item->{status} = $e->retrieve_config_copy_status(
1120 $item->{item_data}->status
1123 push @$targets, $final_item;
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);
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->{first_given_name} = $user->{patron_data}->first_given_name;
1187 $final_user->{family_name} = $user->{patron_data}->family_name;
1188 $final_user->{pref_first_given_name} = $user->{patron_data}->pref_first_given_name;
1189 $final_user->{pref_family_name} = $user->{patron_data}->pref_family_name;
1191 push @$targets, $final_user;
1198 __PACKAGE__->register_method(
1199 method => 'fetch_copy_tags',
1201 api_name => 'open-ils.circ.copy_tags.retrieve',
1203 Returns an array of publicly-visible copy tag objects.
1204 @param args A named hash of parameters including:
1205 copy_id : The id of the item whose notes we want to retrieve
1206 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1207 scope : top of org subtree whose copy tags we want to see
1208 depth : how far down to look for copy tags (optional)
1209 @return An array of copy tag objects
1211 __PACKAGE__->register_method(
1212 method => 'fetch_copy_tags',
1214 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1216 Returns an array of all copy tag objects.
1217 @param args A named hash of parameters including:
1218 authtoken : Required to view non-public notes
1219 copy_id : The id of the item whose notes we want to retrieve (optional)
1220 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1221 scope : top of org subtree whose copy tags we want to see
1222 depth : how far down to look for copy tags (optional)
1223 @return An array of copy tag objects
1226 sub fetch_copy_tags {
1227 my ($self, $conn, $args) = @_;
1229 my $org = $args->{scope};
1230 my $depth = $args->{depth};
1234 if ($self->api_name =~ /\.staff/) {
1235 my $authtoken = $args->{authtoken};
1236 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1237 $e = new_editor(authtoken => $args->{authtoken});
1238 return $e->event unless $e->checkauth;
1239 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1242 $filter->{pub} = 't';
1244 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1245 $filter->{'+acptcm'} = {
1246 copy => $args->{copy_id}
1249 # filter by owner of copy tag and depth
1250 $filter->{owner} = {
1252 select => {aou => [{
1254 transform => 'actor.org_unit_descendants',
1255 result_field => 'id',
1256 (defined($depth) ? ( params => [$depth] ) : ()),
1259 where => {id => $org}
1263 return $e->search_asset_copy_tag([$filter, {
1264 join => { acptcm => {} },
1266 flesh_fields => { acpt => ['tag_type'] }
1271 __PACKAGE__->register_method(
1272 method => 'age_hold_rules',
1273 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1276 sub age_hold_rules {
1277 my( $self, $conn ) = @_;
1278 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1283 __PACKAGE__->register_method(
1284 method => 'copy_details_barcode',
1286 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1287 sub copy_details_barcode {
1288 my( $self, $conn, $auth, $barcode ) = @_;
1289 my $e = new_editor();
1290 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1291 return $e->event unless $cid;
1292 return copy_details( $self, $conn, $auth, $cid );
1296 __PACKAGE__->register_method(
1297 method => 'copy_details',
1298 api_name => 'open-ils.circ.copy_details.retrieve');
1301 my( $self, $conn, $auth, $copy_id ) = @_;
1302 my $e = new_editor(authtoken=>$auth);
1303 return $e->event unless $e->checkauth;
1305 my $flesh = { flesh => 1 };
1307 my $copy = $e->retrieve_asset_copy(
1313 acp => ['call_number','parts','peer_record_maps','floating'],
1314 acn => ['record','prefix','suffix','label_class']
1317 ]) or return $e->event;
1320 # De-flesh the copy for backwards compatibility
1322 my $vol = $copy->call_number;
1324 $copy->call_number($vol->id);
1325 my $record = $vol->record;
1327 $vol->record($record->id);
1328 $mvr = $U->record_to_mvr($record);
1333 my $hold = $e->search_action_hold_request(
1335 current_copy => $copy_id,
1336 capture_time => { "!=" => undef },
1337 fulfillment_time => undef,
1338 cancel_time => undef,
1342 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1344 my $transit = $e->search_action_transit_copy(
1345 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1347 # find the most recent circulation for the requested copy,
1348 # be it active, completed, or aged.
1349 my $circ = $e->search_action_all_circulation_slim([
1350 { target_copy => $copy_id },
1356 'checkin_workstation',
1359 'recurring_fine_rule'
1362 order_by => { aacs => 'xact_start desc' },
1370 transit => $transit,
1380 __PACKAGE__->register_method(
1381 method => 'mark_item',
1382 api_name => 'open-ils.circ.mark_item_damaged',
1384 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1385 @param authtoken The login session key
1386 @param copy_id The ID of the copy to mark as damaged
1387 @return 1 on success - Event otherwise.
1390 __PACKAGE__->register_method(
1391 method => 'mark_item',
1392 api_name => 'open-ils.circ.mark_item_missing',
1394 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1395 @param authtoken The login session key
1396 @param copy_id The ID of the copy to mark as missing
1397 @return 1 on success - Event otherwise.
1400 __PACKAGE__->register_method(
1401 method => 'mark_item',
1402 api_name => 'open-ils.circ.mark_item_bindery',
1404 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1405 @param authtoken The login session key
1406 @param copy_id The ID of the copy to mark as bindery
1407 @return 1 on success - Event otherwise.
1410 __PACKAGE__->register_method(
1411 method => 'mark_item',
1412 api_name => 'open-ils.circ.mark_item_on_order',
1414 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1415 @param authtoken The login session key
1416 @param copy_id The ID of the copy to mark as on order
1417 @return 1 on success - Event otherwise.
1420 __PACKAGE__->register_method(
1421 method => 'mark_item',
1422 api_name => 'open-ils.circ.mark_item_ill',
1424 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1425 @param authtoken The login session key
1426 @param copy_id The ID of the copy to mark as inter-library loan
1427 @return 1 on success - Event otherwise.
1430 __PACKAGE__->register_method(
1431 method => 'mark_item',
1432 api_name => 'open-ils.circ.mark_item_cataloging',
1434 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1435 @param authtoken The login session key
1436 @param copy_id The ID of the copy to mark as cataloging
1437 @return 1 on success - Event otherwise.
1440 __PACKAGE__->register_method(
1441 method => 'mark_item',
1442 api_name => 'open-ils.circ.mark_item_reserves',
1444 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1445 @param authtoken The login session key
1446 @param copy_id The ID of the copy to mark as reserves
1447 @return 1 on success - Event otherwise.
1450 __PACKAGE__->register_method(
1451 method => 'mark_item',
1452 api_name => 'open-ils.circ.mark_item_discard',
1454 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1455 @param authtoken The login session key
1456 @param copy_id The ID of the copy to mark as discard
1457 @return 1 on success - Event otherwise.
1462 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1465 my $e = new_editor(authtoken=>$auth);
1466 return $e->die_event unless $e->checkauth;
1467 my $copy = $e->retrieve_asset_copy([
1469 {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
1470 or return $e->die_event;
1473 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1474 $copy->circ_lib : $copy->call_number->owning_lib;
1476 my $evt; # For later.
1477 my $perm = 'MARK_ITEM_MISSING';
1478 my $stat = OILS_COPY_STATUS_MISSING;
1480 if( $self->api_name =~ /damaged/ ) {
1481 $perm = 'MARK_ITEM_DAMAGED';
1482 $stat = OILS_COPY_STATUS_DAMAGED;
1483 } elsif ( $self->api_name =~ /bindery/ ) {
1484 $perm = 'MARK_ITEM_BINDERY';
1485 $stat = OILS_COPY_STATUS_BINDERY;
1486 } elsif ( $self->api_name =~ /on_order/ ) {
1487 $perm = 'MARK_ITEM_ON_ORDER';
1488 $stat = OILS_COPY_STATUS_ON_ORDER;
1489 } elsif ( $self->api_name =~ /ill/ ) {
1490 $perm = 'MARK_ITEM_ILL';
1491 $stat = OILS_COPY_STATUS_ILL;
1492 } elsif ( $self->api_name =~ /cataloging/ ) {
1493 $perm = 'MARK_ITEM_CATALOGING';
1494 $stat = OILS_COPY_STATUS_CATALOGING;
1495 } elsif ( $self->api_name =~ /reserves/ ) {
1496 $perm = 'MARK_ITEM_RESERVES';
1497 $stat = OILS_COPY_STATUS_RESERVES;
1498 } elsif ( $self->api_name =~ /discard/ ) {
1499 $perm = 'MARK_ITEM_DISCARD';
1500 $stat = OILS_COPY_STATUS_DISCARD;
1503 # caller may proceed if either perm is allowed
1504 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1506 # Copy status checks.
1507 if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
1508 # Items must be checked in before any attempt is made to change its status.
1509 if ($args->{handle_checkin}) {
1510 $evt = try_checkin($auth, $copy_id);
1512 $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
1514 } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
1515 # Items in transit need to have the transit aborted before being marked.
1516 if ($args->{handle_transit}) {
1517 $evt = try_abort_transit($auth, $copy_id);
1519 $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
1521 } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
1522 # Items with restrict_copy_delete status require the
1523 # COPY_DELETE_WARNING.override permission to be marked for
1525 if ($args->{handle_copy_delete_warning}) {
1526 $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
1528 $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
1531 return $evt if $evt;
1533 # Retrieving holds for later use.
1534 my $holds = $e->search_action_hold_request([
1536 current_copy => $copy->id,
1537 fulfillment_time => undef,
1538 cancel_time => undef,
1540 {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
1543 # Throw event if attempting to mark discard the only copy to fill a hold.
1544 if ($self->api_name =~ /discard/) {
1545 if (!$args->{handle_last_hold_copy}) {
1546 for my $hold (@$holds) {
1547 my $eligible = $hold->eligible_copies();
1548 if (!defined($eligible) || scalar(@{$eligible}) < 2) {
1549 $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
1555 return $evt if $evt;
1557 # Things below here require a transaction and there is nothing left to interfere with it.
1560 # Handle extra mark damaged charges, etc.
1561 if ($self->api_name =~ /damaged/) {
1562 $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1563 return $evt if $evt;
1567 $copy->status($stat);
1568 $copy->edit_date('now');
1569 $copy->editor($e->requestor->id);
1571 $e->update_asset_copy($copy) or return $e->die_event;
1575 if( $self->api_name =~ /damaged/ ) {
1576 # now that we've committed the changes, create related A/T events
1577 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1578 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1581 $logger->debug("resetting holds that target the marked copy");
1582 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1588 my($auth, $copy_id) = @_;
1590 my $checkin = $U->simplereq(
1592 'open-ils.circ.checkin.override',
1594 copy_id => $copy_id,
1598 if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1600 my $evt_code = $checkin->[0]->{textcode};
1601 $logger->info("try_checkin() received event: $evt_code");
1603 if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1604 $logger->info('try_checkin() successful checkin');
1607 $logger->warn('try_checkin() un-successful checkin');
1612 sub try_abort_transit {
1613 my ($auth, $copy_id) = @_;
1615 my $abort = $U->simplereq(
1617 'open-ils.circ.transit.abort',
1618 $auth, {copyid => $copy_id}
1620 # Above returns 1 or an event.
1621 return $abort if (ref $abort);
1625 sub handle_mark_damaged {
1626 my($e, $copy, $owning_lib, $args) = @_;
1628 my $apply = $args->{apply_fines} || '';
1629 return undef if $apply eq 'noapply';
1631 my $new_amount = $args->{override_amount};
1632 my $new_btype = $args->{override_btype};
1633 my $new_note = $args->{override_note};
1635 # grab the last circulation
1636 my $circ = $e->search_action_circulation([
1637 { target_copy => $copy->id},
1639 order_by => {circ => "xact_start DESC"},
1641 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1645 return undef unless $circ;
1647 my $charge_price = $U->ou_ancestor_setting_value(
1648 $owning_lib, 'circ.charge_on_damaged', $e);
1650 my $proc_fee = $U->ou_ancestor_setting_value(
1651 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1653 my $void_overdue = $U->ou_ancestor_setting_value(
1654 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1656 return undef unless $charge_price or $proc_fee;
1658 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1659 my $total = $copy_price + $proc_fee;
1663 if($new_amount and $new_btype) {
1665 # Allow staff to override the amount to charge for a damaged item
1666 # Consider the case where the item is only partially damaged
1667 # This value is meant to take the place of the item price and
1668 # optional processing fee.
1670 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1671 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1672 return $evt if $evt;
1676 if($charge_price and $copy_price) {
1677 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1678 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1679 return $evt if $evt;
1683 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1684 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1685 return $evt if $evt;
1689 # the assumption is that you would not void the overdues unless you
1690 # were also charging for the item and/or applying a processing fee
1692 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1693 return $evt if $evt;
1696 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1697 return $evt if $evt;
1699 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1700 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1702 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1703 return $evt2 if $evt2;
1706 return OpenILS::Event->new('DAMAGE_CHARGE',
1717 # ----------------------------------------------------------------------
1718 __PACKAGE__->register_method(
1719 method => 'mark_item_missing_pieces',
1720 api_name => 'open-ils.circ.mark_item_missing_pieces',
1722 Changes the status of a copy to "damaged" or to a custom status based on the
1723 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1725 @param authtoken The login session key
1726 @param copy_id The ID of the copy to mark as damaged
1727 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1731 sub mark_item_missing_pieces {
1732 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1733 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1734 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1736 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1737 return $e2->die_event unless $e2->checkauth;
1740 my $copy = $e2->retrieve_asset_copy([
1742 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1743 or return $e2->die_event;
1746 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1747 $copy->circ_lib : $copy->call_number->owning_lib;
1749 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1751 #### grab the last circulation
1752 my $circ = $e2->search_action_circulation([
1753 { target_copy => $copy->id},
1755 order_by => {circ => "xact_start DESC"}
1760 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1762 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1765 my $holds = $e2->search_action_hold_request(
1767 current_copy => $copy->id,
1768 fulfillment_time => undef,
1769 cancel_time => undef,
1773 $logger->debug("resetting holds that target the marked copy");
1774 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1777 if (! $e2->commit) {
1778 return $e2->die_event;
1781 my $e = new_editor(authtoken=>$auth, xact =>1);
1782 return $e->die_event unless $e->checkauth;
1784 if (! $circ->checkin_time) { # if circ active, attempt renew
1785 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1786 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1787 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1788 $circ = $res->[0]->{payload}{'circ'};
1789 $circ->target_copy( $copy->id );
1790 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1792 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1797 'copy_id'=>$circ->target_copy,
1798 'patron_id'=>$circ->usr,
1799 'skip_deposit_fee'=>1,
1800 'skip_rental_fee'=>1
1803 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1805 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1806 $e, $copy, $e->requestor, 1 );
1808 if ($hold) { # needed for hold? then due now
1810 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1811 my $due_date = DateTime->now(time_zone => 'local');
1812 $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
1814 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1818 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1819 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1820 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1821 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1822 $circ = $res->[0]->{payload}{'circ'};
1824 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1830 ### Update the item status
1832 my $custom_stat = $U->ou_ancestor_setting_value(
1833 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1834 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1836 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1837 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1839 $copy->status($stat);
1840 $copy->edit_date('now');
1841 $copy->editor($e->requestor->id);
1843 $e->update_asset_copy($copy) or return $e->die_event;
1847 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1848 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1850 return OpenILS::Event->new('SUCCESS',
1854 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1855 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1860 return $e->die_event;
1868 # ----------------------------------------------------------------------
1869 __PACKAGE__->register_method(
1870 method => 'magic_fetch',
1871 api_name => 'open-ils.agent.fetch'
1874 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1877 my( $self, $conn, $auth, $args ) = @_;
1878 my $e = new_editor( authtoken => $auth );
1879 return $e->event unless $e->checkauth;
1881 my $hint = $$args{hint};
1882 my $id = $$args{id};
1884 # Is the call allowed to fetch this type of object?
1885 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1887 # Find the class the implements the given hint
1888 my ($class) = grep {
1889 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1891 $class =~ s/Fieldmapper:://og;
1892 $class =~ s/::/_/og;
1893 my $method = "retrieve_$class";
1895 my $obj = $e->$method($id) or return $e->event;
1898 # ----------------------------------------------------------------------
1901 __PACKAGE__->register_method(
1902 method => "fleshed_circ_retrieve",
1904 api_name => "open-ils.circ.fleshed.retrieve",);
1906 sub fleshed_circ_retrieve {
1907 my( $self, $client, $id ) = @_;
1908 my $e = new_editor();
1909 my $circ = $e->retrieve_action_circulation(
1915 circ => [ qw/ target_copy / ],
1916 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1917 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1918 acn => [ qw/ record / ],
1922 ) or return $e->event;
1924 my $copy = $circ->target_copy;
1925 my $vol = $copy->call_number;
1926 my $rec = $circ->target_copy->call_number->record;
1928 $vol->record($rec->id);
1929 $copy->call_number($vol->id);
1930 $circ->target_copy($copy->id);
1934 if( $rec->id == OILS_PRECAT_RECORD ) {
1938 $mvr = $U->record_to_mvr($rec);
1939 $rec->marc(''); # drop the bulky marc data
1953 __PACKAGE__->register_method(
1954 method => "test_batch_circ_events",
1955 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1958 # method for testing the behavior of a given event definition
1959 sub test_batch_circ_events {
1960 my($self, $conn, $auth, $event_def, $barcode) = @_;
1962 my $e = new_editor(authtoken => $auth);
1963 return $e->event unless $e->checkauth;
1964 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1966 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1967 or return $e->event;
1969 my $circ = $e->search_action_circulation(
1970 {target_copy => $copy->id, checkin_time => undef})->[0]
1971 or return $e->event;
1973 return undef unless $circ;
1975 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1979 __PACKAGE__->register_method(
1980 method => "fire_circ_events",
1981 api_name => "open-ils.circ.fire_circ_trigger_events",
1983 General event def runner for circ objects. If no event def ID
1984 is provided, the hook will be used to find the best event_def
1985 match based on the context org unit
1989 __PACKAGE__->register_method(
1990 method => "fire_circ_events",
1991 api_name => "open-ils.circ.fire_hold_trigger_events",
1993 General event def runner for hold objects. If no event def ID
1994 is provided, the hook will be used to find the best event_def
1995 match based on the context org unit
1999 __PACKAGE__->register_method(
2000 method => "fire_circ_events",
2001 api_name => "open-ils.circ.fire_user_trigger_events",
2003 General event def runner for user objects. If no event def ID
2004 is provided, the hook will be used to find the best event_def
2005 match based on the context org unit
2010 sub fire_circ_events {
2011 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
2013 my $e = new_editor(authtoken => $auth, xact => 1);
2014 return $e->event unless $e->checkauth;
2018 if($self->api_name =~ /hold/) {
2019 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
2020 $targets = $e->batch_retrieve_action_hold_request($target_ids);
2021 } elsif($self->api_name =~ /user/) {
2022 return $e->event unless $e->allowed('VIEW_USER', $org_id);
2023 $targets = $e->batch_retrieve_actor_user($target_ids);
2025 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
2026 $targets = $e->batch_retrieve_action_circulation($target_ids);
2028 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
2029 # simply making this method authoritative because of weirdness
2030 # with transaction handling in A/T code that causes rollback
2031 # failure down the line if handling many targets
2033 return undef unless @$targets;
2034 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
2037 __PACKAGE__->register_method(
2038 method => "user_payments_list",
2039 api_name => "open-ils.circ.user_payments.filtered.batch",
2042 desc => q/Returns a fleshed, date-limited set of all payments a user
2043 has made. By default, ordered by payment date. Optionally
2044 ordered by other columns in the top-level "mp" object/,
2046 {desc => 'Authentication token', type => 'string'},
2047 {desc => 'User ID', type => 'number'},
2048 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
2050 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
2051 and the related fully-realized payment object (e.g money.cash_payment)/}
2055 sub user_payments_list {
2056 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
2058 my $e = new_editor(authtoken => $auth);
2059 return $e->event unless $e->checkauth;
2061 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
2062 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
2064 $order_by ||= ['payment_ts'];
2066 # all payments by user, between start_date and end_date
2067 my $payments = $e->json_query({
2068 select => {mp => ['id']},
2072 fkey => 'xact', field => 'id'}
2076 '+mbt' => {usr => $user_id},
2077 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
2079 order_by => {mp => $order_by}
2082 for my $payment_id (@$payments) {
2083 my $payment = $e->retrieve_money_payment([
2091 'credit_card_payment',
2092 'debit_card_payment',
2107 $conn->respond($payment);
2114 __PACKAGE__->register_method(
2115 method => "retrieve_circ_chain",
2116 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
2119 desc => q/Given a circulation, this returns all circulation objects
2120 that are part of the same chain of renewals./,
2122 {desc => 'Authentication token', type => 'string'},
2123 {desc => 'Circ ID', type => 'number'},
2125 return => {desc => q/List of circ objects, orderd by oldest circ first/}
2129 __PACKAGE__->register_method(
2130 method => "retrieve_circ_chain",
2131 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
2133 desc => q/Given a circulation, this returns a summary of the circulation objects
2134 that are part of the same chain of renewals./,
2136 {desc => 'Authentication token', type => 'string'},
2137 {desc => 'Circ ID', type => 'number'},
2139 return => {desc => q/Circulation Chain Summary/}
2143 sub retrieve_circ_chain {
2144 my($self, $conn, $auth, $circ_id) = @_;
2146 my $e = new_editor(authtoken => $auth);
2147 return $e->event unless $e->checkauth;
2148 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2150 if($self->api_name =~ /summary/) {
2151 return $U->create_circ_chain_summary($e, $circ_id);
2155 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
2157 for my $circ_info (@$chain) {
2158 my $circ = Fieldmapper::action::all_circulation_slim->new;
2159 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2160 $conn->respond($circ);
2167 __PACKAGE__->register_method(
2168 method => "retrieve_prev_circ_chain",
2169 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
2172 desc => q/Given a circulation, this returns all circulation objects
2173 that are part of the previous chain of renewals./,
2175 {desc => 'Authentication token', type => 'string'},
2176 {desc => 'Circ ID', type => 'number'},
2178 return => {desc => q/List of circ objects, orderd by oldest circ first/}
2182 __PACKAGE__->register_method(
2183 method => "retrieve_prev_circ_chain",
2184 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
2186 desc => q/Given a circulation, this returns a summary of the circulation objects
2187 that are part of the previous chain of renewals./,
2189 {desc => 'Authentication token', type => 'string'},
2190 {desc => 'Circ ID', type => 'number'},
2192 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
2196 sub retrieve_prev_circ_chain {
2197 my($self, $conn, $auth, $circ_id) = @_;
2199 my $e = new_editor(authtoken => $auth);
2200 return $e->event unless $e->checkauth;
2201 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2204 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
2206 my $prev_circ = $e->search_action_all_circulation_slim([
2207 { target_copy => $first_circ->{target_copy},
2208 xact_start => {'<' => $first_circ->{xact_start}}
2217 order_by => { aacs => 'xact_start desc' },
2222 return undef unless $prev_circ;
2224 my $chain_usr = $prev_circ->usr; # note: may be undef
2226 if ($self->api_name =~ /summary/) {
2227 my $sum = $e->json_query({
2229 'action.summarize_all_circ_chain',
2234 my $summary = Fieldmapper::action::circ_chain_summary->new;
2235 $summary->$_($sum->{$_}) for keys %$sum;
2237 return {summary => $summary, usr => $chain_usr};
2241 my $chain = $e->json_query(
2242 {from => ['action.all_circ_chain', $prev_circ->id]});
2244 for my $circ_info (@$chain) {
2245 my $circ = Fieldmapper::action::all_circulation_slim->new;
2246 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2247 $conn->respond($circ);
2254 __PACKAGE__->register_method(
2255 method => "get_copy_due_date",
2256 api_name => "open-ils.circ.copy.due_date.retrieve",
2259 Given a copy ID, returns the due date for the copy if it's
2260 currently circulating. Otherwise, returns null. Note, this is a public
2261 method requiring no authentication. Only the due date is exposed.
2264 {desc => 'Copy ID', type => 'number'}
2266 return => {desc => q/
2267 Due date (ISO date stamp) if the copy is circulating, null otherwise.
2272 sub get_copy_due_date {
2273 my($self, $conn, $copy_id) = @_;
2274 my $e = new_editor();
2276 my $circ = $e->json_query({
2277 select => {circ => ['due_date']},
2280 target_copy => $copy_id,
2281 checkin_time => undef,
2283 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2284 {stop_fines => undef}
2288 })->[0] or return undef;
2290 return $circ->{due_date};
2297 # {"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}}