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