]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
LP#1694529 Claims returned updates patron last-xact-id
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ.pm
1 package OpenILS::Application::Circ;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
5
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;
17
18 use DateTime;
19 use DateTime::Format::ISO8601;
20
21 use OpenILS::Application::AppUtils;
22
23 use OpenSRF::Utils qw/:datetime/;
24 use OpenSRF::AppSession;
25 use OpenILS::Utils::ModsParser;
26 use OpenILS::Event;
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;
34
35 my $apputils = "OpenILS::Application::AppUtils";
36 my $U = $apputils;
37
38 my $holdcode    = "OpenILS::Application::Circ::Holds";
39
40 # ------------------------------------------------------------------------
41 # Top level Circ package;
42 # ------------------------------------------------------------------------
43
44 sub initialize {
45     my $self = shift;
46     OpenILS::Application::Circ::Circulate->initialize();
47 }
48
49
50 __PACKAGE__->register_method(
51     method => 'retrieve_circ',
52     authoritative   => 1,
53     api_name    => 'open-ils.circ.retrieve',
54     signature => q/
55         Retrieve a circ object by id
56         @param authtoken Login session key
57         @pararm circid The id of the circ object
58         @param all_circ Returns an action.all_circulation object instead
59             of an action.circulation object to pick up aged circs.
60     /
61 );
62
63 sub retrieve_circ {
64     my( $s, $c, $a, $i, $all_circ ) = @_;
65     my $e = new_editor(authtoken => $a);
66     return $e->event unless $e->checkauth;
67     my $method = $all_circ ?
68         'retrieve_action_all_circulation' :
69         'retrieve_action_circulation';
70     my $circ = $e->$method($i) or return $e->event;
71     if( $e->requestor->id ne ($circ->usr || '') ) {
72         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
73     }
74     return $circ;
75 }
76
77
78 __PACKAGE__->register_method(
79     method => 'fetch_circ_mods',
80     api_name => 'open-ils.circ.circ_modifier.retrieve.all');
81 sub fetch_circ_mods {
82     my($self, $conn, $args) = @_;
83     my $mods = new_editor()->retrieve_all_config_circ_modifier;
84     return [ map {$_->code} @$mods ] unless $$args{full};
85     return $mods;
86 }
87
88 __PACKAGE__->register_method(
89     method => 'ranged_billing_types',
90     api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
91
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)});
99 }
100
101
102
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",
109     stream => 1,
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
113     NOTES
114
115 sub checkouts_by_user {
116     my($self, $client, $auth, $user_id) = @_;
117
118     my $e = new_editor(authtoken=>$auth);
119     return $e->event unless $e->checkauth;
120
121     my $circ_ids = $e->search_action_circulation(
122         {   usr => $user_id,
123             checkin_time => undef,
124             '-or' => [
125                 {stop_fines => undef},
126                 {stop_fines => ['MAXFINES','LONGOVERDUE']}
127             ]
128         },
129         {idlist => 1}
130     );
131
132     for my $id (@$circ_ids) {
133         my $circ = $e->retrieve_action_circulation([
134             $id,
135             {   flesh => 3,
136                 flesh_fields => {
137                     circ => ['target_copy'],
138                     acp => ['call_number'],
139                     acn => ['record']
140                 }
141             }
142         ]);
143
144         # un-flesh for consistency
145         my $c = $circ->target_copy;
146         $circ->target_copy($c->id);
147
148         my $cn = $c->call_number;
149         $c->call_number($cn->id);
150
151         my $t = $cn->record;
152         $cn->record($t->id);
153
154         $client->respond(
155             {   circ => $circ,
156                 copy => $c,
157                 record => $U->record_to_mvr($t)
158             }
159         );
160     }
161
162     return undef;
163 }
164
165
166
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
172     NOTES
173
174 # DEPRECAT ME?? XXX
175 sub checkouts_by_user_slim {
176     my( $self, $client, $user_session, $user_id ) = @_;
177
178     my( $requestor, $target, $copy, $record, $evt );
179
180     ( $requestor, $target, $evt ) = 
181         $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
182     return $evt if $evt;
183
184     $logger->debug( 'User ' . $requestor->id . 
185         " retrieving checked out items for user " . $target->id );
186
187     # XXX Make the call correct..
188     return $apputils->simplereq(
189         'open-ils.cstore',
190         "open-ils.cstore.direct.action.open_circulation.search.atomic", 
191         { usr => $target->id, checkin_time => undef } );
192 #       { usr => $target->id } );
193 }
194
195
196 __PACKAGE__->register_method(
197     method  => "checkouts_by_user_opac",
198     api_name    => "open-ils.circ.actor.user.checked_out.opac",);
199
200 # XXX Deprecate Me
201 sub checkouts_by_user_opac {
202     my( $self, $client, $auth, $user_id ) = @_;
203
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);
209
210     my $data;
211     my $search = {usr => $user_id, stop_fines => undef};
212
213     if( $user_id ne $e->requestor->id ) {
214         $data = $e->search_action_circulation(
215             $search, {checkperm=>1, permorg=>$patron->home_ou})
216             or return $e->event;
217
218     } else {
219         $data = $e->search_action_circulation($search);
220     }
221
222     return $data;
223 }
224
225
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
232     NOTES
233
234 sub title_from_transaction {
235     my( $self, $client, $login_session, $transactionid ) = @_;
236
237     my( $user, $circ, $title, $evt );
238
239     ( $user, $evt ) = $apputils->checkses( $login_session );
240     return $evt if $evt;
241
242     ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
243     return $evt if $evt;
244     
245     ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
246     return $evt if $evt;
247
248     return $apputils->record_to_mvr($title);
249 }
250
251 __PACKAGE__->register_method(
252     method  => "staff_age_to_lost",
253     api_name    => "open-ils.circ.circulation.age_to_lost",
254     stream => 1,
255     signature   => q/
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
260         library, etc.
261         @param auth
262         @param args : circ_lib, user_profile
263     /
264 );
265
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'});
271
272     my $orgs = $U->get_org_descendants($args->{'circ_lib'});
273     my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
274
275     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
276
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;
281     my $filter = { 
282         "checkin_time" => undef,
283         "due_date" => { "<" => "now" }, 
284         "-or" => [ 
285             { "stop_fines"  => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
286             { "stop_fines"  => undef }
287         ],
288         "-and" => [
289             {"-exists" => {
290                 "select" => {"au" => ["id"]},
291                 "from"   => "au",
292                 "where"  => {
293                     "profile" => $profiles,
294                     "id" => { "=" => {"+circ" => "usr"} }
295                 }
296             }},
297             {"-exists" => {
298                 "select" => {"aou" => ["id"]},
299                 "from"   => "aou",
300                 "where"  => {
301                     "-and" => [
302                         {"id" => { "=" => {"+circ" => "circ_lib"} }},
303                         {"id" => $orgs}
304                     ]
305                 }
306             }}
307         ]
308     };
309     my $req_timeout = 10800;
310     my $chunk_size = 100;
311     my $progress = 1;
312
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
320             @chunked_ids = ();
321         }
322     }
323     if (scalar(@chunked_ids) > 0) {
324         $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
325     }
326
327     if(@event_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});
333     } else {
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'});
336     }
337
338     return undef;
339 }
340
341
342 __PACKAGE__->register_method(
343     method  => "new_set_circ_lost",
344     api_name    => "open-ils.circ.circulation.set_lost",
345     signature   => q/
346         Sets the copy and related open circulation to lost
347         @param auth
348         @param args : barcode
349     /
350 );
351
352
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 ) = @_;
359
360     my $e = new_editor(authtoken=>$auth, xact=>1);
361     return $e->die_event unless $e->checkauth;
362
363     my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
364         or return $e->die_event;
365
366     my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
367     return $evt if $evt;
368
369     $e->commit;
370     return 1;
371 }
372
373
374 __PACKAGE__->register_method(
375     method  => "set_circ_claims_returned",
376     api_name    => "open-ils.circ.circulation.set_claims_returned",
377     signature => {
378         desc => q/Sets the circ for a given item as claims returned
379                 If a backdate is provided, overdue fines will be voided
380                 back to the backdate/,
381         params => [
382             {desc => 'Authentication token', type => 'string'},
383             {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
384         ],
385         return => {desc => q/1 on success, failure event on error, and 
386             PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the 
387             configured claims return maximum/}
388     }
389 );
390
391 __PACKAGE__->register_method(
392     method  => "set_circ_claims_returned",
393     api_name    => "open-ils.circ.circulation.set_claims_returned.override",
394     signature => {
395         desc => q/This adds support for overrideing the configured max 
396                 claims returned amount. 
397                 @see open-ils.circ.circulation.set_claims_returned./,
398     }
399 );
400
401 sub set_circ_claims_returned {
402     my( $self, $conn, $auth, $args, $oargs ) = @_;
403
404     my $e = new_editor(authtoken=>$auth, xact=>1);
405     return $e->die_event unless $e->checkauth;
406
407     $oargs = { all => 1 } unless defined $oargs;
408
409     my $barcode = $$args{barcode};
410     my $backdate = $$args{backdate};
411
412     my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0] 
413         or return $e->die_event;
414
415     my $circ = $e->search_action_circulation(
416         {checkin_time => undef, target_copy => $copy->id})->[0]
417             or return $e->die_event;
418
419     $backdate = $circ->due_date if $$args{use_due_date};
420
421     $logger->info("marking circ for item $barcode as claims returned".
422         (($backdate) ? " with backdate $backdate" : ''));
423
424     my $patron = $e->retrieve_actor_user($circ->usr);
425     my $max_count = $U->ou_ancestor_setting_value(
426         $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
427
428     # If the patron has too instances of many claims returned, 
429     # require an override to continue.  A configured max of 
430     # 0 means all attempts require an override
431     if(defined $max_count and $patron->claims_returned_count >= $max_count) {
432
433         if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
434
435             # see if we're allowed to override
436             return $e->die_event unless 
437                 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
438
439         } else {
440
441             # exit early and return the max claims return event
442             $e->rollback;
443             return OpenILS::Event->new(
444                 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT', 
445                 payload => {
446                     patron_count => $patron->claims_returned_count,
447                     max_count => $max_count
448                 }
449             );
450         }
451     }
452
453     $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib) 
454         or return $e->die_event;
455
456     $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
457     $circ->stop_fines_time('now') unless $circ->stop_fines_time;
458
459     if( $backdate ) {
460         $backdate = cleanse_ISO8601($backdate);
461
462         my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
463         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
464         $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
465
466         # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
467         $backdate = cleanse_ISO8601($backdate);
468
469         # make it look like the circ stopped at the cliams returned time
470         $circ->stop_fines_time($backdate);
471         my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
472         return $evt if $evt;
473     }
474
475     $e->update_action_circulation($circ) or return $e->die_event;
476
477     # see if there is a configured post-claims-return copy status
478     if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
479         $copy->status($stat);
480         $copy->edit_date('now');
481         $copy->editor($e->requestor->id);
482         $e->update_asset_copy($copy) or return $e->die_event;
483     }
484
485     # Check if the copy circ lib wants lost fees voided on claims
486     # returned.
487     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
488         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
489             $e,
490             $circ,
491             3
492         );
493         if ($result) {
494             $e->rollback;
495             return $result;
496         }
497     }
498
499     # Check if the copy circ lib wants lost processing fees voided on
500     # claims returned.
501     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
502         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
503             $e,
504             $circ,
505             4
506         );
507         if ($result) {
508             $e->rollback;
509             return $result;
510         }
511     }
512
513     # Check if the copy circ lib wants longoverdue fees voided on claims
514     # returned.
515     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
516         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
517             $e,
518             $circ,
519             10
520         );
521         if ($result) {
522             $e->rollback;
523             return $result;
524         }
525     }
526
527     # Check if the copy circ lib wants longoverdue processing fees voided on
528     # claims returned.
529     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
530         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
531             $e,
532             $circ,
533             11
534         );
535         if ($result) {
536             $e->rollback;
537             return $result;
538         }
539     }
540
541     # Now that all data has been munged, do a no-op update of 
542     # the patron to force a change of the last_xact_id value.
543     $e->update_actor_user($e->retrieve_actor_user($circ->usr))
544         or return $e->die_event;
545
546     $e->commit;
547     return 1;
548 }
549
550
551 __PACKAGE__->register_method(
552     method  => "post_checkin_backdate_circ",
553     api_name    => "open-ils.circ.post_checkin_backdate",
554     signature => {
555         desc => q/Back-date an already checked in circulation/,
556         params => [
557             {desc => 'Authentication token', type => 'string'},
558             {desc => 'Circ ID', type => 'number'},
559             {desc => 'ISO8601 backdate', type => 'string'},
560         ],
561         return => {desc => q/1 on success, failure event on error/}
562     }
563 );
564
565 __PACKAGE__->register_method(
566     method  => "post_checkin_backdate_circ",
567     api_name    => "open-ils.circ.post_checkin_backdate.batch",
568     stream => 1,
569     signature => {
570         desc => q/@see open-ils.circ.post_checkin_backdate.  Batch mode/,
571         params => [
572             {desc => 'Authentication token', type => 'string'},
573             {desc => 'List of Circ ID', type => 'array'},
574             {desc => 'ISO8601 backdate', type => 'string'},
575         ],
576         return => {desc => q/Set of: 1 on success, failure event on error/}
577     }
578 );
579
580
581 sub post_checkin_backdate_circ {
582     my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
583     my $e = new_editor(authtoken=>$auth);
584     return $e->die_event unless $e->checkauth;
585     if($self->api_name =~ /batch/) {
586         foreach my $c (@$circ_id) {
587             $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
588         }
589     } else {
590         $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
591     }
592
593     $e->disconnect;
594     return undef;
595 }
596
597
598 sub post_checkin_backdate_circ_impl {
599     my($e, $circ_id, $backdate) = @_;
600
601     $e->xact_begin;
602
603     my $circ = $e->retrieve_action_circulation($circ_id)
604         or return $e->die_event;
605
606     # anyone with checkin perms can backdate (more restrictive?)
607     return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
608
609     # don't allow back-dating an open circulation
610     return OpenILS::Event->new('BAD_PARAMS') unless 
611         $backdate and $circ->checkin_time;
612
613     # update the checkin and stop_fines times to reflect the new backdate
614     $circ->stop_fines_time(cleanse_ISO8601($backdate));
615     $circ->checkin_time(cleanse_ISO8601($backdate));
616     $e->update_action_circulation($circ) or return $e->die_event;
617
618     # now void the overdues "erased" by the back-dating
619     my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
620     return $evt if $evt;
621
622     # If the circ was closed before and the balance owned !=0, re-open the transaction
623     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
624     return $evt if $evt;
625
626     $e->xact_commit;
627     return 1;
628 }
629
630
631
632 __PACKAGE__->register_method (
633     method      => 'set_circ_due_date',
634     api_name        => 'open-ils.circ.circulation.due_date.update',
635     signature   => q/
636         Updates the due_date on the given circ
637         @param authtoken
638         @param circid The id of the circ to update
639         @param date The timestamp of the new due date
640     /
641 );
642
643 sub set_circ_due_date {
644     my( $self, $conn, $auth, $circ_id, $date ) = @_;
645
646     my $e = new_editor(xact=>1, authtoken=>$auth);
647     return $e->die_event unless $e->checkauth;
648     my $circ = $e->retrieve_action_circulation($circ_id)
649         or return $e->die_event;
650
651     return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
652     $date = cleanse_ISO8601($date);
653
654     if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
655         my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
656         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
657         $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
658     }
659
660     $circ->due_date($date);
661     $e->update_action_circulation($circ) or return $e->die_event;
662     $e->commit;
663
664     return $circ;
665 }
666
667
668 __PACKAGE__->register_method(
669     method      => "create_in_house_use",
670     api_name        => 'open-ils.circ.in_house_use.create',
671     signature   =>  q/
672         Creates an in-house use action.
673         @param $authtoken The login session key
674         @param params A hash of params including
675             'location' The org unit id where the in-house use occurs
676             'copyid' The copy in question
677             'count' The number of in-house uses to apply to this copy
678         @return An array of id's representing the id's of the newly created
679         in-house use objects or an event on an error
680     /);
681
682 __PACKAGE__->register_method(
683     method      => "create_in_house_use",
684     api_name        => 'open-ils.circ.non_cat_in_house_use.create',
685 );
686
687
688 sub create_in_house_use {
689     my( $self, $client, $auth, $params ) = @_;
690
691     my( $evt, $copy );
692     my $org         = $params->{location};
693     my $copyid      = $params->{copyid};
694     my $count       = $params->{count} || 1;
695     my $nc_type     = $params->{non_cat_type};
696     my $use_time    = $params->{use_time} || 'now';
697
698     my $e = new_editor(xact=>1,authtoken=>$auth);
699     return $e->event unless $e->checkauth;
700     return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
701
702     my $non_cat = 1 if $self->api_name =~ /non_cat/;
703
704     unless( $non_cat ) {
705         if( $copyid ) {
706             $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
707         } else {
708             $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
709                 or return $e->event;
710             $copyid = $copy->id;
711         }
712     }
713
714     if( $use_time ne 'now' ) {
715         $use_time = cleanse_ISO8601($use_time);
716         $logger->debug("in_house_use setting use time to $use_time");
717     }
718
719     my @ids;
720     for(1..$count) {
721
722         my $ihu;
723         my $method;
724         my $cmeth;
725
726         if($non_cat) {
727             $ihu = Fieldmapper::action::non_cat_in_house_use->new;
728             $ihu->item_type($nc_type);
729             $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
730             $cmeth = "create_action_non_cat_in_house_use";
731
732         } else {
733             $ihu = Fieldmapper::action::in_house_use->new;
734             $ihu->item($copyid);
735             $method = 'open-ils.storage.direct.action.in_house_use.create';
736             $cmeth = "create_action_in_house_use";
737         }
738
739         $ihu->staff($e->requestor->id);
740         $ihu->org_unit($org);
741         $ihu->use_time($use_time);
742
743         $ihu = $e->$cmeth($ihu) or return $e->event;
744         push( @ids, $ihu->id );
745     }
746
747     $e->commit;
748     return \@ids;
749 }
750
751
752
753
754
755 __PACKAGE__->register_method(
756     method  => "view_circs",
757     api_name    => "open-ils.circ.copy_checkout_history.retrieve",
758     notes       => q/
759         Retrieves the last X circs for a given copy
760         @param authtoken The login session key
761         @param copyid The copy to check
762         @param count How far to go back in the item history
763         @return An array of circ ids
764     /);
765
766 # ----------------------------------------------------------------------
767 # Returns $count most recent circs.  If count exceeds the configured 
768 # max, use the configured max instead
769 # ----------------------------------------------------------------------
770 sub view_circs {
771     my( $self, $client, $authtoken, $copyid, $count ) = @_; 
772
773     my $e = new_editor(authtoken => $authtoken);
774     return $e->event unless $e->checkauth;
775     
776     my $copy = $e->retrieve_asset_copy([
777         $copyid,
778         {   flesh => 1,
779             flesh_fields => {acp => ['call_number']}
780         }
781     ]) or return $e->event;
782
783     return $e->event unless $e->allowed(
784         'VIEW_COPY_CHECKOUT_HISTORY', 
785         ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ? 
786             $copy->circ_lib : $copy->call_number->owning_lib);
787         
788     my $max_history = $U->ou_ancestor_setting_value(
789         $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
790
791     if(defined $max_history) {
792         $count = $max_history unless defined $count and $count < $max_history;
793     } else {
794         $count = 4 unless defined $count;
795     }
796
797     return $e->search_action_all_circulation([
798         {target_copy => $copyid}, 
799         {limit => $count, order_by => { combcirc => "xact_start DESC" }} 
800     ]);
801 }
802
803
804 __PACKAGE__->register_method(
805     method  => "circ_count",
806     api_name    => "open-ils.circ.circulation.count",
807     notes       => q/
808         Returns the number of times the item has circulated
809         @param copyid The copy to check
810     /);
811
812 sub circ_count {
813     my( $self, $client, $copyid ) = @_; 
814
815     my $count = new_editor()->json_query({
816         select => {
817             circbyyr => [{
818                 column => 'count',
819                 transform => 'sum',
820                 aggregate => 1
821             }]
822         },
823         from => 'circbyyr',
824         where => {'+circbyyr' => {copy => $copyid}}
825     })->[0]->{count};
826
827     return {
828         total => {
829             when => 'total',
830             count => $count
831         }
832     };
833 }
834
835
836 __PACKAGE__->register_method(
837     method      => 'fetch_notes',
838     authoritative   => 1,
839     api_name        => 'open-ils.circ.copy_note.retrieve.all',
840     signature   => q/
841         Returns an array of copy note objects.  
842         @param args A named hash of parameters including:
843             authtoken   : Required if viewing non-public notes
844             itemid      : The id of the item whose notes we want to retrieve
845             pub         : True if all the caller wants are public notes
846         @return An array of note objects
847     /);
848
849 __PACKAGE__->register_method(
850     method      => 'fetch_notes',
851     api_name        => 'open-ils.circ.call_number_note.retrieve.all',
852     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
853
854 __PACKAGE__->register_method(
855     method      => 'fetch_notes',
856     api_name        => 'open-ils.circ.title_note.retrieve.all',
857     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
858
859
860 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
861 sub fetch_notes {
862     my( $self, $connection, $args ) = @_;
863
864     my $id = $$args{itemid};
865     my $authtoken = $$args{authtoken};
866     my( $r, $evt);
867
868     if( $self->api_name =~ /copy/ ) {
869         if( $$args{pub} ) {
870             return $U->cstorereq(
871                 'open-ils.cstore.direct.asset.copy_note.search.atomic',
872                 { owning_copy => $id, pub => 't' } );
873         } else {
874             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
875             return $evt if $evt;
876             return $U->cstorereq(
877                 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
878         }
879
880     } elsif( $self->api_name =~ /call_number/ ) {
881         if( $$args{pub} ) {
882             return $U->cstorereq(
883                 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
884                 { call_number => $id, pub => 't' } );
885         } else {
886             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
887             return $evt if $evt;
888             return $U->cstorereq(
889                 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
890         }
891
892     } elsif( $self->api_name =~ /title/ ) {
893         if( $$args{pub} ) {
894             return $U->cstorereq(
895                 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
896                 { record => $id, pub => 't' } );
897         } else {
898             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
899             return $evt if $evt;
900             return $U->cstorereq(
901                 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
902         }
903     }
904
905     return undef;
906 }
907
908 __PACKAGE__->register_method(
909     method  => 'has_notes',
910     api_name    => 'open-ils.circ.copy.has_notes');
911 __PACKAGE__->register_method(
912     method  => 'has_notes',
913     api_name    => 'open-ils.circ.call_number.has_notes');
914 __PACKAGE__->register_method(
915     method  => 'has_notes',
916     api_name    => 'open-ils.circ.title.has_notes');
917
918
919 sub has_notes {
920     my( $self, $conn, $authtoken, $id ) = @_;
921     my $editor = new_editor(authtoken => $authtoken);
922     return $editor->event unless $editor->checkauth;
923
924     my $n = $editor->search_asset_copy_note(
925         {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
926
927     $n = $editor->search_asset_call_number_note(
928         {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
929
930     $n = $editor->search_biblio_record_note(
931         {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
932
933     return scalar @$n;
934 }
935
936
937
938 __PACKAGE__->register_method(
939     method      => 'create_copy_note',
940     api_name        => 'open-ils.circ.copy_note.create',
941     signature   => q/
942         Creates a new copy note
943         @param authtoken The login session key
944         @param note The note object to create
945         @return The id of the new note object
946     /);
947
948 sub create_copy_note {
949     my( $self, $connection, $authtoken, $note ) = @_;
950
951     my $e = new_editor(xact=>1, authtoken=>$authtoken);
952     return $e->event unless $e->checkauth;
953     my $copy = $e->retrieve_asset_copy(
954         [
955             $note->owning_copy,
956             {   flesh => 1,
957                 flesh_fields => { 'acp' => ['call_number'] }
958             }
959         ]
960     );
961
962     return $e->event unless 
963         $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
964
965     $note->create_date('now');
966     $note->creator($e->requestor->id);
967     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
968     $note->clear_id;
969
970     $e->create_asset_copy_note($note) or return $e->event;
971     $e->commit;
972     return $note->id;
973 }
974
975
976 __PACKAGE__->register_method(
977     method      => 'delete_copy_note',
978     api_name        =>  'open-ils.circ.copy_note.delete',
979     signature   => q/
980         Deletes an existing copy note
981         @param authtoken The login session key
982         @param noteid The id of the note to delete
983         @return 1 on success - Event otherwise.
984         /);
985 sub delete_copy_note {
986     my( $self, $conn, $authtoken, $noteid ) = @_;
987
988     my $e = new_editor(xact=>1, authtoken=>$authtoken);
989     return $e->die_event unless $e->checkauth;
990
991     my $note = $e->retrieve_asset_copy_note([
992         $noteid,
993         { flesh => 2,
994             flesh_fields => {
995                 'acpn' => [ 'owning_copy' ],
996                 'acp' => [ 'call_number' ],
997             }
998         }
999     ]) or return $e->die_event;
1000
1001     if( $note->creator ne $e->requestor->id ) {
1002         return $e->die_event unless 
1003             $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1004     }
1005
1006     $e->delete_asset_copy_note($note) or return $e->die_event;
1007     $e->commit;
1008     return 1;
1009 }
1010
1011 __PACKAGE__->register_method(
1012     method      => 'fetch_copy_tags',
1013     authoritative   => 1,
1014     api_name        => 'open-ils.circ.copy_tags.retrieve',
1015     signature   => q/
1016         Returns an array of publicly-visible copy tag objects.  
1017         @param args A named hash of parameters including:
1018             copy_id     : The id of the item whose notes we want to retrieve
1019             tag_type    : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1020             scope       : top of org subtree whose copy tags we want to see
1021             depth       : how far down to look for copy tags (optional)
1022         @return An array of copy tag objects
1023     /);
1024 __PACKAGE__->register_method(
1025     method      => 'fetch_copy_tags',
1026     authoritative   => 1,
1027     api_name        => 'open-ils.circ.copy_tags.retrieve.staff',
1028     signature   => q/
1029         Returns an array of all copy tag objects.  
1030         @param args A named hash of parameters including:
1031             authtoken   : Required to view non-public notes
1032             copy_id     : The id of the item whose notes we want to retrieve (optional)
1033             tag_type    : Type of copy tags to retrieve, e.g., 'bookplate'
1034             scope       : top of org subtree whose copy tags we want to see
1035             depth       : how far down to look for copy tags (optional)
1036         @return An array of copy tag objects
1037     /);
1038
1039 sub fetch_copy_tags {
1040     my ($self, $conn, $args) = @_;
1041
1042     my $org = $args->{scope};
1043     my $depth = $args->{depth};
1044
1045     my $filter = {};
1046     my $e;
1047     if ($self->api_name =~ /\.staff/) {
1048         my $authtoken = $args->{authtoken};
1049         return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;    
1050         $e = new_editor(authtoken => $args->{authtoken});
1051         return $e->event unless $e->checkauth;
1052         return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1053     } else {
1054         $e = new_editor();
1055         $filter->{pub} = 't';
1056     }
1057     $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1058     $filter->{'+acptcm'} = {
1059         copy => $args->{copy_id}
1060     };
1061
1062     # filter by owner of copy tag and depth
1063     $filter->{owner} = {
1064         in => {
1065             select => {aou => [{
1066                 column => 'id',
1067                 transform => 'actor.org_unit_descendants',
1068                 result_field => 'id',
1069                 (defined($depth) ? ( params => [$depth] ) : ()),
1070             }]},
1071             from => 'aou',
1072             where => {id => $org}
1073         }
1074     };
1075
1076     return $e->search_asset_copy_tag([$filter, { join => { acptcm => {} } }]);
1077 }
1078
1079
1080 __PACKAGE__->register_method(
1081     method => 'age_hold_rules',
1082     api_name    =>  'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1083 );
1084
1085 sub age_hold_rules {
1086     my( $self, $conn ) = @_;
1087     return new_editor()->retrieve_all_config_rules_age_hold_protect();
1088 }
1089
1090
1091
1092 __PACKAGE__->register_method(
1093     method => 'copy_details_barcode',
1094     authoritative => 1,
1095     api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1096 sub copy_details_barcode {
1097     my( $self, $conn, $auth, $barcode ) = @_;
1098     my $e = new_editor();
1099     my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1100     return $e->event unless $cid;
1101     return copy_details( $self, $conn, $auth, $cid );
1102 }
1103
1104
1105 __PACKAGE__->register_method(
1106     method => 'copy_details',
1107     api_name => 'open-ils.circ.copy_details.retrieve');
1108
1109 sub copy_details {
1110     my( $self, $conn, $auth, $copy_id ) = @_;
1111     my $e = new_editor(authtoken=>$auth);
1112     return $e->event unless $e->checkauth;
1113
1114     my $flesh = { flesh => 1 };
1115
1116     my $copy = $e->retrieve_asset_copy(
1117         [
1118             $copy_id,
1119             {
1120                 flesh => 2,
1121                 flesh_fields => {
1122                     acp => ['call_number','parts','peer_record_maps','floating'],
1123                     acn => ['record','prefix','suffix','label_class']
1124                 }
1125             }
1126         ]) or return $e->event;
1127
1128
1129     # De-flesh the copy for backwards compatibility
1130     my $mvr;
1131     my $vol = $copy->call_number;
1132     if( ref $vol ) {
1133         $copy->call_number($vol->id);
1134         my $record = $vol->record;
1135         if( ref $record ) {
1136             $vol->record($record->id);
1137             $mvr = $U->record_to_mvr($record);
1138         }
1139     }
1140
1141
1142     my $hold = $e->search_action_hold_request(
1143         { 
1144             current_copy        => $copy_id, 
1145             capture_time        => { "!=" => undef },
1146             fulfillment_time    => undef,
1147             cancel_time         => undef,
1148         }
1149     )->[0];
1150
1151     OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1152
1153     my $transit = $e->search_action_transit_copy(
1154         { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1155
1156     # find the most recent circulation for the requested copy,
1157     # be it active, completed, or aged.
1158     my $circ = $e->search_action_all_circulation([
1159         { target_copy => $copy_id },
1160         {
1161             flesh => 1,
1162             flesh_fields => {
1163                 combcirc => [
1164                     'workstation',
1165                     'checkin_workstation',
1166                     'duration_rule',
1167                     'max_fine_rule',
1168                     'recurring_fine_rule'
1169                 ],
1170             },
1171             order_by => { combcirc => 'xact_start desc' },
1172             limit => 1
1173         }
1174     ])->[0];
1175
1176     return {
1177         copy    => $copy,
1178         hold    => $hold,
1179         transit => $transit,
1180         circ    => $circ,
1181         volume  => $vol,
1182         mvr     => $mvr
1183     };
1184 }
1185
1186
1187
1188
1189 __PACKAGE__->register_method(
1190     method => 'mark_item',
1191     api_name => 'open-ils.circ.mark_item_damaged',
1192     signature   => q/
1193         Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1194         @param authtoken The login session key
1195         @param copy_id The ID of the copy to mark as damaged
1196         @return 1 on success - Event otherwise.
1197         /
1198 );
1199 __PACKAGE__->register_method(
1200     method => 'mark_item',
1201     api_name => 'open-ils.circ.mark_item_missing',
1202     signature   => q/
1203         Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1204         @param authtoken The login session key
1205         @param copy_id The ID of the copy to mark as missing 
1206         @return 1 on success - Event otherwise.
1207         /
1208 );
1209 __PACKAGE__->register_method(
1210     method => 'mark_item',
1211     api_name => 'open-ils.circ.mark_item_bindery',
1212     signature   => q/
1213         Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1214         @param authtoken The login session key
1215         @param copy_id The ID of the copy to mark as bindery
1216         @return 1 on success - Event otherwise.
1217         /
1218 );
1219 __PACKAGE__->register_method(
1220     method => 'mark_item',
1221     api_name => 'open-ils.circ.mark_item_on_order',
1222     signature   => q/
1223         Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1224         @param authtoken The login session key
1225         @param copy_id The ID of the copy to mark as on order 
1226         @return 1 on success - Event otherwise.
1227         /
1228 );
1229 __PACKAGE__->register_method(
1230     method => 'mark_item',
1231     api_name => 'open-ils.circ.mark_item_ill',
1232     signature   => q/
1233         Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1234         @param authtoken The login session key
1235         @param copy_id The ID of the copy to mark as inter-library loan
1236         @return 1 on success - Event otherwise.
1237         /
1238 );
1239 __PACKAGE__->register_method(
1240     method => 'mark_item',
1241     api_name => 'open-ils.circ.mark_item_cataloging',
1242     signature   => q/
1243         Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1244         @param authtoken The login session key
1245         @param copy_id The ID of the copy to mark as cataloging 
1246         @return 1 on success - Event otherwise.
1247         /
1248 );
1249 __PACKAGE__->register_method(
1250     method => 'mark_item',
1251     api_name => 'open-ils.circ.mark_item_reserves',
1252     signature   => q/
1253         Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1254         @param authtoken The login session key
1255         @param copy_id The ID of the copy to mark as reserves
1256         @return 1 on success - Event otherwise.
1257         /
1258 );
1259 __PACKAGE__->register_method(
1260     method => 'mark_item',
1261     api_name => 'open-ils.circ.mark_item_discard',
1262     signature   => q/
1263         Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1264         @param authtoken The login session key
1265         @param copy_id The ID of the copy to mark as discard
1266         @return 1 on success - Event otherwise.
1267         /
1268 );
1269
1270 sub mark_item {
1271     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1272     my $e = new_editor(authtoken=>$auth, xact =>1);
1273     return $e->die_event unless $e->checkauth;
1274     $args ||= {};
1275
1276     my $copy = $e->retrieve_asset_copy([
1277         $copy_id,
1278         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1279             or return $e->die_event;
1280
1281     my $owning_lib = 
1282         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1283             $copy->circ_lib : $copy->call_number->owning_lib;
1284
1285     my $perm = 'MARK_ITEM_MISSING';
1286     my $stat = OILS_COPY_STATUS_MISSING;
1287
1288     if( $self->api_name =~ /damaged/ ) {
1289         $perm = 'MARK_ITEM_DAMAGED';
1290         $stat = OILS_COPY_STATUS_DAMAGED;
1291         my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1292         return $evt if $evt;
1293
1294     } elsif ( $self->api_name =~ /bindery/ ) {
1295         $perm = 'MARK_ITEM_BINDERY';
1296         $stat = OILS_COPY_STATUS_BINDERY;
1297     } elsif ( $self->api_name =~ /on_order/ ) {
1298         $perm = 'MARK_ITEM_ON_ORDER';
1299         $stat = OILS_COPY_STATUS_ON_ORDER;
1300     } elsif ( $self->api_name =~ /ill/ ) {
1301         $perm = 'MARK_ITEM_ILL';
1302         $stat = OILS_COPY_STATUS_ILL;
1303     } elsif ( $self->api_name =~ /cataloging/ ) {
1304         $perm = 'MARK_ITEM_CATALOGING';
1305         $stat = OILS_COPY_STATUS_CATALOGING;
1306     } elsif ( $self->api_name =~ /reserves/ ) {
1307         $perm = 'MARK_ITEM_RESERVES';
1308         $stat = OILS_COPY_STATUS_RESERVES;
1309     } elsif ( $self->api_name =~ /discard/ ) {
1310         $perm = 'MARK_ITEM_DISCARD';
1311         $stat = OILS_COPY_STATUS_DISCARD;
1312     }
1313
1314     # caller may proceed if either perm is allowed
1315     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1316
1317     $copy->status($stat);
1318     $copy->edit_date('now');
1319     $copy->editor($e->requestor->id);
1320
1321     $e->update_asset_copy($copy) or return $e->die_event;
1322
1323     my $holds = $e->search_action_hold_request(
1324         { 
1325             current_copy => $copy->id,
1326             fulfillment_time => undef,
1327             cancel_time => undef,
1328         }
1329     );
1330
1331     $e->commit;
1332
1333     if( $self->api_name =~ /damaged/ ) {
1334         # now that we've committed the changes, create related A/T events
1335         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1336         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1337     }
1338
1339     $logger->debug("resetting holds that target the marked copy");
1340     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1341
1342     return 1;
1343 }
1344
1345 sub handle_mark_damaged {
1346     my($e, $copy, $owning_lib, $args) = @_;
1347
1348     my $apply = $args->{apply_fines} || '';
1349     return undef if $apply eq 'noapply';
1350
1351     my $new_amount = $args->{override_amount};
1352     my $new_btype = $args->{override_btype};
1353     my $new_note = $args->{override_note};
1354
1355     # grab the last circulation
1356     my $circ = $e->search_action_circulation([
1357         {   target_copy => $copy->id}, 
1358         {   limit => 1, 
1359             order_by => {circ => "xact_start DESC"},
1360             flesh => 2,
1361             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1362         }
1363     ])->[0];
1364
1365     return undef unless $circ;
1366
1367     my $charge_price = $U->ou_ancestor_setting_value(
1368         $owning_lib, 'circ.charge_on_damaged', $e);
1369
1370     my $proc_fee = $U->ou_ancestor_setting_value(
1371         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1372
1373     my $void_overdue = $U->ou_ancestor_setting_value(
1374         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1375
1376     return undef unless $charge_price or $proc_fee;
1377
1378     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1379     my $total = $copy_price + $proc_fee;
1380
1381     if($apply) {
1382         
1383         if($new_amount and $new_btype) {
1384
1385             # Allow staff to override the amount to charge for a damaged item
1386             # Consider the case where the item is only partially damaged
1387             # This value is meant to take the place of the item price and
1388             # optional processing fee.
1389
1390             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1391                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1392             return $evt if $evt;
1393
1394         } else {
1395
1396             if($charge_price and $copy_price) {
1397                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1398                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1399                 return $evt if $evt;
1400             }
1401
1402             if($proc_fee) {
1403                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1404                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1405                 return $evt if $evt;
1406             }
1407         }
1408
1409         # the assumption is that you would not void the overdues unless you 
1410         # were also charging for the item and/or applying a processing fee
1411         if($void_overdue) {
1412             my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1413             return $evt if $evt;
1414         }
1415
1416         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1417         return $evt if $evt;
1418
1419         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1420         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1421
1422         my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1423         return $evt2 if $evt2;
1424
1425         return undef;
1426
1427     } else {
1428         return OpenILS::Event->new('DAMAGE_CHARGE', 
1429             payload => {
1430                 circ => $circ,
1431                 charge => $total
1432             }
1433         );
1434     }
1435 }
1436
1437
1438
1439 # ----------------------------------------------------------------------
1440 __PACKAGE__->register_method(
1441     method => 'mark_item_missing_pieces',
1442     api_name => 'open-ils.circ.mark_item_missing_pieces',
1443     signature   => q/
1444         Changes the status of a copy to "damaged" or to a custom status based on the 
1445         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1446         permission.
1447         @param authtoken The login session key
1448         @param copy_id The ID of the copy to mark as damaged
1449         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1450         /
1451 );
1452
1453 sub mark_item_missing_pieces {
1454     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1455     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1456     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1457
1458     my $e2 = new_editor(authtoken=>$auth, xact =>1);
1459     return $e2->die_event unless $e2->checkauth;
1460     $args ||= {};
1461
1462     my $copy = $e2->retrieve_asset_copy([
1463         $copy_id,
1464         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1465             or return $e2->die_event;
1466
1467     my $owning_lib = 
1468         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1469             $copy->circ_lib : $copy->call_number->owning_lib;
1470
1471     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1472
1473     #### grab the last circulation
1474     my $circ = $e2->search_action_circulation([
1475         {   target_copy => $copy->id}, 
1476         {   limit => 1, 
1477             order_by => {circ => "xact_start DESC"}
1478         }
1479     ])->[0];
1480
1481     if (!$circ) {
1482         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1483         $e2->rollback;
1484         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1485     }
1486
1487     my $holds = $e2->search_action_hold_request(
1488         { 
1489             current_copy => $copy->id,
1490             fulfillment_time => undef,
1491             cancel_time => undef,
1492         }
1493     );
1494
1495     $logger->debug("resetting holds that target the marked copy");
1496     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1497
1498     
1499     if (! $e2->commit) {
1500         return $e2->die_event;
1501     }
1502
1503     my $e = new_editor(authtoken=>$auth, xact =>1);
1504     return $e->die_event unless $e->checkauth;
1505
1506     if (! $circ->checkin_time) { # if circ active, attempt renew
1507         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1508         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1509         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1510             $circ = $res->[0]->{payload}{'circ'};
1511             $circ->target_copy( $copy->id );
1512             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1513         } else {
1514             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1515         }
1516     } else {
1517
1518         my $co_params = {
1519             'copy_id'=>$circ->target_copy,
1520             'patron_id'=>$circ->usr,
1521             'skip_deposit_fee'=>1,
1522             'skip_rental_fee'=>1
1523         };
1524
1525         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1526
1527             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1528                 $e, $copy, $e->requestor, 1 );
1529
1530             if ($hold) { # needed for hold? then due now
1531
1532                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1533                 my $due_date = DateTime->now(time_zone => 'local');
1534                 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1535             } else {
1536                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1537             }
1538         }
1539
1540         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1541         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1542         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1543             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1544             $circ = $res->[0]->{payload}{'circ'};
1545         } else {
1546             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1547             $e->rollback;
1548             return $res;
1549         }
1550     }
1551
1552     ### Update the item status
1553
1554     my $custom_stat = $U->ou_ancestor_setting_value(
1555         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1556     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1557
1558     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1559     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1560
1561     $copy->status($stat);
1562     $copy->edit_date('now');
1563     $copy->editor($e->requestor->id);
1564
1565     $e->update_asset_copy($copy) or return $e->die_event;
1566
1567     if ($e->commit) {
1568
1569         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1570         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1571
1572         return OpenILS::Event->new('SUCCESS',
1573             payload => {
1574                 circ => $circ,
1575                 copy => $copy,
1576                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1577                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1578             }
1579         ); 
1580
1581     } else {
1582         return $e->die_event;
1583     }
1584 }
1585
1586
1587
1588
1589
1590 # ----------------------------------------------------------------------
1591 __PACKAGE__->register_method(
1592     method => 'magic_fetch',
1593     api_name => 'open-ils.agent.fetch'
1594 );
1595
1596 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1597
1598 sub magic_fetch {
1599     my( $self, $conn, $auth, $args ) = @_;
1600     my $e = new_editor( authtoken => $auth );
1601     return $e->event unless $e->checkauth;
1602
1603     my $hint = $$args{hint};
1604     my $id  = $$args{id};
1605
1606     # Is the call allowed to fetch this type of object?
1607     return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1608
1609     # Find the class the implements the given hint
1610     my ($class) = grep { 
1611         $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1612
1613     $class =~ s/Fieldmapper:://og;
1614     $class =~ s/::/_/og;
1615     my $method = "retrieve_$class";
1616
1617     my $obj = $e->$method($id) or return $e->event;
1618     return $obj;
1619 }
1620 # ----------------------------------------------------------------------
1621
1622
1623 __PACKAGE__->register_method(
1624     method  => "fleshed_circ_retrieve",
1625     authoritative => 1,
1626     api_name    => "open-ils.circ.fleshed.retrieve",);
1627
1628 sub fleshed_circ_retrieve {
1629     my( $self, $client, $id ) = @_;
1630     my $e = new_editor();
1631     my $circ = $e->retrieve_action_circulation(
1632         [
1633             $id,
1634             { 
1635                 flesh               => 4,
1636                 flesh_fields    => { 
1637                     circ => [ qw/ target_copy / ],
1638                     acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1639                     ascecm => [ qw/ stat_cat stat_cat_entry / ],
1640                     acn => [ qw/ record / ],
1641                 }
1642             }
1643         ]
1644     ) or return $e->event;
1645     
1646     my $copy = $circ->target_copy;
1647     my $vol = $copy->call_number;
1648     my $rec = $circ->target_copy->call_number->record;
1649
1650     $vol->record($rec->id);
1651     $copy->call_number($vol->id);
1652     $circ->target_copy($copy->id);
1653
1654     my $mvr;
1655
1656     if( $rec->id == OILS_PRECAT_RECORD ) {
1657         $rec = undef;
1658         $vol = undef;
1659     } else { 
1660         $mvr = $U->record_to_mvr($rec);
1661         $rec->marc(''); # drop the bulky marc data
1662     }
1663
1664     return {
1665         circ => $circ,
1666         copy => $copy,
1667         volume => $vol,
1668         record => $rec,
1669         mvr => $mvr,
1670     };
1671 }
1672
1673
1674
1675 __PACKAGE__->register_method(
1676     method  => "test_batch_circ_events",
1677     api_name    => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1678 );
1679
1680 #  method for testing the behavior of a given event definition
1681 sub test_batch_circ_events {
1682     my($self, $conn, $auth, $event_def, $barcode) = @_;
1683
1684     my $e = new_editor(authtoken => $auth);
1685     return $e->event unless $e->checkauth;
1686     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1687
1688     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1689         or return $e->event;
1690
1691     my $circ = $e->search_action_circulation(
1692         {target_copy => $copy->id, checkin_time => undef})->[0]
1693         or return $e->event;
1694         
1695     return undef unless $circ;
1696
1697     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1698 }
1699
1700
1701 __PACKAGE__->register_method(
1702     method  => "fire_circ_events", 
1703     api_name    => "open-ils.circ.fire_circ_trigger_events",
1704     signature => q/
1705         General event def runner for circ objects.  If no event def ID
1706         is provided, the hook will be used to find the best event_def
1707         match based on the context org unit
1708     /
1709 );
1710
1711 __PACKAGE__->register_method(
1712     method  => "fire_circ_events", 
1713     api_name    => "open-ils.circ.fire_hold_trigger_events",
1714     signature => q/
1715         General event def runner for hold objects.  If no event def ID
1716         is provided, the hook will be used to find the best event_def
1717         match based on the context org unit
1718     /
1719 );
1720
1721 __PACKAGE__->register_method(
1722     method  => "fire_circ_events", 
1723     api_name    => "open-ils.circ.fire_user_trigger_events",
1724     signature => q/
1725         General event def runner for user objects.  If no event def ID
1726         is provided, the hook will be used to find the best event_def
1727         match based on the context org unit
1728     /
1729 );
1730
1731
1732 sub fire_circ_events {
1733     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1734
1735     my $e = new_editor(authtoken => $auth, xact => 1);
1736     return $e->event unless $e->checkauth;
1737
1738     my $targets;
1739
1740     if($self->api_name =~ /hold/) {
1741         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1742         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1743     } elsif($self->api_name =~ /user/) {
1744         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1745         $targets = $e->batch_retrieve_actor_user($target_ids);
1746     } else {
1747         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1748         $targets = $e->batch_retrieve_action_circulation($target_ids);
1749     }
1750     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1751                   # simply making this method authoritative because of weirdness
1752                   # with transaction handling in A/T code that causes rollback
1753                   # failure down the line if handling many targets
1754
1755     return undef unless @$targets;
1756     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1757 }
1758
1759 __PACKAGE__->register_method(
1760     method  => "user_payments_list",
1761     api_name    => "open-ils.circ.user_payments.filtered.batch",
1762     stream => 1,
1763     signature => {
1764         desc => q/Returns a fleshed, date-limited set of all payments a user
1765                 has made.  By default, ordered by payment date.  Optionally
1766                 ordered by other columns in the top-level "mp" object/,
1767         params => [
1768             {desc => 'Authentication token', type => 'string'},
1769             {desc => 'User ID', type => 'number'},
1770             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1771         ],
1772         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1773             and the related fully-realized payment object (e.g money.cash_payment)/}
1774     }
1775 );
1776
1777 sub user_payments_list {
1778     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1779
1780     my $e = new_editor(authtoken => $auth);
1781     return $e->event unless $e->checkauth;
1782
1783     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1784     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1785
1786     $order_by ||= ['payment_ts'];
1787
1788     # all payments by user, between start_date and end_date
1789     my $payments = $e->json_query({
1790         select => {mp => ['id']}, 
1791         from => {
1792             mp => {
1793                 mbt => {
1794                     fkey => 'xact', field => 'id'}
1795             }
1796         }, 
1797         where => {
1798             '+mbt' => {usr => $user_id}, 
1799             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1800         },
1801         order_by => {mp => $order_by}
1802     });
1803
1804     for my $payment_id (@$payments) {
1805         my $payment = $e->retrieve_money_payment([
1806             $payment_id->{id}, 
1807             {   
1808                 flesh => 2,
1809                 flesh_fields => {
1810                     mp => [
1811                         'xact',
1812                         'cash_payment',
1813                         'credit_card_payment',
1814                         'credit_payment',
1815                         'check_payment',
1816                         'work_payment',
1817                         'forgive_payment',
1818                         'goods_payment'
1819                     ],
1820                     mbt => [
1821                         'circulation', 
1822                         'grocery',
1823                         'reservation'
1824                     ]
1825                 }
1826             }
1827         ]);
1828         $conn->respond($payment);
1829     }
1830
1831     return undef;
1832 }
1833
1834
1835 __PACKAGE__->register_method(
1836     method  => "retrieve_circ_chain",
1837     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ",
1838     stream => 1,
1839     signature => {
1840         desc => q/Given a circulation, this returns all circulation objects
1841                 that are part of the same chain of renewals./,
1842         params => [
1843             {desc => 'Authentication token', type => 'string'},
1844             {desc => 'Circ ID', type => 'number'},
1845         ],
1846         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1847     }
1848 );
1849
1850 __PACKAGE__->register_method(
1851     method  => "retrieve_circ_chain",
1852     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1853     signature => {
1854         desc => q/Given a circulation, this returns a summary of the circulation objects
1855                 that are part of the same chain of renewals./,
1856         params => [
1857             {desc => 'Authentication token', type => 'string'},
1858             {desc => 'Circ ID', type => 'number'},
1859         ],
1860         return => {desc => q/Circulation Chain Summary/}
1861     }
1862 );
1863
1864 sub retrieve_circ_chain {
1865     my($self, $conn, $auth, $circ_id) = @_;
1866
1867     my $e = new_editor(authtoken => $auth);
1868     return $e->event unless $e->checkauth;
1869     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1870
1871     if($self->api_name =~ /summary/) {
1872         return $U->create_circ_chain_summary($e, $circ_id);
1873
1874     } else {
1875
1876         my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
1877
1878         for my $circ_info (@$chain) {
1879             my $circ = Fieldmapper::action::all_circulation->new;
1880             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1881             $conn->respond($circ);
1882         }
1883     }
1884
1885     return undef;
1886 }
1887
1888 __PACKAGE__->register_method(
1889     method  => "retrieve_prev_circ_chain",
1890     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1891     stream => 1,
1892     signature => {
1893         desc => q/Given a circulation, this returns all circulation objects
1894                 that are part of the previous chain of renewals./,
1895         params => [
1896             {desc => 'Authentication token', type => 'string'},
1897             {desc => 'Circ ID', type => 'number'},
1898         ],
1899         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1900     }
1901 );
1902
1903 __PACKAGE__->register_method(
1904     method  => "retrieve_prev_circ_chain",
1905     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1906     signature => {
1907         desc => q/Given a circulation, this returns a summary of the circulation objects
1908                 that are part of the previous chain of renewals./,
1909         params => [
1910             {desc => 'Authentication token', type => 'string'},
1911             {desc => 'Circ ID', type => 'number'},
1912         ],
1913         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1914     }
1915 );
1916
1917 sub retrieve_prev_circ_chain {
1918     my($self, $conn, $auth, $circ_id) = @_;
1919
1920     my $e = new_editor(authtoken => $auth);
1921     return $e->event unless $e->checkauth;
1922     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1923
1924     my $first_circ = 
1925         $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
1926
1927     my $prev_circ = $e->search_action_all_circulation([
1928         {   target_copy => $first_circ->{target_copy},
1929             xact_start => {'<' => $first_circ->{xact_start}}
1930         }, {   
1931             flesh => 1,
1932             flesh_fields => {
1933                 combcirc => [
1934                     'active_circ',
1935                     'aged_circ'
1936                 ]
1937             },
1938             order_by => { combcirc => 'xact_start desc' },
1939             limit => 1 
1940         }
1941     ])->[0];
1942
1943     return undef unless $prev_circ;
1944
1945     my $chain_usr = $prev_circ->usr; # note: may be undef
1946
1947     if ($self->api_name =~ /summary/) {
1948         my $sum = $e->json_query({
1949             from => [
1950                 'action.summarize_all_circ_chain', 
1951                 $prev_circ->id
1952             ]
1953         })->[0];
1954
1955         my $summary = Fieldmapper::action::circ_chain_summary->new;
1956         $summary->$_($sum->{$_}) for keys %$sum;
1957
1958         return {summary => $summary, usr => $chain_usr};
1959     }
1960
1961
1962     my $chain = $e->json_query(
1963         {from => ['action.all_circ_chain', $prev_circ->id]});
1964
1965     for my $circ_info (@$chain) {
1966         my $circ = Fieldmapper::action::all_circulation->new;
1967         $circ->$_($circ_info->{$_}) for keys %$circ_info;
1968         $conn->respond($circ);
1969     }
1970
1971     return undef;
1972 }
1973
1974
1975 __PACKAGE__->register_method(
1976     method  => "get_copy_due_date",
1977     api_name    => "open-ils.circ.copy.due_date.retrieve",
1978     signature => {
1979         desc => q/
1980             Given a copy ID, returns the due date for the copy if it's 
1981             currently circulating.  Otherwise, returns null.  Note, this is a public 
1982             method requiring no authentication.  Only the due date is exposed.
1983             /,
1984         params => [
1985             {desc => 'Copy ID', type => 'number'}
1986         ],
1987         return => {desc => q/
1988             Due date (ISO date stamp) if the copy is circulating, null otherwise.
1989         /}
1990     }
1991 );
1992
1993 sub get_copy_due_date {
1994     my($self, $conn, $copy_id) = @_;
1995     my $e = new_editor();
1996
1997     my $circ = $e->json_query({
1998         select => {circ => ['due_date']},
1999         from => 'circ',
2000         where => {
2001             target_copy => $copy_id,
2002             checkin_time => undef,
2003             '-or' => [
2004                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2005                 {stop_fines => undef}
2006             ],
2007         },
2008         limit => 1
2009     })->[0] or return undef;
2010
2011     return $circ->{due_date};
2012 }
2013
2014
2015
2016
2017
2018 # {"select":{"acp":["id"],"circ":[{"aggregate":true,"transform":"count","alias":"count","column":"id"}]},"from":{"acp":{"circ":{"field":"target_copy","fkey":"id","type":"left"},"acn"{"field":"id","fkey":"call_number"}}},"where":{"+acn":{"record":200057}}
2019
2020
2021 1;