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