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