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