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