]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
LP 2061136 follow-up: ng lint --fix
[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 OpenILS::Utils::DateTime 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_slim 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_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');
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 __PACKAGE__->register_method(
374     method    => "update_copy_inventory",
375     api_name  => "open-ils.circ.circulation.update_copy_inventory");
376
377 sub update_copy_inventory {
378     my( $self, $conn, $auth, $args ) = @_;
379     my $e = new_editor(authtoken=>$auth);
380     return [0, 0, $e->die_event] unless $e->checkauth;
381
382     my ($success,$failure) = (0,0);
383     my $spname = "aci_update_savepoint";
384     $e->xact_begin();
385     my $copies = $$args{copy_list};
386     foreach my $copyid (@$copies) {
387         my $aci = Fieldmapper::asset::copy_inventory->new;
388         $aci->inventory_date('now');
389         $aci->inventory_workstation($e->requestor->wsid);
390         $aci->copy($copyid);
391         $e->set_savepoint($spname);
392         if ($e->create_asset_copy_inventory($aci)) {
393             $success++;
394         } else {
395             $failure++;
396             $e->rollback_savepoint($spname);
397         }
398         $e->release_savepoint($spname);
399     }
400     if ($success) {
401         $e->commit();
402     } else {
403         $e->rollback();
404     }
405
406     return [ $success, $failure ];
407 }
408
409 __PACKAGE__->register_method(
410     method  => "set_circ_claims_returned",
411     api_name    => "open-ils.circ.circulation.set_claims_returned",
412     signature => {
413         desc => q/Sets the circ for a given item as claims returned
414                 If a backdate is provided, overdue fines will be voided
415                 back to the backdate/,
416         params => [
417             {desc => 'Authentication token', type => 'string'},
418             {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
419         ],
420         return => {desc => q/1 on success, failure event on error, and 
421             PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the 
422             configured claims return maximum/}
423     }
424 );
425
426 __PACKAGE__->register_method(
427     method  => "set_circ_claims_returned",
428     api_name    => "open-ils.circ.circulation.set_claims_returned.override",
429     signature => {
430         desc => q/This adds support for overrideing the configured max 
431                 claims returned amount. 
432                 @see open-ils.circ.circulation.set_claims_returned./,
433     }
434 );
435
436 sub set_circ_claims_returned {
437     my( $self, $conn, $auth, $args, $oargs ) = @_;
438
439     my $e = new_editor(authtoken=>$auth, xact=>1);
440     return $e->die_event unless $e->checkauth;
441
442     $oargs = { all => 1 } unless defined $oargs;
443
444     my $barcode = $$args{barcode};
445     my $backdate = $$args{backdate};
446
447     my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0] 
448         or return $e->die_event;
449
450     my $circ = $e->search_action_circulation(
451         {checkin_time => undef, target_copy => $copy->id})->[0]
452             or return $e->die_event;
453
454     $backdate = $circ->due_date if $$args{use_due_date};
455
456     $logger->info("marking circ for item $barcode as claims returned".
457         (($backdate) ? " with backdate $backdate" : ''));
458
459     my $patron = $e->retrieve_actor_user($circ->usr);
460     my $max_count = $U->ou_ancestor_setting_value(
461         $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
462
463     # If the patron has too instances of many claims returned, 
464     # require an override to continue.  A configured max of 
465     # 0 means all attempts require an override
466     if(defined $max_count and $patron->claims_returned_count >= $max_count) {
467
468         if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
469
470             # see if we're allowed to override
471             return $e->die_event unless 
472                 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
473
474         } else {
475
476             # exit early and return the max claims return event
477             $e->rollback;
478             return OpenILS::Event->new(
479                 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT', 
480                 payload => {
481                     patron_count => $patron->claims_returned_count,
482                     max_count => $max_count
483                 }
484             );
485         }
486     }
487
488     $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib) 
489         or return $e->die_event;
490
491     $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
492     $circ->stop_fines_time('now') unless $circ->stop_fines_time;
493
494     if( $backdate ) {
495         $backdate = clean_ISO8601($backdate);
496
497         my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
498         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
499         $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
500
501         # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
502         $backdate = clean_ISO8601($backdate);
503
504         # make it look like the circ stopped at the cliams returned time
505         $circ->stop_fines_time($backdate);
506         my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
507         return $evt if $evt;
508     }
509
510     $e->update_action_circulation($circ) or return $e->die_event;
511
512     # see if there is a configured post-claims-return copy status
513     if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
514         $copy->status($stat);
515         $copy->edit_date('now');
516         $copy->editor($e->requestor->id);
517         $e->update_asset_copy($copy) or return $e->die_event;
518     }
519
520     # Check if the copy circ lib wants lost fees voided on claims
521     # returned.
522     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
523         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
524             $e,
525             $circ,
526             3
527         );
528         if ($result) {
529             $e->rollback;
530             return $result;
531         }
532     }
533
534     # Check if the copy circ lib wants lost processing fees voided on
535     # claims returned.
536     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
537         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
538             $e,
539             $circ,
540             4
541         );
542         if ($result) {
543             $e->rollback;
544             return $result;
545         }
546     }
547
548     # Check if the copy circ lib wants longoverdue fees voided on claims
549     # returned.
550     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
551         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
552             $e,
553             $circ,
554             10
555         );
556         if ($result) {
557             $e->rollback;
558             return $result;
559         }
560     }
561
562     # Check if the copy circ lib wants longoverdue processing fees voided on
563     # claims returned.
564     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
565         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
566             $e,
567             $circ,
568             11
569         );
570         if ($result) {
571             $e->rollback;
572             return $result;
573         }
574     }
575
576     # Now that all data has been munged, do a no-op update of 
577     # the patron to force a change of the last_xact_id value.
578     $e->update_actor_user($e->retrieve_actor_user($circ->usr))
579         or return $e->die_event;
580
581     $e->commit;
582     return 1;
583 }
584
585
586 __PACKAGE__->register_method(
587     method  => "post_checkin_backdate_circ",
588     api_name    => "open-ils.circ.post_checkin_backdate",
589     signature => {
590         desc => q/Back-date an already checked in circulation/,
591         params => [
592             {desc => 'Authentication token', type => 'string'},
593             {desc => 'Circ ID', type => 'number'},
594             {desc => 'ISO8601 backdate', type => 'string'},
595         ],
596         return => {desc => q/1 on success, failure event on error/}
597     }
598 );
599
600 __PACKAGE__->register_method(
601     method  => "post_checkin_backdate_circ",
602     api_name    => "open-ils.circ.post_checkin_backdate.batch",
603     stream => 1,
604     signature => {
605         desc => q/@see open-ils.circ.post_checkin_backdate.  Batch mode/,
606         params => [
607             {desc => 'Authentication token', type => 'string'},
608             {desc => 'List of Circ ID', type => 'array'},
609             {desc => 'ISO8601 backdate', type => 'string'},
610         ],
611         return => {desc => q/Set of: 1 on success, failure event on error/}
612     }
613 );
614
615
616 sub post_checkin_backdate_circ {
617     my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
618     my $e = new_editor(authtoken=>$auth);
619     return $e->die_event unless $e->checkauth;
620     if($self->api_name =~ /batch/) {
621         foreach my $c (@$circ_id) {
622             $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
623         }
624     } else {
625         $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
626     }
627
628     $e->disconnect;
629     return undef;
630 }
631
632
633 sub post_checkin_backdate_circ_impl {
634     my($e, $circ_id, $backdate) = @_;
635
636     $e->xact_begin;
637
638     my $circ = $e->retrieve_action_circulation($circ_id)
639         or return $e->die_event;
640
641     # anyone with checkin perms can backdate (more restrictive?)
642     return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
643
644     # don't allow back-dating an open circulation
645     return OpenILS::Event->new('BAD_PARAMS') unless 
646         $backdate and $circ->checkin_time;
647
648     # update the checkin and stop_fines times to reflect the new backdate
649     $circ->stop_fines_time(clean_ISO8601($backdate));
650     $circ->checkin_time(clean_ISO8601($backdate));
651     $e->update_action_circulation($circ) or return $e->die_event;
652
653     # now void the overdues "erased" by the back-dating
654     my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
655     return $evt if $evt;
656
657     # If the circ was closed before and the balance owned !=0, re-open the transaction
658     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
659     return $evt if $evt;
660
661     $e->xact_commit;
662     return 1;
663 }
664
665
666
667 __PACKAGE__->register_method (
668     method      => 'set_circ_due_date',
669     api_name        => 'open-ils.circ.circulation.due_date.update',
670     signature   => q/
671         Updates the due_date on the given circ
672         @param authtoken
673         @param circid The id of the circ to update
674         @param date The timestamp of the new due date
675     /
676 );
677
678 sub set_circ_due_date {
679     my( $self, $conn, $auth, $circ_id, $date ) = @_;
680
681     my $e = new_editor(xact=>1, authtoken=>$auth);
682     return $e->die_event unless $e->checkauth;
683     my $circ = $e->retrieve_action_circulation($circ_id)
684         or return $e->die_event;
685
686     return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
687     $date = clean_ISO8601($date);
688
689     if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
690         my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
691         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
692
693         # since the new date may be coming in as UTC, convert it
694         # to the same time zone as the original due date so that
695         # ->ymd is more likely to yield the expected results
696         $new_date->set_time_zone($original_date->time_zone());
697         $date = clean_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
698     }
699
700     $circ->due_date($date);
701     $e->update_action_circulation($circ) or return $e->die_event;
702     $e->commit;
703
704     return $circ;
705 }
706
707
708 __PACKAGE__->register_method(
709     method      => "create_in_house_use",
710     api_name        => 'open-ils.circ.in_house_use.create',
711     signature   =>  q/
712         Creates an in-house use action.
713         @param $authtoken The login session key
714         @param params A hash of params including
715             'location' The org unit id where the in-house use occurs
716             'copyid' The copy in question
717             'count' The number of in-house uses to apply to this copy
718         @return An array of id's representing the id's of the newly created
719         in-house use objects or an event on an error
720     /);
721
722 __PACKAGE__->register_method(
723     method      => "create_in_house_use",
724     api_name        => 'open-ils.circ.non_cat_in_house_use.create',
725 );
726
727
728 sub create_in_house_use {
729     my( $self, $client, $auth, $params ) = @_;
730
731     my( $evt, $copy );
732     my $org         = $params->{location};
733     my $copyid      = $params->{copyid};
734     my $count       = $params->{count} || 1;
735     my $nc_type     = $params->{non_cat_type};
736     my $use_time    = $params->{use_time} || 'now';
737
738     my $e = new_editor(xact=>1,authtoken=>$auth);
739     return $e->event unless $e->checkauth;
740     return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
741
742     my $non_cat = 1 if $self->api_name =~ /non_cat/;
743
744     unless( $non_cat ) {
745         if( $copyid ) {
746             $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
747         } else {
748             $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
749                 or return $e->event;
750             $copyid = $copy->id;
751         }
752     }
753
754     if( $use_time ne 'now' ) {
755         $use_time = clean_ISO8601($use_time);
756         $logger->debug("in_house_use setting use time to $use_time");
757     }
758
759     my @ids;
760     for(1..$count) {
761
762         my $ihu;
763         my $method;
764         my $cmeth;
765
766         if($non_cat) {
767             $ihu = Fieldmapper::action::non_cat_in_house_use->new;
768             $ihu->item_type($nc_type);
769             $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
770             $cmeth = "create_action_non_cat_in_house_use";
771
772         } else {
773             $ihu = Fieldmapper::action::in_house_use->new;
774             $ihu->item($copyid);
775             $method = 'open-ils.storage.direct.action.in_house_use.create';
776             $cmeth = "create_action_in_house_use";
777         }
778
779         $ihu->staff($e->requestor->id);
780         $ihu->workstation($e->requestor->wsid);
781         $ihu->org_unit($org);
782         $ihu->use_time($use_time);
783
784         $ihu = $e->$cmeth($ihu) or return $e->event;
785         push( @ids, $ihu->id );
786     }
787
788     $e->commit;
789     return \@ids;
790 }
791
792
793
794
795
796 __PACKAGE__->register_method(
797     method  => "view_circs",
798     api_name    => "open-ils.circ.copy_checkout_history.retrieve",
799     notes       => q/
800         Retrieves the last X circs for a given copy
801         @param authtoken The login session key
802         @param copyid The copy to check
803         @param count How far to go back in the item history
804         @return An array of circ ids
805     /);
806
807 # ----------------------------------------------------------------------
808 # Returns $count most recent circs.  If count exceeds the configured 
809 # max, use the configured max instead
810 # ----------------------------------------------------------------------
811 sub view_circs {
812     my( $self, $client, $authtoken, $copyid, $count ) = @_; 
813
814     my $e = new_editor(authtoken => $authtoken);
815     return $e->event unless $e->checkauth;
816     
817     my $copy = $e->retrieve_asset_copy([
818         $copyid,
819         {   flesh => 1,
820             flesh_fields => {acp => ['call_number']}
821         }
822     ]) or return $e->event;
823
824     return $e->event unless $e->allowed(
825         'VIEW_COPY_CHECKOUT_HISTORY', 
826         ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ? 
827             $copy->circ_lib : $copy->call_number->owning_lib);
828         
829     my $max_history = $U->ou_ancestor_setting_value(
830         $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
831
832     if(defined $max_history) {
833         $count = $max_history unless defined $count and $count < $max_history;
834     } else {
835         $count = 4 unless defined $count;
836     }
837
838     return $e->search_action_all_circulation_slim([
839         {target_copy => $copyid}, 
840         {limit => $count, order_by => { aacs => "xact_start DESC" }} 
841     ]);
842 }
843
844
845 __PACKAGE__->register_method(
846     method  => "circ_count",
847     api_name    => "open-ils.circ.circulation.count",
848     notes       => q/
849         Returns the number of times the item has circulated
850         @param copyid The copy to check
851     /);
852
853 sub circ_count {
854     my( $self, $client, $copyid ) = @_; 
855
856     my $count = new_editor()->json_query({
857         select => {
858             circbyyr => [{
859                 column => 'count',
860                 transform => 'sum',
861                 aggregate => 1
862             }]
863         },
864         from => 'circbyyr',
865         where => {'+circbyyr' => {copy => $copyid}}
866     })->[0]->{count};
867
868     return {
869         total => {
870             when => 'total',
871             count => $count
872         }
873     };
874 }
875
876
877 __PACKAGE__->register_method(
878     method      => 'fetch_notes',
879     authoritative   => 1,
880     api_name        => 'open-ils.circ.copy_note.retrieve.all',
881     signature   => q/
882         Returns an array of copy note objects.  
883         @param args A named hash of parameters including:
884             authtoken   : Required if viewing non-public notes
885             itemid      : The id of the item whose notes we want to retrieve
886             pub         : True if all the caller wants are public notes
887         @return An array of note objects
888     /);
889
890 __PACKAGE__->register_method(
891     method      => 'fetch_notes',
892     api_name        => 'open-ils.circ.call_number_note.retrieve.all',
893     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
894
895 __PACKAGE__->register_method(
896     method      => 'fetch_notes',
897     api_name        => 'open-ils.circ.title_note.retrieve.all',
898     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
899
900
901 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
902 sub fetch_notes {
903     my( $self, $connection, $args ) = @_;
904
905     my $id = $$args{itemid};
906     my $authtoken = $$args{authtoken};
907     my( $r, $evt);
908
909     if( $self->api_name =~ /copy/ ) {
910         if( $$args{pub} ) {
911             return $U->cstorereq(
912                 'open-ils.cstore.direct.asset.copy_note.search.atomic',
913                 { owning_copy => $id, pub => 't' } );
914         } else {
915             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
916             return $evt if $evt;
917             return $U->cstorereq(
918                 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
919         }
920
921     } elsif( $self->api_name =~ /call_number/ ) {
922         if( $$args{pub} ) {
923             return $U->cstorereq(
924                 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
925                 { call_number => $id, pub => 't' } );
926         } else {
927             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
928             return $evt if $evt;
929             return $U->cstorereq(
930                 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
931         }
932
933     } elsif( $self->api_name =~ /title/ ) {
934         if( $$args{pub} ) {
935             return $U->cstorereq(
936                 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
937                 { record => $id, pub => 't' } );
938         } else {
939             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
940             return $evt if $evt;
941             return $U->cstorereq(
942                 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
943         }
944     }
945
946     return undef;
947 }
948
949 __PACKAGE__->register_method(
950     method  => 'has_notes',
951     api_name    => 'open-ils.circ.copy.has_notes');
952 __PACKAGE__->register_method(
953     method  => 'has_notes',
954     api_name    => 'open-ils.circ.call_number.has_notes');
955 __PACKAGE__->register_method(
956     method  => 'has_notes',
957     api_name    => 'open-ils.circ.title.has_notes');
958
959
960 sub has_notes {
961     my( $self, $conn, $authtoken, $id ) = @_;
962     my $editor = new_editor(authtoken => $authtoken);
963     return $editor->event unless $editor->checkauth;
964
965     my $n = $editor->search_asset_copy_note(
966         {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
967
968     $n = $editor->search_asset_call_number_note(
969         {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
970
971     $n = $editor->search_biblio_record_note(
972         {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
973
974     return scalar @$n;
975 }
976
977
978
979 __PACKAGE__->register_method(
980     method      => 'create_copy_note',
981     api_name        => 'open-ils.circ.copy_note.create',
982     signature   => q/
983         Creates a new copy note
984         @param authtoken The login session key
985         @param note The note object to create
986         @return The id of the new note object
987     /);
988
989 sub create_copy_note {
990     my( $self, $connection, $authtoken, $note ) = @_;
991
992     my $e = new_editor(xact=>1, authtoken=>$authtoken);
993     return $e->event unless $e->checkauth;
994     my $copy = $e->retrieve_asset_copy(
995         [
996             $note->owning_copy,
997             {   flesh => 1,
998                 flesh_fields => { 'acp' => ['call_number'] }
999             }
1000         ]
1001     );
1002
1003     return $e->event unless 
1004         $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
1005
1006     $note->create_date('now');
1007     $note->creator($e->requestor->id);
1008     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1009     $note->clear_id;
1010
1011     $e->create_asset_copy_note($note) or return $e->event;
1012     $e->commit;
1013     return $note->id;
1014 }
1015
1016
1017 __PACKAGE__->register_method(
1018     method      => 'delete_copy_note',
1019     api_name        =>  'open-ils.circ.copy_note.delete',
1020     signature   => q/
1021         Deletes an existing copy note
1022         @param authtoken The login session key
1023         @param noteid The id of the note to delete
1024         @return 1 on success - Event otherwise.
1025         /);
1026 sub delete_copy_note {
1027     my( $self, $conn, $authtoken, $noteid ) = @_;
1028
1029     my $e = new_editor(xact=>1, authtoken=>$authtoken);
1030     return $e->die_event unless $e->checkauth;
1031
1032     my $note = $e->retrieve_asset_copy_note([
1033         $noteid,
1034         { flesh => 2,
1035             flesh_fields => {
1036                 'acpn' => [ 'owning_copy' ],
1037                 'acp' => [ 'call_number' ],
1038             }
1039         }
1040     ]) or return $e->die_event;
1041
1042     if( $note->creator ne $e->requestor->id ) {
1043         return $e->die_event unless 
1044             $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1045     }
1046
1047     $e->delete_asset_copy_note($note) or return $e->die_event;
1048     $e->commit;
1049     return 1;
1050 }
1051
1052 __PACKAGE__->register_method(
1053     method      => 'fetch_copy_tags',
1054     authoritative   => 1,
1055     api_name        => 'open-ils.circ.copy_tags.retrieve',
1056     signature   => q/
1057         Returns an array of publicly-visible copy tag objects.  
1058         @param args A named hash of parameters including:
1059             copy_id     : The id of the item whose notes we want to retrieve
1060             tag_type    : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1061             scope       : top of org subtree whose copy tags we want to see
1062             depth       : how far down to look for copy tags (optional)
1063         @return An array of copy tag objects
1064     /);
1065 __PACKAGE__->register_method(
1066     method      => 'fetch_copy_tags',
1067     authoritative   => 1,
1068     api_name        => 'open-ils.circ.copy_tags.retrieve.staff',
1069     signature   => q/
1070         Returns an array of all copy tag objects.  
1071         @param args A named hash of parameters including:
1072             authtoken   : Required to view non-public notes
1073             copy_id     : The id of the item whose notes we want to retrieve (optional)
1074             tag_type    : Type of copy tags to retrieve, e.g., 'bookplate'
1075             scope       : top of org subtree whose copy tags we want to see
1076             depth       : how far down to look for copy tags (optional)
1077         @return An array of copy tag objects
1078     /);
1079
1080 sub fetch_copy_tags {
1081     my ($self, $conn, $args) = @_;
1082
1083     my $org = $args->{scope};
1084     my $depth = $args->{depth};
1085
1086     my $filter = {};
1087     my $e;
1088     if ($self->api_name =~ /\.staff/) {
1089         my $authtoken = $args->{authtoken};
1090         return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;    
1091         $e = new_editor(authtoken => $args->{authtoken});
1092         return $e->event unless $e->checkauth;
1093         return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1094     } else {
1095         $e = new_editor();
1096         $filter->{pub} = 't';
1097     }
1098     $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1099     $filter->{'+acptcm'} = {
1100         copy => $args->{copy_id}
1101     };
1102
1103     # filter by owner of copy tag and depth
1104     $filter->{owner} = {
1105         in => {
1106             select => {aou => [{
1107                 column => 'id',
1108                 transform => 'actor.org_unit_descendants',
1109                 result_field => 'id',
1110                 (defined($depth) ? ( params => [$depth] ) : ()),
1111             }]},
1112             from => 'aou',
1113             where => {id => $org}
1114         }
1115     };
1116
1117     return $e->search_asset_copy_tag([$filter, {
1118         join => { acptcm => {} },
1119         flesh => 1,
1120         flesh_fields => { acpt => ['tag_type'] }
1121     }]);
1122 }
1123
1124
1125 __PACKAGE__->register_method(
1126     method => 'age_hold_rules',
1127     api_name    =>  'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1128 );
1129
1130 sub age_hold_rules {
1131     my( $self, $conn ) = @_;
1132     return new_editor()->retrieve_all_config_rules_age_hold_protect();
1133 }
1134
1135
1136
1137 __PACKAGE__->register_method(
1138     method => 'copy_details_barcode',
1139     authoritative => 1,
1140     api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1141 sub copy_details_barcode {
1142     my( $self, $conn, $auth, $barcode ) = @_;
1143     my $e = new_editor();
1144     my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1145     return $e->event unless $cid;
1146     return copy_details( $self, $conn, $auth, $cid );
1147 }
1148
1149
1150 __PACKAGE__->register_method(
1151     method => 'copy_details',
1152     api_name => 'open-ils.circ.copy_details.retrieve');
1153
1154 sub copy_details {
1155     my( $self, $conn, $auth, $copy_id ) = @_;
1156     my $e = new_editor(authtoken=>$auth);
1157     return $e->event unless $e->checkauth;
1158
1159     my $flesh = { flesh => 1 };
1160
1161     my $copy = $e->retrieve_asset_copy(
1162         [
1163             $copy_id,
1164             {
1165                 flesh => 2,
1166                 flesh_fields => {
1167                     acp => ['call_number','parts','peer_record_maps','floating'],
1168                     acn => ['record','prefix','suffix','label_class']
1169                 }
1170             }
1171         ]) or return $e->event;
1172
1173
1174     # De-flesh the copy for backwards compatibility
1175     my $mvr;
1176     my $vol = $copy->call_number;
1177     if( ref $vol ) {
1178         $copy->call_number($vol->id);
1179         my $record = $vol->record;
1180         if( ref $record ) {
1181             $vol->record($record->id);
1182             $mvr = $U->record_to_mvr($record);
1183         }
1184     }
1185
1186
1187     my $hold = $e->search_action_hold_request(
1188         { 
1189             current_copy        => $copy_id, 
1190             capture_time        => { "!=" => undef },
1191             fulfillment_time    => undef,
1192             cancel_time         => undef,
1193         }
1194     )->[0];
1195
1196     OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1197
1198     my $transit = $e->search_action_transit_copy(
1199         { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1200
1201     # find the most recent circulation for the requested copy,
1202     # be it active, completed, or aged.
1203     my $circ = $e->search_action_all_circulation_slim([
1204         { target_copy => $copy_id },
1205         {
1206             flesh => 1,
1207             flesh_fields => {
1208                 aacs => [
1209                     'workstation',
1210                     'checkin_workstation',
1211                     'duration_rule',
1212                     'max_fine_rule',
1213                     'recurring_fine_rule'
1214                 ],
1215             },
1216             order_by => { aacs => 'xact_start desc' },
1217             limit => 1
1218         }
1219     ])->[0];
1220
1221     return {
1222         copy    => $copy,
1223         hold    => $hold,
1224         transit => $transit,
1225         circ    => $circ,
1226         volume  => $vol,
1227         mvr     => $mvr
1228     };
1229 }
1230
1231
1232
1233
1234 __PACKAGE__->register_method(
1235     method => 'mark_item',
1236     api_name => 'open-ils.circ.mark_item_damaged',
1237     signature   => q/
1238         Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1239         @param authtoken The login session key
1240         @param copy_id The ID of the copy to mark as damaged
1241         @return 1 on success - Event otherwise.
1242         /
1243 );
1244 __PACKAGE__->register_method(
1245     method => 'mark_item',
1246     api_name => 'open-ils.circ.mark_item_missing',
1247     signature   => q/
1248         Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1249         @param authtoken The login session key
1250         @param copy_id The ID of the copy to mark as missing 
1251         @return 1 on success - Event otherwise.
1252         /
1253 );
1254 __PACKAGE__->register_method(
1255     method => 'mark_item',
1256     api_name => 'open-ils.circ.mark_item_bindery',
1257     signature   => q/
1258         Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1259         @param authtoken The login session key
1260         @param copy_id The ID of the copy to mark as bindery
1261         @return 1 on success - Event otherwise.
1262         /
1263 );
1264 __PACKAGE__->register_method(
1265     method => 'mark_item',
1266     api_name => 'open-ils.circ.mark_item_on_order',
1267     signature   => q/
1268         Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1269         @param authtoken The login session key
1270         @param copy_id The ID of the copy to mark as on order 
1271         @return 1 on success - Event otherwise.
1272         /
1273 );
1274 __PACKAGE__->register_method(
1275     method => 'mark_item',
1276     api_name => 'open-ils.circ.mark_item_ill',
1277     signature   => q/
1278         Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1279         @param authtoken The login session key
1280         @param copy_id The ID of the copy to mark as inter-library loan
1281         @return 1 on success - Event otherwise.
1282         /
1283 );
1284 __PACKAGE__->register_method(
1285     method => 'mark_item',
1286     api_name => 'open-ils.circ.mark_item_cataloging',
1287     signature   => q/
1288         Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1289         @param authtoken The login session key
1290         @param copy_id The ID of the copy to mark as cataloging 
1291         @return 1 on success - Event otherwise.
1292         /
1293 );
1294 __PACKAGE__->register_method(
1295     method => 'mark_item',
1296     api_name => 'open-ils.circ.mark_item_reserves',
1297     signature   => q/
1298         Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1299         @param authtoken The login session key
1300         @param copy_id The ID of the copy to mark as reserves
1301         @return 1 on success - Event otherwise.
1302         /
1303 );
1304 __PACKAGE__->register_method(
1305     method => 'mark_item',
1306     api_name => 'open-ils.circ.mark_item_discard',
1307     signature   => q/
1308         Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1309         @param authtoken The login session key
1310         @param copy_id The ID of the copy to mark as discard
1311         @return 1 on success - Event otherwise.
1312         /
1313 );
1314
1315 sub mark_item {
1316     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1317     $args ||= {};
1318
1319     my $e = new_editor(authtoken=>$auth);
1320     return $e->die_event unless $e->checkauth;
1321     my $copy = $e->retrieve_asset_copy([
1322         $copy_id,
1323         {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
1324             or return $e->die_event;
1325
1326     my $owning_lib =
1327         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1328             $copy->circ_lib : $copy->call_number->owning_lib;
1329
1330     my $evt; # For later.
1331     my $perm = 'MARK_ITEM_MISSING';
1332     my $stat = OILS_COPY_STATUS_MISSING;
1333
1334     if( $self->api_name =~ /damaged/ ) {
1335         $perm = 'MARK_ITEM_DAMAGED';
1336         $stat = OILS_COPY_STATUS_DAMAGED;
1337     } elsif ( $self->api_name =~ /bindery/ ) {
1338         $perm = 'MARK_ITEM_BINDERY';
1339         $stat = OILS_COPY_STATUS_BINDERY;
1340     } elsif ( $self->api_name =~ /on_order/ ) {
1341         $perm = 'MARK_ITEM_ON_ORDER';
1342         $stat = OILS_COPY_STATUS_ON_ORDER;
1343     } elsif ( $self->api_name =~ /ill/ ) {
1344         $perm = 'MARK_ITEM_ILL';
1345         $stat = OILS_COPY_STATUS_ILL;
1346     } elsif ( $self->api_name =~ /cataloging/ ) {
1347         $perm = 'MARK_ITEM_CATALOGING';
1348         $stat = OILS_COPY_STATUS_CATALOGING;
1349     } elsif ( $self->api_name =~ /reserves/ ) {
1350         $perm = 'MARK_ITEM_RESERVES';
1351         $stat = OILS_COPY_STATUS_RESERVES;
1352     } elsif ( $self->api_name =~ /discard/ ) {
1353         $perm = 'MARK_ITEM_DISCARD';
1354         $stat = OILS_COPY_STATUS_DISCARD;
1355     }
1356
1357     # caller may proceed if either perm is allowed
1358     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1359
1360     # Copy status checks.
1361     if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
1362         # Checked out items should not be marked missing.
1363         # Otherwise, attempt a checkin before a status change.
1364         if ($stat ne OILS_COPY_STATUS_MISSING && $args->{handle_checkin}) {
1365             $evt = try_checkin($auth, $copy_id);
1366         } else {
1367             $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
1368         }
1369     } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
1370         # Items in transit need to have the transit aborted before being marked.
1371         if ($args->{handle_transit}) {
1372             $evt = try_abort_transit($auth, $copy_id);
1373         } else {
1374             $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
1375         }
1376     } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
1377         # Items with restrict_copy_delete status require the
1378         # COPY_DELETE_WARNING.override permission to be marked for
1379         # discard.
1380         if ($args->{handle_copy_delete_warning}) {
1381             $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
1382         } else {
1383             $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
1384         }
1385     }
1386     return $evt if $evt;
1387
1388     # Retrieving holds for later use.
1389     my $holds = $e->search_action_hold_request([
1390         {
1391             current_copy => $copy->id,
1392             fulfillment_time => undef,
1393             cancel_time => undef,
1394         },
1395         {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
1396     ]);
1397
1398     # Throw event if attempting to  mark discard the only copy to fill a hold.
1399     if ($self->api_name =~ /discard/) {
1400         if (!$args->{handle_last_hold_copy}) {
1401             for my $hold (@$holds) {
1402                 my $eligible = $hold->eligible_copies();
1403                 if (!defined($eligible) || scalar(@{$eligible}) < 2) {
1404                     $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
1405                     last;
1406                 }
1407             }
1408         }
1409     }
1410     return $evt if $evt;
1411
1412     # Things below here require a transaction and there is nothing left to interfere with it.
1413     $e->xact_begin;
1414
1415     # Handle extra mark damaged charges, etc.
1416     if ($self->api_name =~ /damaged/) {
1417         $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1418         return $evt if $evt;
1419     }
1420
1421     # Mark the copy.
1422     $copy->status($stat);
1423     $copy->edit_date('now');
1424     $copy->editor($e->requestor->id);
1425
1426     $e->update_asset_copy($copy) or return $e->die_event;
1427
1428     $e->commit;
1429
1430     if( $self->api_name =~ /damaged/ ) {
1431         # now that we've committed the changes, create related A/T events
1432         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1433         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1434     }
1435
1436     $logger->debug("resetting holds that target the marked copy");
1437     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1438
1439     return 1;
1440 }
1441
1442 sub try_checkin {
1443     my($auth, $copy_id) = @_;
1444
1445     my $checkin = $U->simplereq(
1446         'open-ils.circ',
1447         'open-ils.circ.checkin.override',
1448         $auth, {
1449             copy_id => $copy_id,
1450             noop => 1
1451         }
1452     );
1453     if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1454
1455     my $evt_code = $checkin->[0]->{textcode};
1456     $logger->info("try_checkin() received event: $evt_code");
1457
1458     if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1459         $logger->info('try_checkin() successful checkin');
1460         return undef;
1461     } else {
1462         $logger->warn('try_checkin() un-successful checkin');
1463         return $checkin;
1464     }
1465 }
1466
1467 sub try_abort_transit {
1468     my ($auth, $copy_id) = @_;
1469
1470     my $abort = $U->simplereq(
1471         'open-ils.circ',
1472         'open-ils.circ.transit.abort',
1473         $auth, {copyid => $copy_id}
1474     );
1475     # Above returns 1 or an event.
1476     return $abort if (ref $abort);
1477     return undef;
1478 }
1479
1480 sub handle_mark_damaged {
1481     my($e, $copy, $owning_lib, $args) = @_;
1482
1483     my $apply = $args->{apply_fines} || '';
1484     return undef if $apply eq 'noapply';
1485
1486     my $new_amount = $args->{override_amount};
1487     my $new_btype = $args->{override_btype};
1488     my $new_note = $args->{override_note};
1489
1490     # grab the last circulation
1491     my $circ = $e->search_action_circulation([
1492         {   target_copy => $copy->id}, 
1493         {   limit => 1, 
1494             order_by => {circ => "xact_start DESC"},
1495             flesh => 2,
1496             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1497         }
1498     ])->[0];
1499
1500     return undef unless $circ;
1501
1502     my $charge_price = $U->ou_ancestor_setting_value(
1503         $owning_lib, 'circ.charge_on_damaged', $e);
1504
1505     my $proc_fee = $U->ou_ancestor_setting_value(
1506         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1507
1508     my $void_overdue = $U->ou_ancestor_setting_value(
1509         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1510
1511     return undef unless $charge_price or $proc_fee;
1512
1513     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1514     my $total = $copy_price + $proc_fee;
1515
1516     if($apply) {
1517         
1518         if($new_amount and $new_btype) {
1519
1520             # Allow staff to override the amount to charge for a damaged item
1521             # Consider the case where the item is only partially damaged
1522             # This value is meant to take the place of the item price and
1523             # optional processing fee.
1524
1525             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1526                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1527             return $evt if $evt;
1528
1529         } else {
1530
1531             if($charge_price and $copy_price) {
1532                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1533                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1534                 return $evt if $evt;
1535             }
1536
1537             if($proc_fee) {
1538                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1539                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1540                 return $evt if $evt;
1541             }
1542         }
1543
1544         # the assumption is that you would not void the overdues unless you 
1545         # were also charging for the item and/or applying a processing fee
1546         if($void_overdue) {
1547             my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1548             return $evt if $evt;
1549         }
1550
1551         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1552         return $evt if $evt;
1553
1554         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1555         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1556
1557         my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1558         return $evt2 if $evt2;
1559
1560     } else {
1561         return OpenILS::Event->new('DAMAGE_CHARGE', 
1562             payload => {
1563                 circ => $circ,
1564                 charge => $total
1565             }
1566         );
1567     }
1568 }
1569
1570
1571
1572 # ----------------------------------------------------------------------
1573 __PACKAGE__->register_method(
1574     method => 'mark_item_missing_pieces',
1575     api_name => 'open-ils.circ.mark_item_missing_pieces',
1576     signature   => q/
1577         Changes the status of a copy to "damaged" or to a custom status based on the 
1578         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1579         permission.
1580         @param authtoken The login session key
1581         @param copy_id The ID of the copy to mark as damaged
1582         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1583         /
1584 );
1585
1586 sub mark_item_missing_pieces {
1587     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1588     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1589     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1590
1591     my $e2 = new_editor(authtoken=>$auth, xact =>1);
1592     return $e2->die_event unless $e2->checkauth;
1593     $args ||= {};
1594
1595     my $copy = $e2->retrieve_asset_copy([
1596         $copy_id,
1597         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1598             or return $e2->die_event;
1599
1600     my $owning_lib = 
1601         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1602             $copy->circ_lib : $copy->call_number->owning_lib;
1603
1604     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1605
1606     #### grab the last circulation
1607     my $circ = $e2->search_action_circulation([
1608         {   target_copy => $copy->id}, 
1609         {   limit => 1, 
1610             order_by => {circ => "xact_start DESC"}
1611         }
1612     ])->[0];
1613
1614     if (!$circ) {
1615         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1616         $e2->rollback;
1617         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1618     }
1619
1620     my $holds = $e2->search_action_hold_request(
1621         { 
1622             current_copy => $copy->id,
1623             fulfillment_time => undef,
1624             cancel_time => undef,
1625         }
1626     );
1627
1628     $logger->debug("resetting holds that target the marked copy");
1629     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1630
1631     
1632     if (! $e2->commit) {
1633         return $e2->die_event;
1634     }
1635
1636     my $e = new_editor(authtoken=>$auth, xact =>1);
1637     return $e->die_event unless $e->checkauth;
1638
1639     if (! $circ->checkin_time) { # if circ active, attempt renew
1640         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1641         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1642         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1643             $circ = $res->[0]->{payload}{'circ'};
1644             $circ->target_copy( $copy->id );
1645             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1646         } else {
1647             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1648         }
1649     } else {
1650
1651         my $co_params = {
1652             'copy_id'=>$circ->target_copy,
1653             'patron_id'=>$circ->usr,
1654             'skip_deposit_fee'=>1,
1655             'skip_rental_fee'=>1
1656         };
1657
1658         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1659
1660             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1661                 $e, $copy, $e->requestor, 1 );
1662
1663             if ($hold) { # needed for hold? then due now
1664
1665                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1666                 my $due_date = DateTime->now(time_zone => 'local');
1667                 $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
1668             } else {
1669                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1670             }
1671         }
1672
1673         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1674         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1675         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1676             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1677             $circ = $res->[0]->{payload}{'circ'};
1678         } else {
1679             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1680             $e->rollback;
1681             return $res;
1682         }
1683     }
1684
1685     ### Update the item status
1686
1687     my $custom_stat = $U->ou_ancestor_setting_value(
1688         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1689     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1690
1691     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1692     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1693
1694     $copy->status($stat);
1695     $copy->edit_date('now');
1696     $copy->editor($e->requestor->id);
1697
1698     $e->update_asset_copy($copy) or return $e->die_event;
1699
1700     if ($e->commit) {
1701
1702         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1703         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1704
1705         return OpenILS::Event->new('SUCCESS',
1706             payload => {
1707                 circ => $circ,
1708                 copy => $copy,
1709                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1710                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1711             }
1712         ); 
1713
1714     } else {
1715         return $e->die_event;
1716     }
1717 }
1718
1719
1720
1721
1722
1723 # ----------------------------------------------------------------------
1724 __PACKAGE__->register_method(
1725     method => 'magic_fetch',
1726     api_name => 'open-ils.agent.fetch'
1727 );
1728
1729 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1730
1731 sub magic_fetch {
1732     my( $self, $conn, $auth, $args ) = @_;
1733     my $e = new_editor( authtoken => $auth );
1734     return $e->event unless $e->checkauth;
1735
1736     my $hint = $$args{hint};
1737     my $id  = $$args{id};
1738
1739     # Is the call allowed to fetch this type of object?
1740     return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1741
1742     # Find the class the implements the given hint
1743     my ($class) = grep { 
1744         $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1745
1746     $class =~ s/Fieldmapper:://og;
1747     $class =~ s/::/_/og;
1748     my $method = "retrieve_$class";
1749
1750     my $obj = $e->$method($id) or return $e->event;
1751     return $obj;
1752 }
1753 # ----------------------------------------------------------------------
1754
1755
1756 __PACKAGE__->register_method(
1757     method  => "fleshed_circ_retrieve",
1758     authoritative => 1,
1759     api_name    => "open-ils.circ.fleshed.retrieve",);
1760
1761 sub fleshed_circ_retrieve {
1762     my( $self, $client, $id ) = @_;
1763     my $e = new_editor();
1764     my $circ = $e->retrieve_action_circulation(
1765         [
1766             $id,
1767             { 
1768                 flesh               => 4,
1769                 flesh_fields    => { 
1770                     circ => [ qw/ target_copy / ],
1771                     acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1772                     ascecm => [ qw/ stat_cat stat_cat_entry / ],
1773                     acn => [ qw/ record / ],
1774                 }
1775             }
1776         ]
1777     ) or return $e->event;
1778     
1779     my $copy = $circ->target_copy;
1780     my $vol = $copy->call_number;
1781     my $rec = $circ->target_copy->call_number->record;
1782
1783     $vol->record($rec->id);
1784     $copy->call_number($vol->id);
1785     $circ->target_copy($copy->id);
1786
1787     my $mvr;
1788
1789     if( $rec->id == OILS_PRECAT_RECORD ) {
1790         $rec = undef;
1791         $vol = undef;
1792     } else { 
1793         $mvr = $U->record_to_mvr($rec);
1794         $rec->marc(''); # drop the bulky marc data
1795     }
1796
1797     return {
1798         circ => $circ,
1799         copy => $copy,
1800         volume => $vol,
1801         record => $rec,
1802         mvr => $mvr,
1803     };
1804 }
1805
1806
1807
1808 __PACKAGE__->register_method(
1809     method  => "test_batch_circ_events",
1810     api_name    => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1811 );
1812
1813 #  method for testing the behavior of a given event definition
1814 sub test_batch_circ_events {
1815     my($self, $conn, $auth, $event_def, $barcode) = @_;
1816
1817     my $e = new_editor(authtoken => $auth);
1818     return $e->event unless $e->checkauth;
1819     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1820
1821     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1822         or return $e->event;
1823
1824     my $circ = $e->search_action_circulation(
1825         {target_copy => $copy->id, checkin_time => undef})->[0]
1826         or return $e->event;
1827         
1828     return undef unless $circ;
1829
1830     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1831 }
1832
1833
1834 __PACKAGE__->register_method(
1835     method  => "fire_circ_events", 
1836     api_name    => "open-ils.circ.fire_circ_trigger_events",
1837     signature => q/
1838         General event def runner for circ objects.  If no event def ID
1839         is provided, the hook will be used to find the best event_def
1840         match based on the context org unit
1841     /
1842 );
1843
1844 __PACKAGE__->register_method(
1845     method  => "fire_circ_events", 
1846     api_name    => "open-ils.circ.fire_hold_trigger_events",
1847     signature => q/
1848         General event def runner for hold objects.  If no event def ID
1849         is provided, the hook will be used to find the best event_def
1850         match based on the context org unit
1851     /
1852 );
1853
1854 __PACKAGE__->register_method(
1855     method  => "fire_circ_events", 
1856     api_name    => "open-ils.circ.fire_user_trigger_events",
1857     signature => q/
1858         General event def runner for user objects.  If no event def ID
1859         is provided, the hook will be used to find the best event_def
1860         match based on the context org unit
1861     /
1862 );
1863
1864
1865 sub fire_circ_events {
1866     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1867
1868     my $e = new_editor(authtoken => $auth, xact => 1);
1869     return $e->event unless $e->checkauth;
1870
1871     my $targets;
1872
1873     if($self->api_name =~ /hold/) {
1874         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1875         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1876     } elsif($self->api_name =~ /user/) {
1877         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1878         $targets = $e->batch_retrieve_actor_user($target_ids);
1879     } else {
1880         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1881         $targets = $e->batch_retrieve_action_circulation($target_ids);
1882     }
1883     $e->rollback; # FIXME using transaction because of pgpool + logical replication
1884                   # setups where statements in an explicit transaction are sent to
1885                   # the primary database in the replica set, but not
1886                   # simply making this method authoritative because of weirdness
1887                   # with transaction handling in A/T code that causes rollback
1888                   # failure down the line if handling many targets
1889
1890     return undef unless @$targets;
1891     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1892 }
1893
1894 __PACKAGE__->register_method(
1895     method  => "user_payments_list",
1896     api_name    => "open-ils.circ.user_payments.filtered.batch",
1897     stream => 1,
1898     signature => {
1899         desc => q/Returns a fleshed, date-limited set of all payments a user
1900                 has made.  By default, ordered by payment date.  Optionally
1901                 ordered by other columns in the top-level "mp" object/,
1902         params => [
1903             {desc => 'Authentication token', type => 'string'},
1904             {desc => 'User ID', type => 'number'},
1905             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1906         ],
1907         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1908             and the related fully-realized payment object (e.g money.cash_payment)/}
1909     }
1910 );
1911
1912 sub user_payments_list {
1913     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1914
1915     my $e = new_editor(authtoken => $auth);
1916     return $e->event unless $e->checkauth;
1917
1918     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1919     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1920
1921     $order_by ||= ['payment_ts'];
1922
1923     # all payments by user, between start_date and end_date
1924     my $payments = $e->json_query({
1925         select => {mp => ['id']}, 
1926         from => {
1927             mp => {
1928                 mbt => {
1929                     fkey => 'xact', field => 'id'}
1930             }
1931         }, 
1932         where => {
1933             '+mbt' => {usr => $user_id}, 
1934             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1935         },
1936         order_by => {mp => $order_by}
1937     });
1938
1939     for my $payment_id (@$payments) {
1940         my $payment = $e->retrieve_money_payment([
1941             $payment_id->{id}, 
1942             {   
1943                 flesh => 2,
1944                 flesh_fields => {
1945                     mp => [
1946                         'xact',
1947                         'cash_payment',
1948                         'credit_card_payment',
1949                         'debit_card_payment',
1950                         'credit_payment',
1951                         'check_payment',
1952                         'work_payment',
1953                         'forgive_payment',
1954                         'goods_payment'
1955                     ],
1956                     mbt => [
1957                         'circulation', 
1958                         'grocery',
1959                         'reservation'
1960                     ]
1961                 }
1962             }
1963         ]);
1964         $conn->respond($payment);
1965     }
1966
1967     return undef;
1968 }
1969
1970
1971 __PACKAGE__->register_method(
1972     method  => "retrieve_circ_chain",
1973     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ",
1974     stream => 1,
1975     signature => {
1976         desc => q/Given a circulation, this returns all circulation objects
1977                 that are part of the same chain of renewals./,
1978         params => [
1979             {desc => 'Authentication token', type => 'string'},
1980             {desc => 'Circ ID', type => 'number'},
1981         ],
1982         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1983     }
1984 );
1985
1986 __PACKAGE__->register_method(
1987     method  => "retrieve_circ_chain",
1988     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1989     signature => {
1990         desc => q/Given a circulation, this returns a summary of the circulation objects
1991                 that are part of the same chain of renewals./,
1992         params => [
1993             {desc => 'Authentication token', type => 'string'},
1994             {desc => 'Circ ID', type => 'number'},
1995         ],
1996         return => {desc => q/Circulation Chain Summary/}
1997     }
1998 );
1999
2000 sub retrieve_circ_chain {
2001     my($self, $conn, $auth, $circ_id) = @_;
2002
2003     my $e = new_editor(authtoken => $auth);
2004     return $e->event unless $e->checkauth;
2005     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2006
2007     if($self->api_name =~ /summary/) {
2008         return $U->create_circ_chain_summary($e, $circ_id);
2009
2010     } else {
2011
2012         my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
2013
2014         for my $circ_info (@$chain) {
2015             my $circ = Fieldmapper::action::all_circulation_slim->new;
2016             $circ->$_($circ_info->{$_}) for keys %$circ_info;
2017             $conn->respond($circ);
2018         }
2019     }
2020
2021     return undef;
2022 }
2023
2024 __PACKAGE__->register_method(
2025     method  => "retrieve_prev_circ_chain",
2026     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
2027     stream => 1,
2028     signature => {
2029         desc => q/Given a circulation, this returns all circulation objects
2030                 that are part of the previous chain of renewals./,
2031         params => [
2032             {desc => 'Authentication token', type => 'string'},
2033             {desc => 'Circ ID', type => 'number'},
2034         ],
2035         return => {desc => q/List of circ objects, orderd by oldest circ first/}
2036     }
2037 );
2038
2039 __PACKAGE__->register_method(
2040     method  => "retrieve_prev_circ_chain",
2041     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
2042     signature => {
2043         desc => q/Given a circulation, this returns a summary of the circulation objects
2044                 that are part of the previous chain of renewals./,
2045         params => [
2046             {desc => 'Authentication token', type => 'string'},
2047             {desc => 'Circ ID', type => 'number'},
2048         ],
2049         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
2050     }
2051 );
2052
2053 sub retrieve_prev_circ_chain {
2054     my($self, $conn, $auth, $circ_id) = @_;
2055
2056     my $e = new_editor(authtoken => $auth);
2057     return $e->event unless $e->checkauth;
2058     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2059
2060     my $first_circ = 
2061         $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
2062
2063     my $prev_circ = $e->search_action_all_circulation_slim([
2064         {   target_copy => $first_circ->{target_copy},
2065             xact_start => {'<' => $first_circ->{xact_start}}
2066         }, {   
2067             flesh => 1,
2068             flesh_fields => {
2069                 aacs => [
2070                     'active_circ',
2071                     'aged_circ'
2072                 ]
2073             },
2074             order_by => { aacs => 'xact_start desc' },
2075             limit => 1 
2076         }
2077     ])->[0];
2078
2079     return undef unless $prev_circ;
2080
2081     my $chain_usr = $prev_circ->usr; # note: may be undef
2082
2083     if ($self->api_name =~ /summary/) {
2084         my $sum = $e->json_query({
2085             from => [
2086                 'action.summarize_all_circ_chain', 
2087                 $prev_circ->id
2088             ]
2089         })->[0];
2090
2091         my $summary = Fieldmapper::action::circ_chain_summary->new;
2092         $summary->$_($sum->{$_}) for keys %$sum;
2093
2094         return {summary => $summary, usr => $chain_usr};
2095     }
2096
2097
2098     my $chain = $e->json_query(
2099         {from => ['action.all_circ_chain', $prev_circ->id]});
2100
2101     for my $circ_info (@$chain) {
2102         my $circ = Fieldmapper::action::all_circulation_slim->new;
2103         $circ->$_($circ_info->{$_}) for keys %$circ_info;
2104         $conn->respond($circ);
2105     }
2106
2107     return undef;
2108 }
2109
2110
2111 __PACKAGE__->register_method(
2112     method  => "get_copy_due_date",
2113     api_name    => "open-ils.circ.copy.due_date.retrieve",
2114     signature => {
2115         desc => q/
2116             Given a copy ID, returns the due date for the copy if it's 
2117             currently circulating.  Otherwise, returns null.  Note, this is a public 
2118             method requiring no authentication.  Only the due date is exposed.
2119             /,
2120         params => [
2121             {desc => 'Copy ID', type => 'number'}
2122         ],
2123         return => {desc => q/
2124             Due date (ISO date stamp) if the copy is circulating, null otherwise.
2125         /}
2126     }
2127 );
2128
2129 sub get_copy_due_date {
2130     my($self, $conn, $copy_id) = @_;
2131     my $e = new_editor();
2132
2133     my $circ = $e->json_query({
2134         select => {circ => ['due_date']},
2135         from => 'circ',
2136         where => {
2137             target_copy => $copy_id,
2138             checkin_time => undef,
2139             '-or' => [
2140                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2141                 {stop_fines => undef}
2142             ],
2143         },
2144         limit => 1
2145     })->[0] or return undef;
2146
2147     return $circ->{due_date};
2148 }
2149
2150
2151 __PACKAGE__->register_method(
2152     method    => 'get_offline_data',
2153     api_name  => 'open-ils.circ.offline.data.retrieve',
2154     stream    => 1,
2155     signature => {
2156         desc => q/Returns a stream of data needed by clients to 
2157             support an offline interface. /,
2158         params => [
2159             {desc => 'Authentication token', type => 'string'}
2160         ],
2161         return => {desc => q/
2162         /}
2163     }
2164 );
2165
2166 sub get_offline_data {
2167     my ($self, $client, $auth) = @_;
2168
2169     my $e = new_editor(authtoken => $auth);
2170     return $e->event unless $e->checkauth;
2171     return $e->event unless $e->allowed('STAFF_LOGIN');
2172
2173     my $org_ids = $U->get_org_ancestors($e->requestor->ws_ou);
2174
2175     $client->respond({
2176         idl_class => 'cnct',
2177         data => $e->search_config_non_cataloged_type({owning_lib => $org_ids})
2178     });
2179
2180     my $surveys = $e->search_action_survey([{
2181             owner => $org_ids,
2182             start_date => {'<=' => 'now'},
2183             end_date => {'>=' => 'now'}
2184         }, {
2185             flesh => 2,
2186             flesh_fields => {
2187                 asv => ['questions'],
2188                 asvq => ['answers']
2189             }
2190         }
2191     ]);
2192
2193     $client->respond({idl_class => 'asv', data => $surveys});
2194
2195     $client->respond({idl_class => 'cit', 
2196         data => $e->retrieve_all_config_identification_type});
2197
2198     $client->respond({idl_class => 'cnal', 
2199         data => $e->retrieve_all_config_net_access_level});
2200
2201     $client->respond({idl_class => 'fdoc', 
2202         data => $e->retrieve_all_config_idl_field_doc});
2203
2204     $client->respond({idl_class => 'pgt', 
2205         data => $e->retrieve_all_permission_grp_tree});
2206     
2207     my $stat_cats = $U->simplereq(
2208         'open-ils.circ', 
2209         'open-ils.circ.stat_cat.actor.retrieve.all', 
2210         $auth, $e->requestor->ws_ou);
2211
2212     $client->respond({idl_class => 'actsc', data => $stat_cats});
2213
2214     my $settings = $e->search_config_usr_setting_type({
2215         '-or' => [{
2216             name => [qw/
2217                 circ.holds_behind_desk 
2218                 circ.collections.exempt 
2219                 opac.hold_notify 
2220                 opac.default_phone 
2221                 opac.default_pickup_location 
2222                 opac.default_sms_carrier 
2223                 opac.default_sms_notify/]
2224         }, {
2225             name => {
2226                 in => {
2227                     select => {atevdef => ['opt_in_setting']},
2228                     from => 'atevdef',
2229                     where => {'+atevdef' => {owner => $org_ids}}
2230                 }
2231             }
2232         }]
2233     });
2234
2235     $client->respond({idl_class => 'cust', data => $settings});
2236
2237     return undef;
2238 }
2239
2240
2241
2242 # {"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}}
2243
2244
2245 1;