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