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