Merge branch 'opac-tt-poc' of ssh://senator@yeti.esilibrary.com/home/evergreen/evergr...
[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'],
977                                         acn => ['record']
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     return $e->die_event unless $e->allowed('UPDATE_COPY', $owning_lib);
1142
1143
1144         my $perm = 'MARK_ITEM_MISSING';
1145         my $stat = OILS_COPY_STATUS_MISSING;
1146
1147         if( $self->api_name =~ /damaged/ ) {
1148                 $perm = 'MARK_ITEM_DAMAGED';
1149                 $stat = OILS_COPY_STATUS_DAMAGED;
1150         my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1151         return $evt if $evt;
1152
1153         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1154         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1155
1156         } elsif ( $self->api_name =~ /bindery/ ) {
1157                 $perm = 'MARK_ITEM_BINDERY';
1158                 $stat = OILS_COPY_STATUS_BINDERY;
1159         } elsif ( $self->api_name =~ /on_order/ ) {
1160                 $perm = 'MARK_ITEM_ON_ORDER';
1161                 $stat = OILS_COPY_STATUS_ON_ORDER;
1162         } elsif ( $self->api_name =~ /ill/ ) {
1163                 $perm = 'MARK_ITEM_ILL';
1164                 $stat = OILS_COPY_STATUS_ILL;
1165         } elsif ( $self->api_name =~ /cataloging/ ) {
1166                 $perm = 'MARK_ITEM_CATALOGING';
1167                 $stat = OILS_COPY_STATUS_CATALOGING;
1168         } elsif ( $self->api_name =~ /reserves/ ) {
1169                 $perm = 'MARK_ITEM_RESERVES';
1170                 $stat = OILS_COPY_STATUS_RESERVES;
1171         } elsif ( $self->api_name =~ /discard/ ) {
1172                 $perm = 'MARK_ITEM_DISCARD';
1173                 $stat = OILS_COPY_STATUS_DISCARD;
1174         }
1175
1176
1177         $copy->status($stat);
1178         $copy->edit_date('now');
1179         $copy->editor($e->requestor->id);
1180
1181         $e->update_asset_copy($copy) or return $e->die_event;
1182
1183         my $holds = $e->search_action_hold_request(
1184                 { 
1185                         current_copy => $copy->id,
1186                         fulfillment_time => undef,
1187                         cancel_time => undef,
1188                 }
1189         );
1190
1191         $e->commit;
1192
1193         $logger->debug("resetting holds that target the marked copy");
1194         OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1195
1196         return 1;
1197 }
1198
1199 sub handle_mark_damaged {
1200     my($e, $copy, $owning_lib, $args) = @_;
1201
1202     my $apply = $args->{apply_fines} || '';
1203     return undef if $apply eq 'noapply';
1204
1205     my $new_amount = $args->{override_amount};
1206     my $new_btype = $args->{override_btype};
1207     my $new_note = $args->{override_note};
1208
1209     # grab the last circulation
1210     my $circ = $e->search_action_circulation([
1211         {   target_copy => $copy->id}, 
1212         {   limit => 1, 
1213             order_by => {circ => "xact_start DESC"},
1214             flesh => 2,
1215             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1216         }
1217     ])->[0];
1218
1219     return undef unless $circ;
1220
1221     my $charge_price = $U->ou_ancestor_setting_value(
1222         $owning_lib, 'circ.charge_on_damaged', $e);
1223
1224     my $proc_fee = $U->ou_ancestor_setting_value(
1225         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1226
1227     my $void_overdue = $U->ou_ancestor_setting_value(
1228         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1229
1230     return undef unless $charge_price or $proc_fee;
1231
1232     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1233     my $total = $copy_price + $proc_fee;
1234
1235     if($apply) {
1236         
1237         if($new_amount and $new_btype) {
1238
1239             # Allow staff to override the amount to charge for a damaged item
1240             # Consider the case where the item is only partially damaged
1241             # This value is meant to take the place of the item price and
1242             # optional processing fee.
1243
1244             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1245                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1246             return $evt if $evt;
1247
1248         } else {
1249
1250             if($charge_price and $copy_price) {
1251                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1252                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1253                 return $evt if $evt;
1254             }
1255
1256             if($proc_fee) {
1257                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1258                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1259                 return $evt if $evt;
1260             }
1261         }
1262
1263         # the assumption is that you would not void the overdues unless you 
1264         # were also charging for the item and/or applying a processing fee
1265         if($void_overdue) {
1266             my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1267             return $evt if $evt;
1268         }
1269
1270         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1271         return $evt if $evt;
1272
1273         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1274         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1275
1276         return undef;
1277
1278     } else {
1279         return OpenILS::Event->new('DAMAGE_CHARGE', 
1280             payload => {
1281                 circ => $circ,
1282                 charge => $total
1283             }
1284         );
1285     }
1286 }
1287
1288
1289
1290 # ----------------------------------------------------------------------
1291 __PACKAGE__->register_method(
1292     method => 'mark_item_missing_pieces',
1293     api_name => 'open-ils.circ.mark_item_missing_pieces',
1294     signature   => q/
1295         Changes the status of a copy to "damaged" or to a custom status based on the 
1296         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1297         permission.
1298         @param authtoken The login session key
1299         @param copy_id The ID of the copy to mark as damaged
1300         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1301         /
1302 );
1303
1304 sub mark_item_missing_pieces {
1305         my( $self, $conn, $auth, $copy_id, $args ) = @_;
1306     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1307         my $e = new_editor(authtoken=>$auth, xact =>1);
1308         return $e->die_event unless $e->checkauth;
1309     $args ||= {};
1310
1311     my $copy = $e->retrieve_asset_copy([
1312         $copy_id,
1313         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1314             or return $e->die_event;
1315
1316     my $owning_lib = 
1317         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1318             $copy->circ_lib : $copy->call_number->owning_lib;
1319
1320     return $e->die_event unless $e->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1321
1322     #### grab the last circulation
1323     my $circ = $e->search_action_circulation([
1324         {   target_copy => $copy->id}, 
1325         {   limit => 1, 
1326             order_by => {circ => "xact_start DESC"}
1327         }
1328     ])->[0];
1329
1330     if ($circ) {
1331         if (! $circ->checkin_time) { # if circ active, attempt renew
1332             my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1333             if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1334             if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1335                 $circ = $res->[0]->{payload}{'circ'};
1336                 $circ->target_copy( $copy->id );
1337                 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1338             } else {
1339                 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1340             }
1341         } else {
1342
1343             my $co_params = {
1344                 'copy_id'=>$circ->target_copy,
1345                 'patron_id'=>$circ->usr,
1346                 'skip_deposit_fee'=>1,
1347                 'skip_rental_fee'=>1
1348             };
1349
1350             if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1351
1352                 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1353                     $e, $copy, $e->requestor, 1 );
1354
1355                 if ($hold) { # needed for hold? then due now
1356
1357                     $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1358                     my $due_date = DateTime->now(time_zone => 'local');
1359                     $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1360                 } else {
1361                     $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1362                 }
1363             }
1364
1365             my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params);
1366             if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1367             if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1368                 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1369                 $circ = $res->[0]->{payload}{'circ'};
1370             } else {
1371                 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1372                 $e->rollback;
1373                 return $res;
1374             }
1375         }
1376     } else {
1377         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1378         $e->rollback;
1379         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1380     }
1381
1382     ### Update the item status
1383
1384     my $custom_stat = $U->ou_ancestor_setting_value(
1385         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1386     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1387
1388     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1389     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1390
1391         $copy->status($stat);
1392         $copy->edit_date('now');
1393         $copy->editor($e->requestor->id);
1394
1395         $e->update_asset_copy($copy) or return $e->die_event;
1396
1397         my $holds = $e->search_action_hold_request(
1398                 { 
1399                         current_copy => $copy->id,
1400                         fulfillment_time => undef,
1401                         cancel_time => undef,
1402                 }
1403         );
1404
1405     $logger->debug("resetting holds that target the marked copy");
1406     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1407
1408         if ($e->commit) {
1409
1410         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1411         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1412
1413         return OpenILS::Event->new('SUCCESS',
1414             payload => {
1415                 circ => $circ,
1416                 copy => $copy,
1417                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1418                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1419             }
1420         ); 
1421
1422     } else {
1423         return $e->die_event;
1424     }
1425 }
1426
1427
1428
1429
1430
1431 # ----------------------------------------------------------------------
1432 __PACKAGE__->register_method(
1433         method => 'magic_fetch',
1434         api_name => 'open-ils.agent.fetch'
1435 );
1436
1437 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1438
1439 sub magic_fetch {
1440         my( $self, $conn, $auth, $args ) = @_;
1441         my $e = new_editor( authtoken => $auth );
1442         return $e->event unless $e->checkauth;
1443
1444         my $hint = $$args{hint};
1445         my $id  = $$args{id};
1446
1447         # Is the call allowed to fetch this type of object?
1448         return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1449
1450         # Find the class the implements the given hint
1451         my ($class) = grep { 
1452                 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1453
1454         $class =~ s/Fieldmapper:://og;
1455         $class =~ s/::/_/og;
1456         my $method = "retrieve_$class";
1457
1458         my $obj = $e->$method($id) or return $e->event;
1459         return $obj;
1460 }
1461 # ----------------------------------------------------------------------
1462
1463
1464 __PACKAGE__->register_method(
1465         method  => "fleshed_circ_retrieve",
1466     authoritative => 1,
1467         api_name        => "open-ils.circ.fleshed.retrieve",);
1468
1469 sub fleshed_circ_retrieve {
1470         my( $self, $client, $id ) = @_;
1471         my $e = new_editor();
1472         my $circ = $e->retrieve_action_circulation(
1473                 [
1474                         $id,
1475                         { 
1476                                 flesh                           => 4,
1477                                 flesh_fields    => { 
1478                                         circ => [ qw/ target_copy / ],
1479                                         acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number / ],
1480                                         ascecm => [ qw/ stat_cat stat_cat_entry / ],
1481                                         acn => [ qw/ record / ],
1482                                 }
1483                         }
1484                 ]
1485         ) or return $e->event;
1486         
1487         my $copy = $circ->target_copy;
1488         my $vol = $copy->call_number;
1489         my $rec = $circ->target_copy->call_number->record;
1490
1491         $vol->record($rec->id);
1492         $copy->call_number($vol->id);
1493         $circ->target_copy($copy->id);
1494
1495         my $mvr;
1496
1497         if( $rec->id == OILS_PRECAT_RECORD ) {
1498                 $rec = undef;
1499                 $vol = undef;
1500         } else { 
1501                 $mvr = $U->record_to_mvr($rec);
1502                 $rec->marc(''); # drop the bulky marc data
1503         }
1504
1505         return {
1506                 circ => $circ,
1507                 copy => $copy,
1508                 volume => $vol,
1509                 record => $rec,
1510                 mvr => $mvr,
1511         };
1512 }
1513
1514
1515
1516 __PACKAGE__->register_method(
1517         method  => "test_batch_circ_events",
1518         api_name        => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1519 );
1520
1521 #  method for testing the behavior of a given event definition
1522 sub test_batch_circ_events {
1523     my($self, $conn, $auth, $event_def, $barcode) = @_;
1524
1525     my $e = new_editor(authtoken => $auth);
1526         return $e->event unless $e->checkauth;
1527     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1528
1529     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1530         or return $e->event;
1531
1532     my $circ = $e->search_action_circulation(
1533         {target_copy => $copy->id, checkin_time => undef})->[0]
1534         or return $e->event;
1535         
1536     return undef unless $circ;
1537
1538     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1539 }
1540
1541
1542 __PACKAGE__->register_method(
1543         method  => "fire_circ_events", 
1544         api_name        => "open-ils.circ.fire_circ_trigger_events",
1545     signature => q/
1546         General event def runner for circ objects.  If no event def ID
1547         is provided, the hook will be used to find the best event_def
1548         match based on the context org unit
1549     /
1550 );
1551
1552 __PACKAGE__->register_method(
1553         method  => "fire_circ_events", 
1554         api_name        => "open-ils.circ.fire_hold_trigger_events",
1555     signature => q/
1556         General event def runner for hold objects.  If no event def ID
1557         is provided, the hook will be used to find the best event_def
1558         match based on the context org unit
1559     /
1560 );
1561
1562 __PACKAGE__->register_method(
1563         method  => "fire_circ_events", 
1564         api_name        => "open-ils.circ.fire_user_trigger_events",
1565     signature => q/
1566         General event def runner for user objects.  If no event def ID
1567         is provided, the hook will be used to find the best event_def
1568         match based on the context org unit
1569     /
1570 );
1571
1572
1573 sub fire_circ_events {
1574     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1575
1576     my $e = new_editor(authtoken => $auth, xact => 1);
1577         return $e->event unless $e->checkauth;
1578
1579     my $targets;
1580
1581     if($self->api_name =~ /hold/) {
1582         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1583         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1584     } elsif($self->api_name =~ /user/) {
1585         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1586         $targets = $e->batch_retrieve_actor_user($target_ids);
1587     } else {
1588         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1589         $targets = $e->batch_retrieve_action_circulation($target_ids);
1590     }
1591     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1592                   # simply making this method authoritative because of weirdness
1593                   # with transaction handling in A/T code that causes rollback
1594                   # failure down the line if handling many targets
1595
1596     return undef unless @$targets;
1597     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1598 }
1599
1600 __PACKAGE__->register_method(
1601         method  => "user_payments_list",
1602         api_name        => "open-ils.circ.user_payments.filtered.batch",
1603     stream => 1,
1604         signature => {
1605         desc => q/Returns a fleshed, date-limited set of all payments a user
1606                 has made.  By default, ordered by payment date.  Optionally
1607                 ordered by other columns in the top-level "mp" object/,
1608         params => [
1609             {desc => 'Authentication token', type => 'string'},
1610             {desc => 'User ID', type => 'number'},
1611             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1612         ],
1613         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1614             and the related fully-realized payment object (e.g money.cash_payment)/}
1615     }
1616 );
1617
1618 sub user_payments_list {
1619     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1620
1621     my $e = new_editor(authtoken => $auth);
1622     return $e->event unless $e->checkauth;
1623
1624     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1625     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1626
1627     $order_by ||= ['payment_ts'];
1628
1629     # all payments by user, between start_date and end_date
1630     my $payments = $e->json_query({
1631         select => {mp => ['id']}, 
1632         from => {
1633             mp => {
1634                 mbt => {
1635                     fkey => 'xact', field => 'id'}
1636             }
1637         }, 
1638         where => {
1639             '+mbt' => {usr => $user_id}, 
1640             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1641         },
1642         order_by => {mp => $order_by}
1643     });
1644
1645     for my $payment_id (@$payments) {
1646         my $payment = $e->retrieve_money_payment([
1647             $payment_id->{id}, 
1648             {   
1649                 flesh => 2,
1650                 flesh_fields => {
1651                     mp => [
1652                         'xact',
1653                         'cash_payment',
1654                         'credit_card_payment',
1655                         'credit_payment',
1656                         'check_payment',
1657                         'work_payment',
1658                         'forgive_payment',
1659                         'goods_payment'
1660                     ],
1661                     mbt => [
1662                         'circulation', 
1663                         'grocery',
1664                         'reservation'
1665                     ]
1666                 }
1667             }
1668         ]);
1669         $conn->respond($payment);
1670     }
1671
1672     return undef;
1673 }
1674
1675
1676 __PACKAGE__->register_method(
1677         method  => "retrieve_circ_chain",
1678         api_name        => "open-ils.circ.renewal_chain.retrieve_by_circ",
1679     stream => 1,
1680         signature => {
1681         desc => q/Given a circulation, this returns all circulation objects
1682                 that are part of the same chain of renewals./,
1683         params => [
1684             {desc => 'Authentication token', type => 'string'},
1685             {desc => 'Circ ID', type => 'number'},
1686         ],
1687         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1688     }
1689 );
1690
1691 __PACKAGE__->register_method(
1692         method  => "retrieve_circ_chain",
1693         api_name        => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1694         signature => {
1695         desc => q/Given a circulation, this returns a summary of the circulation objects
1696                 that are part of the same chain of renewals./,
1697         params => [
1698             {desc => 'Authentication token', type => 'string'},
1699             {desc => 'Circ ID', type => 'number'},
1700         ],
1701         return => {desc => q/Circulation Chain Summary/}
1702     }
1703 );
1704
1705 sub retrieve_circ_chain {
1706     my($self, $conn, $auth, $circ_id) = @_;
1707
1708     my $e = new_editor(authtoken => $auth);
1709     return $e->event unless $e->checkauth;
1710         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1711
1712     if($self->api_name =~ /summary/) {
1713         return $U->create_circ_chain_summary($e, $circ_id);
1714
1715     } else {
1716
1717         my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1718
1719         for my $circ_info (@$chain) {
1720             my $circ = Fieldmapper::action::circulation->new;
1721             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1722             $conn->respond($circ);
1723         }
1724     }
1725
1726     return undef;
1727 }
1728
1729 __PACKAGE__->register_method(
1730         method  => "retrieve_prev_circ_chain",
1731         api_name        => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1732     stream => 1,
1733         signature => {
1734         desc => q/Given a circulation, this returns all circulation objects
1735                 that are part of the previous chain of renewals./,
1736         params => [
1737             {desc => 'Authentication token', type => 'string'},
1738             {desc => 'Circ ID', type => 'number'},
1739         ],
1740         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1741     }
1742 );
1743
1744 __PACKAGE__->register_method(
1745         method  => "retrieve_prev_circ_chain",
1746         api_name        => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1747         signature => {
1748         desc => q/Given a circulation, this returns a summary of the circulation objects
1749                 that are part of the previous chain of renewals./,
1750         params => [
1751             {desc => 'Authentication token', type => 'string'},
1752             {desc => 'Circ ID', type => 'number'},
1753         ],
1754         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1755     }
1756 );
1757
1758 sub retrieve_prev_circ_chain {
1759     my($self, $conn, $auth, $circ_id) = @_;
1760
1761     my $e = new_editor(authtoken => $auth);
1762     return $e->event unless $e->checkauth;
1763         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1764
1765     if($self->api_name =~ /summary/) {
1766         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1767         my $target_copy = $$first_circ{'target_copy'};
1768         my $usr = $$first_circ{'usr'};
1769         my $last_circ_from_prev_chain = $e->json_query({
1770             'select' => { 'circ' => ['id','usr'] },
1771             'from' => 'circ', 
1772             'where' => {
1773                 target_copy => $target_copy,
1774                 xact_start => { '<' => $$first_circ{'xact_start'} }
1775             },
1776             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1777             'limit' => 1
1778         })->[0];
1779         return undef unless $last_circ_from_prev_chain;
1780         return undef unless $$last_circ_from_prev_chain{'id'};
1781         my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1782         return undef unless $sum;
1783         my $obj = Fieldmapper::action::circ_chain_summary->new;
1784         $obj->$_($sum->{$_}) for keys %$sum;
1785         return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1786
1787     } else {
1788
1789         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1790         my $target_copy = $$first_circ{'target_copy'};
1791         my $last_circ_from_prev_chain = $e->json_query({
1792             'select' => { 'circ' => ['id'] },
1793             'from' => 'circ', 
1794             'where' => {
1795                 target_copy => $target_copy,
1796                 xact_start => { '<' => $$first_circ{'xact_start'} }
1797             },
1798             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1799             'limit' => 1
1800         })->[0];
1801         return undef unless $last_circ_from_prev_chain;
1802         return undef unless $$last_circ_from_prev_chain{'id'};
1803         my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1804
1805         for my $circ_info (@$chain) {
1806             my $circ = Fieldmapper::action::circulation->new;
1807             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1808             $conn->respond($circ);
1809         }
1810     }
1811
1812     return undef;
1813 }
1814
1815
1816 __PACKAGE__->register_method(
1817         method  => "get_copy_due_date",
1818         api_name        => "open-ils.circ.copy.due_date.retrieve",
1819         signature => {
1820         desc => q/
1821             Given a copy ID, returns the due date for the copy if it's 
1822             currently circulating.  Otherwise, returns null.  Note, this is a public 
1823             method requiring no authentication.  Only the due date is exposed.
1824             /,
1825         params => [
1826             {desc => 'Copy ID', type => 'number'}
1827         ],
1828         return => {desc => q/
1829             Due date (ISO date stamp) if the copy is circulating, null otherwise.
1830         /}
1831     }
1832 );
1833
1834 sub get_copy_due_date {
1835     my($self, $conn, $copy_id) = @_;
1836     my $e = new_editor();
1837
1838     my $circ = $e->json_query({
1839         select => {circ => ['due_date']},
1840         from => 'circ',
1841         where => {
1842             target_copy => $copy_id,
1843             checkin_time => undef,
1844             '-or' => [
1845                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1846                 {stop_fines => undef}
1847             ],
1848         },
1849         limit => 1
1850     })->[0] or return undef;
1851
1852     return $circ->{due_date};
1853 }
1854
1855
1856
1857
1858
1859 # {"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}}
1860
1861
1862 1;