]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
3c40e2dabc3493df6352867e8e5400ed50d8ae8c
[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, {
1113         join => { acptcm => {} },
1114         flesh => 1,
1115         flesh_fields => { acpt => ['tag_type'] }
1116     }]);
1117 }
1118
1119
1120 __PACKAGE__->register_method(
1121     method => 'age_hold_rules',
1122     api_name    =>  'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1123 );
1124
1125 sub age_hold_rules {
1126     my( $self, $conn ) = @_;
1127     return new_editor()->retrieve_all_config_rules_age_hold_protect();
1128 }
1129
1130
1131
1132 __PACKAGE__->register_method(
1133     method => 'copy_details_barcode',
1134     authoritative => 1,
1135     api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1136 sub copy_details_barcode {
1137     my( $self, $conn, $auth, $barcode ) = @_;
1138     my $e = new_editor();
1139     my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1140     return $e->event unless $cid;
1141     return copy_details( $self, $conn, $auth, $cid );
1142 }
1143
1144
1145 __PACKAGE__->register_method(
1146     method => 'copy_details',
1147     api_name => 'open-ils.circ.copy_details.retrieve');
1148
1149 sub copy_details {
1150     my( $self, $conn, $auth, $copy_id ) = @_;
1151     my $e = new_editor(authtoken=>$auth);
1152     return $e->event unless $e->checkauth;
1153
1154     my $flesh = { flesh => 1 };
1155
1156     my $copy = $e->retrieve_asset_copy(
1157         [
1158             $copy_id,
1159             {
1160                 flesh => 2,
1161                 flesh_fields => {
1162                     acp => ['call_number','parts','peer_record_maps','floating'],
1163                     acn => ['record','prefix','suffix','label_class']
1164                 }
1165             }
1166         ]) or return $e->event;
1167
1168
1169     # De-flesh the copy for backwards compatibility
1170     my $mvr;
1171     my $vol = $copy->call_number;
1172     if( ref $vol ) {
1173         $copy->call_number($vol->id);
1174         my $record = $vol->record;
1175         if( ref $record ) {
1176             $vol->record($record->id);
1177             $mvr = $U->record_to_mvr($record);
1178         }
1179     }
1180
1181
1182     my $hold = $e->search_action_hold_request(
1183         { 
1184             current_copy        => $copy_id, 
1185             capture_time        => { "!=" => undef },
1186             fulfillment_time    => undef,
1187             cancel_time         => undef,
1188         }
1189     )->[0];
1190
1191     OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1192
1193     my $transit = $e->search_action_transit_copy(
1194         { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1195
1196     # find the most recent circulation for the requested copy,
1197     # be it active, completed, or aged.
1198     my $circ = $e->search_action_all_circulation_slim([
1199         { target_copy => $copy_id },
1200         {
1201             flesh => 1,
1202             flesh_fields => {
1203                 aacs => [
1204                     'workstation',
1205                     'checkin_workstation',
1206                     'duration_rule',
1207                     'max_fine_rule',
1208                     'recurring_fine_rule'
1209                 ],
1210             },
1211             order_by => { aacs => 'xact_start desc' },
1212             limit => 1
1213         }
1214     ])->[0];
1215
1216     return {
1217         copy    => $copy,
1218         hold    => $hold,
1219         transit => $transit,
1220         circ    => $circ,
1221         volume  => $vol,
1222         mvr     => $mvr
1223     };
1224 }
1225
1226
1227
1228
1229 __PACKAGE__->register_method(
1230     method => 'mark_item',
1231     api_name => 'open-ils.circ.mark_item_damaged',
1232     signature   => q/
1233         Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1234         @param authtoken The login session key
1235         @param copy_id The ID of the copy to mark as damaged
1236         @return 1 on success - Event otherwise.
1237         /
1238 );
1239 __PACKAGE__->register_method(
1240     method => 'mark_item',
1241     api_name => 'open-ils.circ.mark_item_missing',
1242     signature   => q/
1243         Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1244         @param authtoken The login session key
1245         @param copy_id The ID of the copy to mark as missing 
1246         @return 1 on success - Event otherwise.
1247         /
1248 );
1249 __PACKAGE__->register_method(
1250     method => 'mark_item',
1251     api_name => 'open-ils.circ.mark_item_bindery',
1252     signature   => q/
1253         Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1254         @param authtoken The login session key
1255         @param copy_id The ID of the copy to mark as bindery
1256         @return 1 on success - Event otherwise.
1257         /
1258 );
1259 __PACKAGE__->register_method(
1260     method => 'mark_item',
1261     api_name => 'open-ils.circ.mark_item_on_order',
1262     signature   => q/
1263         Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1264         @param authtoken The login session key
1265         @param copy_id The ID of the copy to mark as on order 
1266         @return 1 on success - Event otherwise.
1267         /
1268 );
1269 __PACKAGE__->register_method(
1270     method => 'mark_item',
1271     api_name => 'open-ils.circ.mark_item_ill',
1272     signature   => q/
1273         Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1274         @param authtoken The login session key
1275         @param copy_id The ID of the copy to mark as inter-library loan
1276         @return 1 on success - Event otherwise.
1277         /
1278 );
1279 __PACKAGE__->register_method(
1280     method => 'mark_item',
1281     api_name => 'open-ils.circ.mark_item_cataloging',
1282     signature   => q/
1283         Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1284         @param authtoken The login session key
1285         @param copy_id The ID of the copy to mark as cataloging 
1286         @return 1 on success - Event otherwise.
1287         /
1288 );
1289 __PACKAGE__->register_method(
1290     method => 'mark_item',
1291     api_name => 'open-ils.circ.mark_item_reserves',
1292     signature   => q/
1293         Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1294         @param authtoken The login session key
1295         @param copy_id The ID of the copy to mark as reserves
1296         @return 1 on success - Event otherwise.
1297         /
1298 );
1299 __PACKAGE__->register_method(
1300     method => 'mark_item',
1301     api_name => 'open-ils.circ.mark_item_discard',
1302     signature   => q/
1303         Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1304         @param authtoken The login session key
1305         @param copy_id The ID of the copy to mark as discard
1306         @return 1 on success - Event otherwise.
1307         /
1308 );
1309
1310 sub mark_item {
1311     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1312     $args ||= {};
1313
1314     my $e = new_editor(authtoken=>$auth);
1315     return $e->die_event unless $e->checkauth;
1316     my $copy = $e->retrieve_asset_copy([
1317         $copy_id,
1318         {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
1319             or return $e->die_event;
1320
1321     my $owning_lib =
1322         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1323             $copy->circ_lib : $copy->call_number->owning_lib;
1324
1325     my $evt; # For later.
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     } elsif ( $self->api_name =~ /bindery/ ) {
1333         $perm = 'MARK_ITEM_BINDERY';
1334         $stat = OILS_COPY_STATUS_BINDERY;
1335     } elsif ( $self->api_name =~ /on_order/ ) {
1336         $perm = 'MARK_ITEM_ON_ORDER';
1337         $stat = OILS_COPY_STATUS_ON_ORDER;
1338     } elsif ( $self->api_name =~ /ill/ ) {
1339         $perm = 'MARK_ITEM_ILL';
1340         $stat = OILS_COPY_STATUS_ILL;
1341     } elsif ( $self->api_name =~ /cataloging/ ) {
1342         $perm = 'MARK_ITEM_CATALOGING';
1343         $stat = OILS_COPY_STATUS_CATALOGING;
1344     } elsif ( $self->api_name =~ /reserves/ ) {
1345         $perm = 'MARK_ITEM_RESERVES';
1346         $stat = OILS_COPY_STATUS_RESERVES;
1347     } elsif ( $self->api_name =~ /discard/ ) {
1348         $perm = 'MARK_ITEM_DISCARD';
1349         $stat = OILS_COPY_STATUS_DISCARD;
1350     }
1351
1352     # caller may proceed if either perm is allowed
1353     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1354
1355     # Copy status checks.
1356     if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
1357         # Items must be checked in before any attempt is made to change its status.
1358         if ($args->{handle_checkin}) {
1359             $evt = try_checkin($auth, $copy_id);
1360         } else {
1361             $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
1362         }
1363     } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
1364         # Items in transit need to have the transit aborted before being marked.
1365         if ($args->{handle_transit}) {
1366             $evt = try_abort_transit($auth, $copy_id);
1367         } else {
1368             $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
1369         }
1370     } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
1371         # Items with restrict_copy_delete status require the
1372         # COPY_DELETE_WARNING.override permission to be marked for
1373         # discard.
1374         if ($args->{handle_copy_delete_warning}) {
1375             $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
1376         } else {
1377             $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
1378         }
1379     }
1380     return $evt if $evt;
1381
1382     # Retrieving holds for later use.
1383     my $holds = $e->search_action_hold_request([
1384         {
1385             current_copy => $copy->id,
1386             fulfillment_time => undef,
1387             cancel_time => undef,
1388         },
1389         {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
1390     ]);
1391
1392     # Throw event if attempting to  mark discard the only copy to fill a hold.
1393     if ($self->api_name =~ /discard/) {
1394         if (!$args->{handle_last_hold_copy}) {
1395             for my $hold (@$holds) {
1396                 my $eligible = $hold->eligible_copies();
1397                 if (!defined($eligible) || scalar(@{$eligible}) < 2) {
1398                     $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
1399                     last;
1400                 }
1401             }
1402         }
1403     }
1404     return $evt if $evt;
1405
1406     # Things below here require a transaction and there is nothing left to interfere with it.
1407     $e->xact_begin;
1408
1409     # Handle extra mark damaged charges, etc.
1410     if ($self->api_name =~ /damaged/) {
1411         $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1412         return $evt if $evt;
1413     }
1414
1415     # Mark the copy.
1416     $copy->status($stat);
1417     $copy->edit_date('now');
1418     $copy->editor($e->requestor->id);
1419
1420     $e->update_asset_copy($copy) or return $e->die_event;
1421
1422     $e->commit;
1423
1424     if( $self->api_name =~ /damaged/ ) {
1425         # now that we've committed the changes, create related A/T events
1426         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1427         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1428     }
1429
1430     $logger->debug("resetting holds that target the marked copy");
1431     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1432
1433     return 1;
1434 }
1435
1436 sub try_checkin {
1437     my($auth, $copy_id) = @_;
1438
1439     my $checkin = $U->simplereq(
1440         'open-ils.circ',
1441         'open-ils.circ.checkin.override',
1442         $auth, {
1443             copy_id => $copy_id,
1444             noop => 1
1445         }
1446     );
1447     if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1448
1449     my $evt_code = $checkin->[0]->{textcode};
1450     $logger->info("try_checkin() received event: $evt_code");
1451
1452     if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1453         $logger->info('try_checkin() successful checkin');
1454         return undef;
1455     } else {
1456         $logger->warn('try_checkin() un-successful checkin');
1457         return $checkin;
1458     }
1459 }
1460
1461 sub try_abort_transit {
1462     my ($auth, $copy_id) = @_;
1463
1464     my $abort = $U->simplereq(
1465         'open-ils.circ',
1466         'open-ils.circ.transit.abort',
1467         $auth, {copyid => $copy_id}
1468     );
1469     # Above returns 1 or an event.
1470     return $abort if (ref $abort);
1471     return undef;
1472 }
1473
1474 sub handle_mark_damaged {
1475     my($e, $copy, $owning_lib, $args) = @_;
1476
1477     my $apply = $args->{apply_fines} || '';
1478     return undef if $apply eq 'noapply';
1479
1480     my $new_amount = $args->{override_amount};
1481     my $new_btype = $args->{override_btype};
1482     my $new_note = $args->{override_note};
1483
1484     # grab the last circulation
1485     my $circ = $e->search_action_circulation([
1486         {   target_copy => $copy->id}, 
1487         {   limit => 1, 
1488             order_by => {circ => "xact_start DESC"},
1489             flesh => 2,
1490             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1491         }
1492     ])->[0];
1493
1494     return undef unless $circ;
1495
1496     my $charge_price = $U->ou_ancestor_setting_value(
1497         $owning_lib, 'circ.charge_on_damaged', $e);
1498
1499     my $proc_fee = $U->ou_ancestor_setting_value(
1500         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1501
1502     my $void_overdue = $U->ou_ancestor_setting_value(
1503         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1504
1505     return undef unless $charge_price or $proc_fee;
1506
1507     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1508     my $total = $copy_price + $proc_fee;
1509
1510     if($apply) {
1511         
1512         if($new_amount and $new_btype) {
1513
1514             # Allow staff to override the amount to charge for a damaged item
1515             # Consider the case where the item is only partially damaged
1516             # This value is meant to take the place of the item price and
1517             # optional processing fee.
1518
1519             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1520                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1521             return $evt if $evt;
1522
1523         } else {
1524
1525             if($charge_price and $copy_price) {
1526                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1527                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1528                 return $evt if $evt;
1529             }
1530
1531             if($proc_fee) {
1532                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1533                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1534                 return $evt if $evt;
1535             }
1536         }
1537
1538         # the assumption is that you would not void the overdues unless you 
1539         # were also charging for the item and/or applying a processing fee
1540         if($void_overdue) {
1541             my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1542             return $evt if $evt;
1543         }
1544
1545         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1546         return $evt if $evt;
1547
1548         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1549         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1550
1551         my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1552         return $evt2 if $evt2;
1553
1554     } else {
1555         return OpenILS::Event->new('DAMAGE_CHARGE', 
1556             payload => {
1557                 circ => $circ,
1558                 charge => $total
1559             }
1560         );
1561     }
1562 }
1563
1564
1565
1566 # ----------------------------------------------------------------------
1567 __PACKAGE__->register_method(
1568     method => 'mark_item_missing_pieces',
1569     api_name => 'open-ils.circ.mark_item_missing_pieces',
1570     signature   => q/
1571         Changes the status of a copy to "damaged" or to a custom status based on the 
1572         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1573         permission.
1574         @param authtoken The login session key
1575         @param copy_id The ID of the copy to mark as damaged
1576         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1577         /
1578 );
1579
1580 sub mark_item_missing_pieces {
1581     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1582     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1583     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1584
1585     my $e2 = new_editor(authtoken=>$auth, xact =>1);
1586     return $e2->die_event unless $e2->checkauth;
1587     $args ||= {};
1588
1589     my $copy = $e2->retrieve_asset_copy([
1590         $copy_id,
1591         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1592             or return $e2->die_event;
1593
1594     my $owning_lib = 
1595         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1596             $copy->circ_lib : $copy->call_number->owning_lib;
1597
1598     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1599
1600     #### grab the last circulation
1601     my $circ = $e2->search_action_circulation([
1602         {   target_copy => $copy->id}, 
1603         {   limit => 1, 
1604             order_by => {circ => "xact_start DESC"}
1605         }
1606     ])->[0];
1607
1608     if (!$circ) {
1609         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1610         $e2->rollback;
1611         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1612     }
1613
1614     my $holds = $e2->search_action_hold_request(
1615         { 
1616             current_copy => $copy->id,
1617             fulfillment_time => undef,
1618             cancel_time => undef,
1619         }
1620     );
1621
1622     $logger->debug("resetting holds that target the marked copy");
1623     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1624
1625     
1626     if (! $e2->commit) {
1627         return $e2->die_event;
1628     }
1629
1630     my $e = new_editor(authtoken=>$auth, xact =>1);
1631     return $e->die_event unless $e->checkauth;
1632
1633     if (! $circ->checkin_time) { # if circ active, attempt renew
1634         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1635         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1636         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1637             $circ = $res->[0]->{payload}{'circ'};
1638             $circ->target_copy( $copy->id );
1639             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1640         } else {
1641             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1642         }
1643     } else {
1644
1645         my $co_params = {
1646             'copy_id'=>$circ->target_copy,
1647             'patron_id'=>$circ->usr,
1648             'skip_deposit_fee'=>1,
1649             'skip_rental_fee'=>1
1650         };
1651
1652         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1653
1654             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1655                 $e, $copy, $e->requestor, 1 );
1656
1657             if ($hold) { # needed for hold? then due now
1658
1659                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1660                 my $due_date = DateTime->now(time_zone => 'local');
1661                 $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
1662             } else {
1663                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1664             }
1665         }
1666
1667         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1668         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1669         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1670             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1671             $circ = $res->[0]->{payload}{'circ'};
1672         } else {
1673             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1674             $e->rollback;
1675             return $res;
1676         }
1677     }
1678
1679     ### Update the item status
1680
1681     my $custom_stat = $U->ou_ancestor_setting_value(
1682         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1683     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1684
1685     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1686     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1687
1688     $copy->status($stat);
1689     $copy->edit_date('now');
1690     $copy->editor($e->requestor->id);
1691
1692     $e->update_asset_copy($copy) or return $e->die_event;
1693
1694     if ($e->commit) {
1695
1696         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1697         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1698
1699         return OpenILS::Event->new('SUCCESS',
1700             payload => {
1701                 circ => $circ,
1702                 copy => $copy,
1703                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1704                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1705             }
1706         ); 
1707
1708     } else {
1709         return $e->die_event;
1710     }
1711 }
1712
1713
1714
1715
1716
1717 # ----------------------------------------------------------------------
1718 __PACKAGE__->register_method(
1719     method => 'magic_fetch',
1720     api_name => 'open-ils.agent.fetch'
1721 );
1722
1723 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1724
1725 sub magic_fetch {
1726     my( $self, $conn, $auth, $args ) = @_;
1727     my $e = new_editor( authtoken => $auth );
1728     return $e->event unless $e->checkauth;
1729
1730     my $hint = $$args{hint};
1731     my $id  = $$args{id};
1732
1733     # Is the call allowed to fetch this type of object?
1734     return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1735
1736     # Find the class the implements the given hint
1737     my ($class) = grep { 
1738         $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1739
1740     $class =~ s/Fieldmapper:://og;
1741     $class =~ s/::/_/og;
1742     my $method = "retrieve_$class";
1743
1744     my $obj = $e->$method($id) or return $e->event;
1745     return $obj;
1746 }
1747 # ----------------------------------------------------------------------
1748
1749
1750 __PACKAGE__->register_method(
1751     method  => "fleshed_circ_retrieve",
1752     authoritative => 1,
1753     api_name    => "open-ils.circ.fleshed.retrieve",);
1754
1755 sub fleshed_circ_retrieve {
1756     my( $self, $client, $id ) = @_;
1757     my $e = new_editor();
1758     my $circ = $e->retrieve_action_circulation(
1759         [
1760             $id,
1761             { 
1762                 flesh               => 4,
1763                 flesh_fields    => { 
1764                     circ => [ qw/ target_copy / ],
1765                     acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1766                     ascecm => [ qw/ stat_cat stat_cat_entry / ],
1767                     acn => [ qw/ record / ],
1768                 }
1769             }
1770         ]
1771     ) or return $e->event;
1772     
1773     my $copy = $circ->target_copy;
1774     my $vol = $copy->call_number;
1775     my $rec = $circ->target_copy->call_number->record;
1776
1777     $vol->record($rec->id);
1778     $copy->call_number($vol->id);
1779     $circ->target_copy($copy->id);
1780
1781     my $mvr;
1782
1783     if( $rec->id == OILS_PRECAT_RECORD ) {
1784         $rec = undef;
1785         $vol = undef;
1786     } else { 
1787         $mvr = $U->record_to_mvr($rec);
1788         $rec->marc(''); # drop the bulky marc data
1789     }
1790
1791     return {
1792         circ => $circ,
1793         copy => $copy,
1794         volume => $vol,
1795         record => $rec,
1796         mvr => $mvr,
1797     };
1798 }
1799
1800
1801
1802 __PACKAGE__->register_method(
1803     method  => "test_batch_circ_events",
1804     api_name    => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1805 );
1806
1807 #  method for testing the behavior of a given event definition
1808 sub test_batch_circ_events {
1809     my($self, $conn, $auth, $event_def, $barcode) = @_;
1810
1811     my $e = new_editor(authtoken => $auth);
1812     return $e->event unless $e->checkauth;
1813     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1814
1815     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1816         or return $e->event;
1817
1818     my $circ = $e->search_action_circulation(
1819         {target_copy => $copy->id, checkin_time => undef})->[0]
1820         or return $e->event;
1821         
1822     return undef unless $circ;
1823
1824     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1825 }
1826
1827
1828 __PACKAGE__->register_method(
1829     method  => "fire_circ_events", 
1830     api_name    => "open-ils.circ.fire_circ_trigger_events",
1831     signature => q/
1832         General event def runner for circ objects.  If no event def ID
1833         is provided, the hook will be used to find the best event_def
1834         match based on the context org unit
1835     /
1836 );
1837
1838 __PACKAGE__->register_method(
1839     method  => "fire_circ_events", 
1840     api_name    => "open-ils.circ.fire_hold_trigger_events",
1841     signature => q/
1842         General event def runner for hold objects.  If no event def ID
1843         is provided, the hook will be used to find the best event_def
1844         match based on the context org unit
1845     /
1846 );
1847
1848 __PACKAGE__->register_method(
1849     method  => "fire_circ_events", 
1850     api_name    => "open-ils.circ.fire_user_trigger_events",
1851     signature => q/
1852         General event def runner for user objects.  If no event def ID
1853         is provided, the hook will be used to find the best event_def
1854         match based on the context org unit
1855     /
1856 );
1857
1858
1859 sub fire_circ_events {
1860     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1861
1862     my $e = new_editor(authtoken => $auth, xact => 1);
1863     return $e->event unless $e->checkauth;
1864
1865     my $targets;
1866
1867     if($self->api_name =~ /hold/) {
1868         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1869         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1870     } elsif($self->api_name =~ /user/) {
1871         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1872         $targets = $e->batch_retrieve_actor_user($target_ids);
1873     } else {
1874         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1875         $targets = $e->batch_retrieve_action_circulation($target_ids);
1876     }
1877     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1878                   # simply making this method authoritative because of weirdness
1879                   # with transaction handling in A/T code that causes rollback
1880                   # failure down the line if handling many targets
1881
1882     return undef unless @$targets;
1883     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1884 }
1885
1886 __PACKAGE__->register_method(
1887     method  => "user_payments_list",
1888     api_name    => "open-ils.circ.user_payments.filtered.batch",
1889     stream => 1,
1890     signature => {
1891         desc => q/Returns a fleshed, date-limited set of all payments a user
1892                 has made.  By default, ordered by payment date.  Optionally
1893                 ordered by other columns in the top-level "mp" object/,
1894         params => [
1895             {desc => 'Authentication token', type => 'string'},
1896             {desc => 'User ID', type => 'number'},
1897             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1898         ],
1899         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1900             and the related fully-realized payment object (e.g money.cash_payment)/}
1901     }
1902 );
1903
1904 sub user_payments_list {
1905     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1906
1907     my $e = new_editor(authtoken => $auth);
1908     return $e->event unless $e->checkauth;
1909
1910     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1911     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1912
1913     $order_by ||= ['payment_ts'];
1914
1915     # all payments by user, between start_date and end_date
1916     my $payments = $e->json_query({
1917         select => {mp => ['id']}, 
1918         from => {
1919             mp => {
1920                 mbt => {
1921                     fkey => 'xact', field => 'id'}
1922             }
1923         }, 
1924         where => {
1925             '+mbt' => {usr => $user_id}, 
1926             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1927         },
1928         order_by => {mp => $order_by}
1929     });
1930
1931     for my $payment_id (@$payments) {
1932         my $payment = $e->retrieve_money_payment([
1933             $payment_id->{id}, 
1934             {   
1935                 flesh => 2,
1936                 flesh_fields => {
1937                     mp => [
1938                         'xact',
1939                         'cash_payment',
1940                         'credit_card_payment',
1941                         'credit_payment',
1942                         'check_payment',
1943                         'work_payment',
1944                         'forgive_payment',
1945                         'goods_payment'
1946                     ],
1947                     mbt => [
1948                         'circulation', 
1949                         'grocery',
1950                         'reservation'
1951                     ]
1952                 }
1953             }
1954         ]);
1955         $conn->respond($payment);
1956     }
1957
1958     return undef;
1959 }
1960
1961
1962 __PACKAGE__->register_method(
1963     method  => "retrieve_circ_chain",
1964     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ",
1965     stream => 1,
1966     signature => {
1967         desc => q/Given a circulation, this returns all circulation objects
1968                 that are part of the same chain of renewals./,
1969         params => [
1970             {desc => 'Authentication token', type => 'string'},
1971             {desc => 'Circ ID', type => 'number'},
1972         ],
1973         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1974     }
1975 );
1976
1977 __PACKAGE__->register_method(
1978     method  => "retrieve_circ_chain",
1979     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1980     signature => {
1981         desc => q/Given a circulation, this returns a summary of the circulation objects
1982                 that are part of the same chain of renewals./,
1983         params => [
1984             {desc => 'Authentication token', type => 'string'},
1985             {desc => 'Circ ID', type => 'number'},
1986         ],
1987         return => {desc => q/Circulation Chain Summary/}
1988     }
1989 );
1990
1991 sub retrieve_circ_chain {
1992     my($self, $conn, $auth, $circ_id) = @_;
1993
1994     my $e = new_editor(authtoken => $auth);
1995     return $e->event unless $e->checkauth;
1996     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1997
1998     if($self->api_name =~ /summary/) {
1999         return $U->create_circ_chain_summary($e, $circ_id);
2000
2001     } else {
2002
2003         my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
2004
2005         for my $circ_info (@$chain) {
2006             my $circ = Fieldmapper::action::all_circulation_slim->new;
2007             $circ->$_($circ_info->{$_}) for keys %$circ_info;
2008             $conn->respond($circ);
2009         }
2010     }
2011
2012     return undef;
2013 }
2014
2015 __PACKAGE__->register_method(
2016     method  => "retrieve_prev_circ_chain",
2017     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
2018     stream => 1,
2019     signature => {
2020         desc => q/Given a circulation, this returns all circulation objects
2021                 that are part of the previous chain of renewals./,
2022         params => [
2023             {desc => 'Authentication token', type => 'string'},
2024             {desc => 'Circ ID', type => 'number'},
2025         ],
2026         return => {desc => q/List of circ objects, orderd by oldest circ first/}
2027     }
2028 );
2029
2030 __PACKAGE__->register_method(
2031     method  => "retrieve_prev_circ_chain",
2032     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
2033     signature => {
2034         desc => q/Given a circulation, this returns a summary of the circulation objects
2035                 that are part of the previous chain of renewals./,
2036         params => [
2037             {desc => 'Authentication token', type => 'string'},
2038             {desc => 'Circ ID', type => 'number'},
2039         ],
2040         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
2041     }
2042 );
2043
2044 sub retrieve_prev_circ_chain {
2045     my($self, $conn, $auth, $circ_id) = @_;
2046
2047     my $e = new_editor(authtoken => $auth);
2048     return $e->event unless $e->checkauth;
2049     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2050
2051     my $first_circ = 
2052         $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
2053
2054     my $prev_circ = $e->search_action_all_circulation_slim([
2055         {   target_copy => $first_circ->{target_copy},
2056             xact_start => {'<' => $first_circ->{xact_start}}
2057         }, {   
2058             flesh => 1,
2059             flesh_fields => {
2060                 aacs => [
2061                     'active_circ',
2062                     'aged_circ'
2063                 ]
2064             },
2065             order_by => { aacs => 'xact_start desc' },
2066             limit => 1 
2067         }
2068     ])->[0];
2069
2070     return undef unless $prev_circ;
2071
2072     my $chain_usr = $prev_circ->usr; # note: may be undef
2073
2074     if ($self->api_name =~ /summary/) {
2075         my $sum = $e->json_query({
2076             from => [
2077                 'action.summarize_all_circ_chain', 
2078                 $prev_circ->id
2079             ]
2080         })->[0];
2081
2082         my $summary = Fieldmapper::action::circ_chain_summary->new;
2083         $summary->$_($sum->{$_}) for keys %$sum;
2084
2085         return {summary => $summary, usr => $chain_usr};
2086     }
2087
2088
2089     my $chain = $e->json_query(
2090         {from => ['action.all_circ_chain', $prev_circ->id]});
2091
2092     for my $circ_info (@$chain) {
2093         my $circ = Fieldmapper::action::all_circulation_slim->new;
2094         $circ->$_($circ_info->{$_}) for keys %$circ_info;
2095         $conn->respond($circ);
2096     }
2097
2098     return undef;
2099 }
2100
2101
2102 __PACKAGE__->register_method(
2103     method  => "get_copy_due_date",
2104     api_name    => "open-ils.circ.copy.due_date.retrieve",
2105     signature => {
2106         desc => q/
2107             Given a copy ID, returns the due date for the copy if it's 
2108             currently circulating.  Otherwise, returns null.  Note, this is a public 
2109             method requiring no authentication.  Only the due date is exposed.
2110             /,
2111         params => [
2112             {desc => 'Copy ID', type => 'number'}
2113         ],
2114         return => {desc => q/
2115             Due date (ISO date stamp) if the copy is circulating, null otherwise.
2116         /}
2117     }
2118 );
2119
2120 sub get_copy_due_date {
2121     my($self, $conn, $copy_id) = @_;
2122     my $e = new_editor();
2123
2124     my $circ = $e->json_query({
2125         select => {circ => ['due_date']},
2126         from => 'circ',
2127         where => {
2128             target_copy => $copy_id,
2129             checkin_time => undef,
2130             '-or' => [
2131                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2132                 {stop_fines => undef}
2133             ],
2134         },
2135         limit => 1
2136     })->[0] or return undef;
2137
2138     return $circ->{due_date};
2139 }
2140
2141
2142
2143
2144
2145 # {"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}}
2146
2147
2148 1;