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