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