LP#980296: Add void of lost processing fee on claims returned.
[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_overdues($e, $circ, $backdate);
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         return $result if ($result);
487     }
488
489     # Check if the copy circ lib wants lost processing fees voided on
490     # claims returned.
491     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
492         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
493             $e,
494             $circ,
495             4
496         );
497         return $result if ($result);
498     }
499
500     $e->commit;
501     return 1;
502 }
503
504
505 __PACKAGE__->register_method(
506     method  => "post_checkin_backdate_circ",
507     api_name    => "open-ils.circ.post_checkin_backdate",
508     signature => {
509         desc => q/Back-date an already checked in circulation/,
510         params => [
511             {desc => 'Authentication token', type => 'string'},
512             {desc => 'Circ ID', type => 'number'},
513             {desc => 'ISO8601 backdate', type => 'string'},
514         ],
515         return => {desc => q/1 on success, failure event on error/}
516     }
517 );
518
519 __PACKAGE__->register_method(
520     method  => "post_checkin_backdate_circ",
521     api_name    => "open-ils.circ.post_checkin_backdate.batch",
522     stream => 1,
523     signature => {
524         desc => q/@see open-ils.circ.post_checkin_backdate.  Batch mode/,
525         params => [
526             {desc => 'Authentication token', type => 'string'},
527             {desc => 'List of Circ ID', type => 'array'},
528             {desc => 'ISO8601 backdate', type => 'string'},
529         ],
530         return => {desc => q/Set of: 1 on success, failure event on error/}
531     }
532 );
533
534
535 sub post_checkin_backdate_circ {
536     my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
537     my $e = new_editor(authtoken=>$auth);
538     return $e->die_event unless $e->checkauth;
539     if($self->api_name =~ /batch/) {
540         foreach my $c (@$circ_id) {
541             $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
542         }
543     } else {
544         $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
545     }
546
547     $e->disconnect;
548     return undef;
549 }
550
551
552 sub post_checkin_backdate_circ_impl {
553     my($e, $circ_id, $backdate) = @_;
554
555     $e->xact_begin;
556
557     my $circ = $e->retrieve_action_circulation($circ_id)
558         or return $e->die_event;
559
560     # anyone with checkin perms can backdate (more restrictive?)
561     return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
562
563     # don't allow back-dating an open circulation
564     return OpenILS::Event->new('BAD_PARAMS') unless 
565         $backdate and $circ->checkin_time;
566
567     # update the checkin and stop_fines times to reflect the new backdate
568     $circ->stop_fines_time(cleanse_ISO8601($backdate));
569     $circ->checkin_time(cleanse_ISO8601($backdate));
570     $e->update_action_circulation($circ) or return $e->die_event;
571
572     # now void the overdues "erased" by the back-dating
573     my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ, $backdate);
574     return $evt if $evt;
575
576     # If the circ was closed before and the balance owned !=0, re-open the transaction
577     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
578     return $evt if $evt;
579
580     $e->xact_commit;
581     return 1;
582 }
583
584
585
586 __PACKAGE__->register_method (
587     method      => 'set_circ_due_date',
588     api_name        => 'open-ils.circ.circulation.due_date.update',
589     signature   => q/
590         Updates the due_date on the given circ
591         @param authtoken
592         @param circid The id of the circ to update
593         @param date The timestamp of the new due date
594     /
595 );
596
597 sub set_circ_due_date {
598     my( $self, $conn, $auth, $circ_id, $date ) = @_;
599
600     my $e = new_editor(xact=>1, authtoken=>$auth);
601     return $e->die_event unless $e->checkauth;
602     my $circ = $e->retrieve_action_circulation($circ_id)
603         or return $e->die_event;
604
605     return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
606     $date = cleanse_ISO8601($date);
607
608     if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
609         my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
610         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
611         $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
612     }
613
614     $circ->due_date($date);
615     $e->update_action_circulation($circ) or return $e->die_event;
616     $e->commit;
617
618     return $circ;
619 }
620
621
622 __PACKAGE__->register_method(
623     method      => "create_in_house_use",
624     api_name        => 'open-ils.circ.in_house_use.create',
625     signature   =>  q/
626         Creates an in-house use action.
627         @param $authtoken The login session key
628         @param params A hash of params including
629             'location' The org unit id where the in-house use occurs
630             'copyid' The copy in question
631             'count' The number of in-house uses to apply to this copy
632         @return An array of id's representing the id's of the newly created
633         in-house use objects or an event on an error
634     /);
635
636 __PACKAGE__->register_method(
637     method      => "create_in_house_use",
638     api_name        => 'open-ils.circ.non_cat_in_house_use.create',
639 );
640
641
642 sub create_in_house_use {
643     my( $self, $client, $auth, $params ) = @_;
644
645     my( $evt, $copy );
646     my $org         = $params->{location};
647     my $copyid      = $params->{copyid};
648     my $count       = $params->{count} || 1;
649     my $nc_type     = $params->{non_cat_type};
650     my $use_time    = $params->{use_time} || 'now';
651
652     my $e = new_editor(xact=>1,authtoken=>$auth);
653     return $e->event unless $e->checkauth;
654     return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
655
656     my $non_cat = 1 if $self->api_name =~ /non_cat/;
657
658     unless( $non_cat ) {
659         if( $copyid ) {
660             $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
661         } else {
662             $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
663                 or return $e->event;
664             $copyid = $copy->id;
665         }
666     }
667
668     if( $use_time ne 'now' ) {
669         $use_time = cleanse_ISO8601($use_time);
670         $logger->debug("in_house_use setting use time to $use_time");
671     }
672
673     my @ids;
674     for(1..$count) {
675
676         my $ihu;
677         my $method;
678         my $cmeth;
679
680         if($non_cat) {
681             $ihu = Fieldmapper::action::non_cat_in_house_use->new;
682             $ihu->item_type($nc_type);
683             $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
684             $cmeth = "create_action_non_cat_in_house_use";
685
686         } else {
687             $ihu = Fieldmapper::action::in_house_use->new;
688             $ihu->item($copyid);
689             $method = 'open-ils.storage.direct.action.in_house_use.create';
690             $cmeth = "create_action_in_house_use";
691         }
692
693         $ihu->staff($e->requestor->id);
694         $ihu->org_unit($org);
695         $ihu->use_time($use_time);
696
697         $ihu = $e->$cmeth($ihu) or return $e->event;
698         push( @ids, $ihu->id );
699     }
700
701     $e->commit;
702     return \@ids;
703 }
704
705
706
707
708
709 __PACKAGE__->register_method(
710     method  => "view_circs",
711     api_name    => "open-ils.circ.copy_checkout_history.retrieve",
712     notes       => q/
713         Retrieves the last X circs for a given copy
714         @param authtoken The login session key
715         @param copyid The copy to check
716         @param count How far to go back in the item history
717         @return An array of circ ids
718     /);
719
720 # ----------------------------------------------------------------------
721 # Returns $count most recent circs.  If count exceeds the configured 
722 # max, use the configured max instead
723 # ----------------------------------------------------------------------
724 sub view_circs {
725     my( $self, $client, $authtoken, $copyid, $count ) = @_; 
726
727     my $e = new_editor(authtoken => $authtoken);
728     return $e->event unless $e->checkauth;
729     
730     my $copy = $e->retrieve_asset_copy([
731         $copyid,
732         {   flesh => 1,
733             flesh_fields => {acp => ['call_number']}
734         }
735     ]) or return $e->event;
736
737     return $e->event unless $e->allowed(
738         'VIEW_COPY_CHECKOUT_HISTORY', 
739         ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ? 
740             $copy->circ_lib : $copy->call_number->owning_lib);
741         
742     my $max_history = $U->ou_ancestor_setting_value(
743         $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
744
745     if(defined $max_history) {
746         $count = $max_history unless defined $count and $count < $max_history;
747     } else {
748         $count = 4 unless defined $count;
749     }
750
751     return $e->search_action_circulation([
752         {target_copy => $copyid}, 
753         {limit => $count, order_by => { circ => "xact_start DESC" }} 
754     ]);
755 }
756
757
758 __PACKAGE__->register_method(
759     method  => "circ_count",
760     api_name    => "open-ils.circ.circulation.count",
761     notes       => q/
762         Returns the number of times the item has circulated
763         @param copyid The copy to check
764     /);
765
766 sub circ_count {
767     my( $self, $client, $copyid ) = @_; 
768
769     my $count = new_editor()->json_query({
770         select => {
771             circbyyr => [{
772                 column => 'count',
773                 transform => 'sum',
774                 aggregate => 1
775             }]
776         },
777         from => 'circbyyr',
778         where => {'+circbyyr' => {copy => $copyid}}
779     })->[0]->{count};
780
781     return {
782         total => {
783             when => 'total',
784             count => $count
785         }
786     };
787 }
788
789
790 __PACKAGE__->register_method(
791     method      => 'fetch_notes',
792     authoritative   => 1,
793     api_name        => 'open-ils.circ.copy_note.retrieve.all',
794     signature   => q/
795         Returns an array of copy note objects.  
796         @param args A named hash of parameters including:
797             authtoken   : Required if viewing non-public notes
798             itemid      : The id of the item whose notes we want to retrieve
799             pub         : True if all the caller wants are public notes
800         @return An array of note objects
801     /);
802
803 __PACKAGE__->register_method(
804     method      => 'fetch_notes',
805     api_name        => 'open-ils.circ.call_number_note.retrieve.all',
806     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
807
808 __PACKAGE__->register_method(
809     method      => 'fetch_notes',
810     api_name        => 'open-ils.circ.title_note.retrieve.all',
811     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
812
813
814 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
815 sub fetch_notes {
816     my( $self, $connection, $args ) = @_;
817
818     my $id = $$args{itemid};
819     my $authtoken = $$args{authtoken};
820     my( $r, $evt);
821
822     if( $self->api_name =~ /copy/ ) {
823         if( $$args{pub} ) {
824             return $U->cstorereq(
825                 'open-ils.cstore.direct.asset.copy_note.search.atomic',
826                 { owning_copy => $id, pub => 't' } );
827         } else {
828             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
829             return $evt if $evt;
830             return $U->cstorereq(
831                 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
832         }
833
834     } elsif( $self->api_name =~ /call_number/ ) {
835         if( $$args{pub} ) {
836             return $U->cstorereq(
837                 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
838                 { call_number => $id, pub => 't' } );
839         } else {
840             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
841             return $evt if $evt;
842             return $U->cstorereq(
843                 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
844         }
845
846     } elsif( $self->api_name =~ /title/ ) {
847         if( $$args{pub} ) {
848             return $U->cstorereq(
849                 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
850                 { record => $id, pub => 't' } );
851         } else {
852             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
853             return $evt if $evt;
854             return $U->cstorereq(
855                 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
856         }
857     }
858
859     return undef;
860 }
861
862 __PACKAGE__->register_method(
863     method  => 'has_notes',
864     api_name    => 'open-ils.circ.copy.has_notes');
865 __PACKAGE__->register_method(
866     method  => 'has_notes',
867     api_name    => 'open-ils.circ.call_number.has_notes');
868 __PACKAGE__->register_method(
869     method  => 'has_notes',
870     api_name    => 'open-ils.circ.title.has_notes');
871
872
873 sub has_notes {
874     my( $self, $conn, $authtoken, $id ) = @_;
875     my $editor = new_editor(authtoken => $authtoken);
876     return $editor->event unless $editor->checkauth;
877
878     my $n = $editor->search_asset_copy_note(
879         {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
880
881     $n = $editor->search_asset_call_number_note(
882         {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
883
884     $n = $editor->search_biblio_record_note(
885         {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
886
887     return scalar @$n;
888 }
889
890
891
892 __PACKAGE__->register_method(
893     method      => 'create_copy_note',
894     api_name        => 'open-ils.circ.copy_note.create',
895     signature   => q/
896         Creates a new copy note
897         @param authtoken The login session key
898         @param note The note object to create
899         @return The id of the new note object
900     /);
901
902 sub create_copy_note {
903     my( $self, $connection, $authtoken, $note ) = @_;
904
905     my $e = new_editor(xact=>1, authtoken=>$authtoken);
906     return $e->event unless $e->checkauth;
907     my $copy = $e->retrieve_asset_copy(
908         [
909             $note->owning_copy,
910             {   flesh => 1,
911                 flesh_fields => { 'acp' => ['call_number'] }
912             }
913         ]
914     );
915
916     return $e->event unless 
917         $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
918
919     $note->create_date('now');
920     $note->creator($e->requestor->id);
921     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
922     $note->clear_id;
923
924     $e->create_asset_copy_note($note) or return $e->event;
925     $e->commit;
926     return $note->id;
927 }
928
929
930 __PACKAGE__->register_method(
931     method      => 'delete_copy_note',
932     api_name        =>  'open-ils.circ.copy_note.delete',
933     signature   => q/
934         Deletes an existing copy note
935         @param authtoken The login session key
936         @param noteid The id of the note to delete
937         @return 1 on success - Event otherwise.
938         /);
939 sub delete_copy_note {
940     my( $self, $conn, $authtoken, $noteid ) = @_;
941
942     my $e = new_editor(xact=>1, authtoken=>$authtoken);
943     return $e->die_event unless $e->checkauth;
944
945     my $note = $e->retrieve_asset_copy_note([
946         $noteid,
947         { flesh => 2,
948             flesh_fields => {
949                 'acpn' => [ 'owning_copy' ],
950                 'acp' => [ 'call_number' ],
951             }
952         }
953     ]) or return $e->die_event;
954
955     if( $note->creator ne $e->requestor->id ) {
956         return $e->die_event unless 
957             $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
958     }
959
960     $e->delete_asset_copy_note($note) or return $e->die_event;
961     $e->commit;
962     return 1;
963 }
964
965
966 __PACKAGE__->register_method(
967     method => 'age_hold_rules',
968     api_name    =>  'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
969 );
970
971 sub age_hold_rules {
972     my( $self, $conn ) = @_;
973     return new_editor()->retrieve_all_config_rules_age_hold_protect();
974 }
975
976
977
978 __PACKAGE__->register_method(
979     method => 'copy_details_barcode',
980     authoritative => 1,
981     api_name => 'open-ils.circ.copy_details.retrieve.barcode');
982 sub copy_details_barcode {
983     my( $self, $conn, $auth, $barcode ) = @_;
984     my $e = new_editor();
985     my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
986     return $e->event unless $cid;
987     return copy_details( $self, $conn, $auth, $cid );
988 }
989
990
991 __PACKAGE__->register_method(
992     method => 'copy_details',
993     api_name => 'open-ils.circ.copy_details.retrieve');
994
995 sub copy_details {
996     my( $self, $conn, $auth, $copy_id ) = @_;
997     my $e = new_editor(authtoken=>$auth);
998     return $e->event unless $e->checkauth;
999
1000     my $flesh = { flesh => 1 };
1001
1002     my $copy = $e->retrieve_asset_copy(
1003         [
1004             $copy_id,
1005             {
1006                 flesh => 2,
1007                 flesh_fields => {
1008                     acp => ['call_number','parts','peer_record_maps','floating'],
1009                     acn => ['record','prefix','suffix','label_class']
1010                 }
1011             }
1012         ]) or return $e->event;
1013
1014
1015     # De-flesh the copy for backwards compatibility
1016     my $mvr;
1017     my $vol = $copy->call_number;
1018     if( ref $vol ) {
1019         $copy->call_number($vol->id);
1020         my $record = $vol->record;
1021         if( ref $record ) {
1022             $vol->record($record->id);
1023             $mvr = $U->record_to_mvr($record);
1024         }
1025     }
1026
1027
1028     my $hold = $e->search_action_hold_request(
1029         { 
1030             current_copy        => $copy_id, 
1031             capture_time        => { "!=" => undef },
1032             fulfillment_time    => undef,
1033             cancel_time         => undef,
1034         }
1035     )->[0];
1036
1037     OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1038
1039     my $transit = $e->search_action_transit_copy(
1040         { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1041
1042     # find the latest circ, open or closed
1043     my $circ = $e->search_action_circulation(
1044         [
1045             { target_copy => $copy_id },
1046             { 
1047                 flesh => 1,
1048                 flesh_fields => {
1049                     circ => [
1050                         'workstation',
1051                         'checkin_workstation', 
1052                         'duration_rule', 
1053                         'max_fine_rule', 
1054                         'recurring_fine_rule'
1055                     ]
1056                 },
1057                 order_by => { circ => 'xact_start desc' }, 
1058                 limit => 1 
1059             }
1060         ]
1061     )->[0];
1062
1063
1064     return {
1065         copy        => $copy,
1066         hold        => $hold,
1067         transit => $transit,
1068         circ        => $circ,
1069         volume  => $vol,
1070         mvr     => $mvr,
1071     };
1072 }
1073
1074
1075
1076
1077 __PACKAGE__->register_method(
1078     method => 'mark_item',
1079     api_name => 'open-ils.circ.mark_item_damaged',
1080     signature   => q/
1081         Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1082         @param authtoken The login session key
1083         @param copy_id The ID of the copy to mark as damaged
1084         @return 1 on success - Event otherwise.
1085         /
1086 );
1087 __PACKAGE__->register_method(
1088     method => 'mark_item',
1089     api_name => 'open-ils.circ.mark_item_missing',
1090     signature   => q/
1091         Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1092         @param authtoken The login session key
1093         @param copy_id The ID of the copy to mark as missing 
1094         @return 1 on success - Event otherwise.
1095         /
1096 );
1097 __PACKAGE__->register_method(
1098     method => 'mark_item',
1099     api_name => 'open-ils.circ.mark_item_bindery',
1100     signature   => q/
1101         Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1102         @param authtoken The login session key
1103         @param copy_id The ID of the copy to mark as bindery
1104         @return 1 on success - Event otherwise.
1105         /
1106 );
1107 __PACKAGE__->register_method(
1108     method => 'mark_item',
1109     api_name => 'open-ils.circ.mark_item_on_order',
1110     signature   => q/
1111         Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1112         @param authtoken The login session key
1113         @param copy_id The ID of the copy to mark as on order 
1114         @return 1 on success - Event otherwise.
1115         /
1116 );
1117 __PACKAGE__->register_method(
1118     method => 'mark_item',
1119     api_name => 'open-ils.circ.mark_item_ill',
1120     signature   => q/
1121         Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1122         @param authtoken The login session key
1123         @param copy_id The ID of the copy to mark as inter-library loan
1124         @return 1 on success - Event otherwise.
1125         /
1126 );
1127 __PACKAGE__->register_method(
1128     method => 'mark_item',
1129     api_name => 'open-ils.circ.mark_item_cataloging',
1130     signature   => q/
1131         Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1132         @param authtoken The login session key
1133         @param copy_id The ID of the copy to mark as cataloging 
1134         @return 1 on success - Event otherwise.
1135         /
1136 );
1137 __PACKAGE__->register_method(
1138     method => 'mark_item',
1139     api_name => 'open-ils.circ.mark_item_reserves',
1140     signature   => q/
1141         Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1142         @param authtoken The login session key
1143         @param copy_id The ID of the copy to mark as reserves
1144         @return 1 on success - Event otherwise.
1145         /
1146 );
1147 __PACKAGE__->register_method(
1148     method => 'mark_item',
1149     api_name => 'open-ils.circ.mark_item_discard',
1150     signature   => q/
1151         Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1152         @param authtoken The login session key
1153         @param copy_id The ID of the copy to mark as discard
1154         @return 1 on success - Event otherwise.
1155         /
1156 );
1157
1158 sub mark_item {
1159     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1160     my $e = new_editor(authtoken=>$auth, xact =>1);
1161     return $e->die_event unless $e->checkauth;
1162     $args ||= {};
1163
1164     my $copy = $e->retrieve_asset_copy([
1165         $copy_id,
1166         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1167             or return $e->die_event;
1168
1169     my $owning_lib = 
1170         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1171             $copy->circ_lib : $copy->call_number->owning_lib;
1172
1173     my $perm = 'MARK_ITEM_MISSING';
1174     my $stat = OILS_COPY_STATUS_MISSING;
1175
1176     if( $self->api_name =~ /damaged/ ) {
1177         $perm = 'MARK_ITEM_DAMAGED';
1178         $stat = OILS_COPY_STATUS_DAMAGED;
1179         my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1180         return $evt if $evt;
1181
1182     } elsif ( $self->api_name =~ /bindery/ ) {
1183         $perm = 'MARK_ITEM_BINDERY';
1184         $stat = OILS_COPY_STATUS_BINDERY;
1185     } elsif ( $self->api_name =~ /on_order/ ) {
1186         $perm = 'MARK_ITEM_ON_ORDER';
1187         $stat = OILS_COPY_STATUS_ON_ORDER;
1188     } elsif ( $self->api_name =~ /ill/ ) {
1189         $perm = 'MARK_ITEM_ILL';
1190         $stat = OILS_COPY_STATUS_ILL;
1191     } elsif ( $self->api_name =~ /cataloging/ ) {
1192         $perm = 'MARK_ITEM_CATALOGING';
1193         $stat = OILS_COPY_STATUS_CATALOGING;
1194     } elsif ( $self->api_name =~ /reserves/ ) {
1195         $perm = 'MARK_ITEM_RESERVES';
1196         $stat = OILS_COPY_STATUS_RESERVES;
1197     } elsif ( $self->api_name =~ /discard/ ) {
1198         $perm = 'MARK_ITEM_DISCARD';
1199         $stat = OILS_COPY_STATUS_DISCARD;
1200     }
1201
1202     # caller may proceed if either perm is allowed
1203     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1204
1205     $copy->status($stat);
1206     $copy->edit_date('now');
1207     $copy->editor($e->requestor->id);
1208
1209     $e->update_asset_copy($copy) or return $e->die_event;
1210
1211     my $holds = $e->search_action_hold_request(
1212         { 
1213             current_copy => $copy->id,
1214             fulfillment_time => undef,
1215             cancel_time => undef,
1216         }
1217     );
1218
1219     $e->commit;
1220
1221     if( $self->api_name =~ /damaged/ ) {
1222         # now that we've committed the changes, create related A/T events
1223         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1224         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1225     }
1226
1227     $logger->debug("resetting holds that target the marked copy");
1228     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1229
1230     return 1;
1231 }
1232
1233 sub handle_mark_damaged {
1234     my($e, $copy, $owning_lib, $args) = @_;
1235
1236     my $apply = $args->{apply_fines} || '';
1237     return undef if $apply eq 'noapply';
1238
1239     my $new_amount = $args->{override_amount};
1240     my $new_btype = $args->{override_btype};
1241     my $new_note = $args->{override_note};
1242
1243     # grab the last circulation
1244     my $circ = $e->search_action_circulation([
1245         {   target_copy => $copy->id}, 
1246         {   limit => 1, 
1247             order_by => {circ => "xact_start DESC"},
1248             flesh => 2,
1249             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1250         }
1251     ])->[0];
1252
1253     return undef unless $circ;
1254
1255     my $charge_price = $U->ou_ancestor_setting_value(
1256         $owning_lib, 'circ.charge_on_damaged', $e);
1257
1258     my $proc_fee = $U->ou_ancestor_setting_value(
1259         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1260
1261     my $void_overdue = $U->ou_ancestor_setting_value(
1262         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1263
1264     return undef unless $charge_price or $proc_fee;
1265
1266     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1267     my $total = $copy_price + $proc_fee;
1268
1269     if($apply) {
1270         
1271         if($new_amount and $new_btype) {
1272
1273             # Allow staff to override the amount to charge for a damaged item
1274             # Consider the case where the item is only partially damaged
1275             # This value is meant to take the place of the item price and
1276             # optional processing fee.
1277
1278             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1279                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1280             return $evt if $evt;
1281
1282         } else {
1283
1284             if($charge_price and $copy_price) {
1285                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1286                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1287                 return $evt if $evt;
1288             }
1289
1290             if($proc_fee) {
1291                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1292                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1293                 return $evt if $evt;
1294             }
1295         }
1296
1297         # the assumption is that you would not void the overdues unless you 
1298         # were also charging for the item and/or applying a processing fee
1299         if($void_overdue) {
1300             my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1301             return $evt if $evt;
1302         }
1303
1304         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1305         return $evt if $evt;
1306
1307         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1308         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1309
1310         my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1311         return $evt2 if $evt2;
1312
1313         return undef;
1314
1315     } else {
1316         return OpenILS::Event->new('DAMAGE_CHARGE', 
1317             payload => {
1318                 circ => $circ,
1319                 charge => $total
1320             }
1321         );
1322     }
1323 }
1324
1325
1326
1327 # ----------------------------------------------------------------------
1328 __PACKAGE__->register_method(
1329     method => 'mark_item_missing_pieces',
1330     api_name => 'open-ils.circ.mark_item_missing_pieces',
1331     signature   => q/
1332         Changes the status of a copy to "damaged" or to a custom status based on the 
1333         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1334         permission.
1335         @param authtoken The login session key
1336         @param copy_id The ID of the copy to mark as damaged
1337         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1338         /
1339 );
1340
1341 sub mark_item_missing_pieces {
1342     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1343     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1344     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1345
1346     my $e2 = new_editor(authtoken=>$auth, xact =>1);
1347     return $e2->die_event unless $e2->checkauth;
1348     $args ||= {};
1349
1350     my $copy = $e2->retrieve_asset_copy([
1351         $copy_id,
1352         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1353             or return $e2->die_event;
1354
1355     my $owning_lib = 
1356         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1357             $copy->circ_lib : $copy->call_number->owning_lib;
1358
1359     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1360
1361     #### grab the last circulation
1362     my $circ = $e2->search_action_circulation([
1363         {   target_copy => $copy->id}, 
1364         {   limit => 1, 
1365             order_by => {circ => "xact_start DESC"}
1366         }
1367     ])->[0];
1368
1369     if (!$circ) {
1370         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1371         $e2->rollback;
1372         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1373     }
1374
1375     my $holds = $e2->search_action_hold_request(
1376         { 
1377             current_copy => $copy->id,
1378             fulfillment_time => undef,
1379             cancel_time => undef,
1380         }
1381     );
1382
1383     $logger->debug("resetting holds that target the marked copy");
1384     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1385
1386     
1387     if (! $e2->commit) {
1388         return $e2->die_event;
1389     }
1390
1391     my $e = new_editor(authtoken=>$auth, xact =>1);
1392     return $e->die_event unless $e->checkauth;
1393
1394     if (! $circ->checkin_time) { # if circ active, attempt renew
1395         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1396         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1397         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1398             $circ = $res->[0]->{payload}{'circ'};
1399             $circ->target_copy( $copy->id );
1400             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1401         } else {
1402             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1403         }
1404     } else {
1405
1406         my $co_params = {
1407             'copy_id'=>$circ->target_copy,
1408             'patron_id'=>$circ->usr,
1409             'skip_deposit_fee'=>1,
1410             'skip_rental_fee'=>1
1411         };
1412
1413         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1414
1415             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1416                 $e, $copy, $e->requestor, 1 );
1417
1418             if ($hold) { # needed for hold? then due now
1419
1420                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1421                 my $due_date = DateTime->now(time_zone => 'local');
1422                 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1423             } else {
1424                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1425             }
1426         }
1427
1428         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1429         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1430         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1431             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1432             $circ = $res->[0]->{payload}{'circ'};
1433         } else {
1434             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1435             $e->rollback;
1436             return $res;
1437         }
1438     }
1439
1440     ### Update the item status
1441
1442     my $custom_stat = $U->ou_ancestor_setting_value(
1443         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1444     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1445
1446     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1447     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1448
1449     $copy->status($stat);
1450     $copy->edit_date('now');
1451     $copy->editor($e->requestor->id);
1452
1453     $e->update_asset_copy($copy) or return $e->die_event;
1454
1455     if ($e->commit) {
1456
1457         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1458         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1459
1460         return OpenILS::Event->new('SUCCESS',
1461             payload => {
1462                 circ => $circ,
1463                 copy => $copy,
1464                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1465                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1466             }
1467         ); 
1468
1469     } else {
1470         return $e->die_event;
1471     }
1472 }
1473
1474
1475
1476
1477
1478 # ----------------------------------------------------------------------
1479 __PACKAGE__->register_method(
1480     method => 'magic_fetch',
1481     api_name => 'open-ils.agent.fetch'
1482 );
1483
1484 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1485
1486 sub magic_fetch {
1487     my( $self, $conn, $auth, $args ) = @_;
1488     my $e = new_editor( authtoken => $auth );
1489     return $e->event unless $e->checkauth;
1490
1491     my $hint = $$args{hint};
1492     my $id  = $$args{id};
1493
1494     # Is the call allowed to fetch this type of object?
1495     return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1496
1497     # Find the class the implements the given hint
1498     my ($class) = grep { 
1499         $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1500
1501     $class =~ s/Fieldmapper:://og;
1502     $class =~ s/::/_/og;
1503     my $method = "retrieve_$class";
1504
1505     my $obj = $e->$method($id) or return $e->event;
1506     return $obj;
1507 }
1508 # ----------------------------------------------------------------------
1509
1510
1511 __PACKAGE__->register_method(
1512     method  => "fleshed_circ_retrieve",
1513     authoritative => 1,
1514     api_name    => "open-ils.circ.fleshed.retrieve",);
1515
1516 sub fleshed_circ_retrieve {
1517     my( $self, $client, $id ) = @_;
1518     my $e = new_editor();
1519     my $circ = $e->retrieve_action_circulation(
1520         [
1521             $id,
1522             { 
1523                 flesh               => 4,
1524                 flesh_fields    => { 
1525                     circ => [ qw/ target_copy / ],
1526                     acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1527                     ascecm => [ qw/ stat_cat stat_cat_entry / ],
1528                     acn => [ qw/ record / ],
1529                 }
1530             }
1531         ]
1532     ) or return $e->event;
1533     
1534     my $copy = $circ->target_copy;
1535     my $vol = $copy->call_number;
1536     my $rec = $circ->target_copy->call_number->record;
1537
1538     $vol->record($rec->id);
1539     $copy->call_number($vol->id);
1540     $circ->target_copy($copy->id);
1541
1542     my $mvr;
1543
1544     if( $rec->id == OILS_PRECAT_RECORD ) {
1545         $rec = undef;
1546         $vol = undef;
1547     } else { 
1548         $mvr = $U->record_to_mvr($rec);
1549         $rec->marc(''); # drop the bulky marc data
1550     }
1551
1552     return {
1553         circ => $circ,
1554         copy => $copy,
1555         volume => $vol,
1556         record => $rec,
1557         mvr => $mvr,
1558     };
1559 }
1560
1561
1562
1563 __PACKAGE__->register_method(
1564     method  => "test_batch_circ_events",
1565     api_name    => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1566 );
1567
1568 #  method for testing the behavior of a given event definition
1569 sub test_batch_circ_events {
1570     my($self, $conn, $auth, $event_def, $barcode) = @_;
1571
1572     my $e = new_editor(authtoken => $auth);
1573     return $e->event unless $e->checkauth;
1574     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1575
1576     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1577         or return $e->event;
1578
1579     my $circ = $e->search_action_circulation(
1580         {target_copy => $copy->id, checkin_time => undef})->[0]
1581         or return $e->event;
1582         
1583     return undef unless $circ;
1584
1585     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1586 }
1587
1588
1589 __PACKAGE__->register_method(
1590     method  => "fire_circ_events", 
1591     api_name    => "open-ils.circ.fire_circ_trigger_events",
1592     signature => q/
1593         General event def runner for circ objects.  If no event def ID
1594         is provided, the hook will be used to find the best event_def
1595         match based on the context org unit
1596     /
1597 );
1598
1599 __PACKAGE__->register_method(
1600     method  => "fire_circ_events", 
1601     api_name    => "open-ils.circ.fire_hold_trigger_events",
1602     signature => q/
1603         General event def runner for hold objects.  If no event def ID
1604         is provided, the hook will be used to find the best event_def
1605         match based on the context org unit
1606     /
1607 );
1608
1609 __PACKAGE__->register_method(
1610     method  => "fire_circ_events", 
1611     api_name    => "open-ils.circ.fire_user_trigger_events",
1612     signature => q/
1613         General event def runner for user objects.  If no event def ID
1614         is provided, the hook will be used to find the best event_def
1615         match based on the context org unit
1616     /
1617 );
1618
1619
1620 sub fire_circ_events {
1621     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1622
1623     my $e = new_editor(authtoken => $auth, xact => 1);
1624     return $e->event unless $e->checkauth;
1625
1626     my $targets;
1627
1628     if($self->api_name =~ /hold/) {
1629         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1630         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1631     } elsif($self->api_name =~ /user/) {
1632         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1633         $targets = $e->batch_retrieve_actor_user($target_ids);
1634     } else {
1635         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1636         $targets = $e->batch_retrieve_action_circulation($target_ids);
1637     }
1638     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1639                   # simply making this method authoritative because of weirdness
1640                   # with transaction handling in A/T code that causes rollback
1641                   # failure down the line if handling many targets
1642
1643     return undef unless @$targets;
1644     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1645 }
1646
1647 __PACKAGE__->register_method(
1648     method  => "user_payments_list",
1649     api_name    => "open-ils.circ.user_payments.filtered.batch",
1650     stream => 1,
1651     signature => {
1652         desc => q/Returns a fleshed, date-limited set of all payments a user
1653                 has made.  By default, ordered by payment date.  Optionally
1654                 ordered by other columns in the top-level "mp" object/,
1655         params => [
1656             {desc => 'Authentication token', type => 'string'},
1657             {desc => 'User ID', type => 'number'},
1658             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1659         ],
1660         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1661             and the related fully-realized payment object (e.g money.cash_payment)/}
1662     }
1663 );
1664
1665 sub user_payments_list {
1666     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1667
1668     my $e = new_editor(authtoken => $auth);
1669     return $e->event unless $e->checkauth;
1670
1671     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1672     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1673
1674     $order_by ||= ['payment_ts'];
1675
1676     # all payments by user, between start_date and end_date
1677     my $payments = $e->json_query({
1678         select => {mp => ['id']}, 
1679         from => {
1680             mp => {
1681                 mbt => {
1682                     fkey => 'xact', field => 'id'}
1683             }
1684         }, 
1685         where => {
1686             '+mbt' => {usr => $user_id}, 
1687             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1688         },
1689         order_by => {mp => $order_by}
1690     });
1691
1692     for my $payment_id (@$payments) {
1693         my $payment = $e->retrieve_money_payment([
1694             $payment_id->{id}, 
1695             {   
1696                 flesh => 2,
1697                 flesh_fields => {
1698                     mp => [
1699                         'xact',
1700                         'cash_payment',
1701                         'credit_card_payment',
1702                         'credit_payment',
1703                         'check_payment',
1704                         'work_payment',
1705                         'forgive_payment',
1706                         'goods_payment'
1707                     ],
1708                     mbt => [
1709                         'circulation', 
1710                         'grocery',
1711                         'reservation'
1712                     ]
1713                 }
1714             }
1715         ]);
1716         $conn->respond($payment);
1717     }
1718
1719     return undef;
1720 }
1721
1722
1723 __PACKAGE__->register_method(
1724     method  => "retrieve_circ_chain",
1725     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ",
1726     stream => 1,
1727     signature => {
1728         desc => q/Given a circulation, this returns all circulation objects
1729                 that are part of the same chain of renewals./,
1730         params => [
1731             {desc => 'Authentication token', type => 'string'},
1732             {desc => 'Circ ID', type => 'number'},
1733         ],
1734         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1735     }
1736 );
1737
1738 __PACKAGE__->register_method(
1739     method  => "retrieve_circ_chain",
1740     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1741     signature => {
1742         desc => q/Given a circulation, this returns a summary of the circulation objects
1743                 that are part of the same chain of renewals./,
1744         params => [
1745             {desc => 'Authentication token', type => 'string'},
1746             {desc => 'Circ ID', type => 'number'},
1747         ],
1748         return => {desc => q/Circulation Chain Summary/}
1749     }
1750 );
1751
1752 sub retrieve_circ_chain {
1753     my($self, $conn, $auth, $circ_id) = @_;
1754
1755     my $e = new_editor(authtoken => $auth);
1756     return $e->event unless $e->checkauth;
1757     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1758
1759     if($self->api_name =~ /summary/) {
1760         return $U->create_circ_chain_summary($e, $circ_id);
1761
1762     } else {
1763
1764         my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1765
1766         for my $circ_info (@$chain) {
1767             my $circ = Fieldmapper::action::circulation->new;
1768             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1769             $conn->respond($circ);
1770         }
1771     }
1772
1773     return undef;
1774 }
1775
1776 __PACKAGE__->register_method(
1777     method  => "retrieve_prev_circ_chain",
1778     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1779     stream => 1,
1780     signature => {
1781         desc => q/Given a circulation, this returns all circulation objects
1782                 that are part of the previous chain of renewals./,
1783         params => [
1784             {desc => 'Authentication token', type => 'string'},
1785             {desc => 'Circ ID', type => 'number'},
1786         ],
1787         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1788     }
1789 );
1790
1791 __PACKAGE__->register_method(
1792     method  => "retrieve_prev_circ_chain",
1793     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1794     signature => {
1795         desc => q/Given a circulation, this returns a summary of the circulation objects
1796                 that are part of the previous chain of renewals./,
1797         params => [
1798             {desc => 'Authentication token', type => 'string'},
1799             {desc => 'Circ ID', type => 'number'},
1800         ],
1801         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1802     }
1803 );
1804
1805 sub retrieve_prev_circ_chain {
1806     my($self, $conn, $auth, $circ_id) = @_;
1807
1808     my $e = new_editor(authtoken => $auth);
1809     return $e->event unless $e->checkauth;
1810     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1811
1812     if($self->api_name =~ /summary/) {
1813         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1814         my $target_copy = $$first_circ{'target_copy'};
1815         my $usr = $$first_circ{'usr'};
1816         my $last_circ_from_prev_chain = $e->json_query({
1817             'select' => { 'circ' => ['id','usr'] },
1818             'from' => 'circ', 
1819             'where' => {
1820                 target_copy => $target_copy,
1821                 xact_start => { '<' => $$first_circ{'xact_start'} }
1822             },
1823             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1824             'limit' => 1
1825         })->[0];
1826         return undef unless $last_circ_from_prev_chain;
1827         return undef unless $$last_circ_from_prev_chain{'id'};
1828         my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1829         return undef unless $sum;
1830         my $obj = Fieldmapper::action::circ_chain_summary->new;
1831         $obj->$_($sum->{$_}) for keys %$sum;
1832         return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1833
1834     } else {
1835
1836         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1837         my $target_copy = $$first_circ{'target_copy'};
1838         my $last_circ_from_prev_chain = $e->json_query({
1839             'select' => { 'circ' => ['id'] },
1840             'from' => 'circ', 
1841             'where' => {
1842                 target_copy => $target_copy,
1843                 xact_start => { '<' => $$first_circ{'xact_start'} }
1844             },
1845             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1846             'limit' => 1
1847         })->[0];
1848         return undef unless $last_circ_from_prev_chain;
1849         return undef unless $$last_circ_from_prev_chain{'id'};
1850         my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1851
1852         for my $circ_info (@$chain) {
1853             my $circ = Fieldmapper::action::circulation->new;
1854             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1855             $conn->respond($circ);
1856         }
1857     }
1858
1859     return undef;
1860 }
1861
1862
1863 __PACKAGE__->register_method(
1864     method  => "get_copy_due_date",
1865     api_name    => "open-ils.circ.copy.due_date.retrieve",
1866     signature => {
1867         desc => q/
1868             Given a copy ID, returns the due date for the copy if it's 
1869             currently circulating.  Otherwise, returns null.  Note, this is a public 
1870             method requiring no authentication.  Only the due date is exposed.
1871             /,
1872         params => [
1873             {desc => 'Copy ID', type => 'number'}
1874         ],
1875         return => {desc => q/
1876             Due date (ISO date stamp) if the copy is circulating, null otherwise.
1877         /}
1878     }
1879 );
1880
1881 sub get_copy_due_date {
1882     my($self, $conn, $copy_id) = @_;
1883     my $e = new_editor();
1884
1885     my $circ = $e->json_query({
1886         select => {circ => ['due_date']},
1887         from => 'circ',
1888         where => {
1889             target_copy => $copy_id,
1890             checkin_time => undef,
1891             '-or' => [
1892                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1893                 {stop_fines => undef}
1894             ],
1895         },
1896         limit => 1
1897     })->[0] or return undef;
1898
1899     return $circ->{due_date};
1900 }
1901
1902
1903
1904
1905
1906 # {"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}}
1907
1908
1909 1;