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