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