Merge remote-tracking branch 'remotes/working/user/dbs/tt-opac-delete-poc' into dbs...
[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     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1310
1311         my $e2 = new_editor(authtoken=>$auth, xact =>1);
1312         return $e2->die_event unless $e2->checkauth;
1313     $args ||= {};
1314
1315     my $copy = $e2->retrieve_asset_copy([
1316         $copy_id,
1317         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1318             or return $e2->die_event;
1319
1320     my $owning_lib = 
1321         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1322             $copy->circ_lib : $copy->call_number->owning_lib;
1323
1324     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1325
1326     #### grab the last circulation
1327     my $circ = $e2->search_action_circulation([
1328         {   target_copy => $copy->id}, 
1329         {   limit => 1, 
1330             order_by => {circ => "xact_start DESC"}
1331         }
1332     ])->[0];
1333
1334     if (!$circ) {
1335         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1336         $e2->rollback;
1337         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1338     }
1339
1340         my $holds = $e2->search_action_hold_request(
1341                 { 
1342                         current_copy => $copy->id,
1343                         fulfillment_time => undef,
1344                         cancel_time => undef,
1345                 }
1346         );
1347
1348     $logger->debug("resetting holds that target the marked copy");
1349     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1350
1351     
1352         if (! $e2->commit) {
1353         return $e2->die_event;
1354     }
1355
1356         my $e = new_editor(authtoken=>$auth, xact =>1);
1357         return $e->die_event unless $e->checkauth;
1358
1359     if (! $circ->checkin_time) { # if circ active, attempt renew
1360         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1361         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1362         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1363             $circ = $res->[0]->{payload}{'circ'};
1364             $circ->target_copy( $copy->id );
1365             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1366         } else {
1367             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1368         }
1369     } else {
1370
1371         my $co_params = {
1372             'copy_id'=>$circ->target_copy,
1373             'patron_id'=>$circ->usr,
1374             'skip_deposit_fee'=>1,
1375             'skip_rental_fee'=>1
1376         };
1377
1378         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1379
1380             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1381                 $e, $copy, $e->requestor, 1 );
1382
1383             if ($hold) { # needed for hold? then due now
1384
1385                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1386                 my $due_date = DateTime->now(time_zone => 'local');
1387                 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1388             } else {
1389                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1390             }
1391         }
1392
1393         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params);
1394         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1395         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1396             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1397             $circ = $res->[0]->{payload}{'circ'};
1398         } else {
1399             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1400             $e->rollback;
1401             return $res;
1402         }
1403     }
1404
1405     ### Update the item status
1406
1407     my $custom_stat = $U->ou_ancestor_setting_value(
1408         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1409     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1410
1411     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1412     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1413
1414         $copy->status($stat);
1415         $copy->edit_date('now');
1416         $copy->editor($e->requestor->id);
1417
1418         $e->update_asset_copy($copy) or return $e->die_event;
1419
1420         if ($e->commit) {
1421
1422         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1423         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1424
1425         return OpenILS::Event->new('SUCCESS',
1426             payload => {
1427                 circ => $circ,
1428                 copy => $copy,
1429                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1430                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1431             }
1432         ); 
1433
1434     } else {
1435         return $e->die_event;
1436     }
1437 }
1438
1439
1440
1441
1442
1443 # ----------------------------------------------------------------------
1444 __PACKAGE__->register_method(
1445         method => 'magic_fetch',
1446         api_name => 'open-ils.agent.fetch'
1447 );
1448
1449 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1450
1451 sub magic_fetch {
1452         my( $self, $conn, $auth, $args ) = @_;
1453         my $e = new_editor( authtoken => $auth );
1454         return $e->event unless $e->checkauth;
1455
1456         my $hint = $$args{hint};
1457         my $id  = $$args{id};
1458
1459         # Is the call allowed to fetch this type of object?
1460         return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1461
1462         # Find the class the implements the given hint
1463         my ($class) = grep { 
1464                 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1465
1466         $class =~ s/Fieldmapper:://og;
1467         $class =~ s/::/_/og;
1468         my $method = "retrieve_$class";
1469
1470         my $obj = $e->$method($id) or return $e->event;
1471         return $obj;
1472 }
1473 # ----------------------------------------------------------------------
1474
1475
1476 __PACKAGE__->register_method(
1477         method  => "fleshed_circ_retrieve",
1478     authoritative => 1,
1479         api_name        => "open-ils.circ.fleshed.retrieve",);
1480
1481 sub fleshed_circ_retrieve {
1482         my( $self, $client, $id ) = @_;
1483         my $e = new_editor();
1484         my $circ = $e->retrieve_action_circulation(
1485                 [
1486                         $id,
1487                         { 
1488                                 flesh                           => 4,
1489                                 flesh_fields    => { 
1490                                         circ => [ qw/ target_copy / ],
1491                                         acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1492                                         ascecm => [ qw/ stat_cat stat_cat_entry / ],
1493                                         acn => [ qw/ record / ],
1494                                 }
1495                         }
1496                 ]
1497         ) or return $e->event;
1498         
1499         my $copy = $circ->target_copy;
1500         my $vol = $copy->call_number;
1501         my $rec = $circ->target_copy->call_number->record;
1502
1503         $vol->record($rec->id);
1504         $copy->call_number($vol->id);
1505         $circ->target_copy($copy->id);
1506
1507         my $mvr;
1508
1509         if( $rec->id == OILS_PRECAT_RECORD ) {
1510                 $rec = undef;
1511                 $vol = undef;
1512         } else { 
1513                 $mvr = $U->record_to_mvr($rec);
1514                 $rec->marc(''); # drop the bulky marc data
1515         }
1516
1517         return {
1518                 circ => $circ,
1519                 copy => $copy,
1520                 volume => $vol,
1521                 record => $rec,
1522                 mvr => $mvr,
1523         };
1524 }
1525
1526
1527
1528 __PACKAGE__->register_method(
1529         method  => "test_batch_circ_events",
1530         api_name        => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1531 );
1532
1533 #  method for testing the behavior of a given event definition
1534 sub test_batch_circ_events {
1535     my($self, $conn, $auth, $event_def, $barcode) = @_;
1536
1537     my $e = new_editor(authtoken => $auth);
1538         return $e->event unless $e->checkauth;
1539     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1540
1541     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1542         or return $e->event;
1543
1544     my $circ = $e->search_action_circulation(
1545         {target_copy => $copy->id, checkin_time => undef})->[0]
1546         or return $e->event;
1547         
1548     return undef unless $circ;
1549
1550     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1551 }
1552
1553
1554 __PACKAGE__->register_method(
1555         method  => "fire_circ_events", 
1556         api_name        => "open-ils.circ.fire_circ_trigger_events",
1557     signature => q/
1558         General event def runner for circ 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_hold_trigger_events",
1567     signature => q/
1568         General event def runner for hold 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 __PACKAGE__->register_method(
1575         method  => "fire_circ_events", 
1576         api_name        => "open-ils.circ.fire_user_trigger_events",
1577     signature => q/
1578         General event def runner for user objects.  If no event def ID
1579         is provided, the hook will be used to find the best event_def
1580         match based on the context org unit
1581     /
1582 );
1583
1584
1585 sub fire_circ_events {
1586     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1587
1588     my $e = new_editor(authtoken => $auth, xact => 1);
1589         return $e->event unless $e->checkauth;
1590
1591     my $targets;
1592
1593     if($self->api_name =~ /hold/) {
1594         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1595         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1596     } elsif($self->api_name =~ /user/) {
1597         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1598         $targets = $e->batch_retrieve_actor_user($target_ids);
1599     } else {
1600         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1601         $targets = $e->batch_retrieve_action_circulation($target_ids);
1602     }
1603     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1604                   # simply making this method authoritative because of weirdness
1605                   # with transaction handling in A/T code that causes rollback
1606                   # failure down the line if handling many targets
1607
1608     return undef unless @$targets;
1609     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1610 }
1611
1612 __PACKAGE__->register_method(
1613         method  => "user_payments_list",
1614         api_name        => "open-ils.circ.user_payments.filtered.batch",
1615     stream => 1,
1616         signature => {
1617         desc => q/Returns a fleshed, date-limited set of all payments a user
1618                 has made.  By default, ordered by payment date.  Optionally
1619                 ordered by other columns in the top-level "mp" object/,
1620         params => [
1621             {desc => 'Authentication token', type => 'string'},
1622             {desc => 'User ID', type => 'number'},
1623             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1624         ],
1625         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1626             and the related fully-realized payment object (e.g money.cash_payment)/}
1627     }
1628 );
1629
1630 sub user_payments_list {
1631     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1632
1633     my $e = new_editor(authtoken => $auth);
1634     return $e->event unless $e->checkauth;
1635
1636     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1637     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1638
1639     $order_by ||= ['payment_ts'];
1640
1641     # all payments by user, between start_date and end_date
1642     my $payments = $e->json_query({
1643         select => {mp => ['id']}, 
1644         from => {
1645             mp => {
1646                 mbt => {
1647                     fkey => 'xact', field => 'id'}
1648             }
1649         }, 
1650         where => {
1651             '+mbt' => {usr => $user_id}, 
1652             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1653         },
1654         order_by => {mp => $order_by}
1655     });
1656
1657     for my $payment_id (@$payments) {
1658         my $payment = $e->retrieve_money_payment([
1659             $payment_id->{id}, 
1660             {   
1661                 flesh => 2,
1662                 flesh_fields => {
1663                     mp => [
1664                         'xact',
1665                         'cash_payment',
1666                         'credit_card_payment',
1667                         'credit_payment',
1668                         'check_payment',
1669                         'work_payment',
1670                         'forgive_payment',
1671                         'goods_payment'
1672                     ],
1673                     mbt => [
1674                         'circulation', 
1675                         'grocery',
1676                         'reservation'
1677                     ]
1678                 }
1679             }
1680         ]);
1681         $conn->respond($payment);
1682     }
1683
1684     return undef;
1685 }
1686
1687
1688 __PACKAGE__->register_method(
1689         method  => "retrieve_circ_chain",
1690         api_name        => "open-ils.circ.renewal_chain.retrieve_by_circ",
1691     stream => 1,
1692         signature => {
1693         desc => q/Given a circulation, this returns all circulation objects
1694                 that are part of the same chain of renewals./,
1695         params => [
1696             {desc => 'Authentication token', type => 'string'},
1697             {desc => 'Circ ID', type => 'number'},
1698         ],
1699         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1700     }
1701 );
1702
1703 __PACKAGE__->register_method(
1704         method  => "retrieve_circ_chain",
1705         api_name        => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1706         signature => {
1707         desc => q/Given a circulation, this returns a summary of the circulation objects
1708                 that are part of the same chain of renewals./,
1709         params => [
1710             {desc => 'Authentication token', type => 'string'},
1711             {desc => 'Circ ID', type => 'number'},
1712         ],
1713         return => {desc => q/Circulation Chain Summary/}
1714     }
1715 );
1716
1717 sub retrieve_circ_chain {
1718     my($self, $conn, $auth, $circ_id) = @_;
1719
1720     my $e = new_editor(authtoken => $auth);
1721     return $e->event unless $e->checkauth;
1722         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1723
1724     if($self->api_name =~ /summary/) {
1725         return $U->create_circ_chain_summary($e, $circ_id);
1726
1727     } else {
1728
1729         my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1730
1731         for my $circ_info (@$chain) {
1732             my $circ = Fieldmapper::action::circulation->new;
1733             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1734             $conn->respond($circ);
1735         }
1736     }
1737
1738     return undef;
1739 }
1740
1741 __PACKAGE__->register_method(
1742         method  => "retrieve_prev_circ_chain",
1743         api_name        => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1744     stream => 1,
1745         signature => {
1746         desc => q/Given a circulation, this returns all circulation objects
1747                 that are part of the previous chain of renewals./,
1748         params => [
1749             {desc => 'Authentication token', type => 'string'},
1750             {desc => 'Circ ID', type => 'number'},
1751         ],
1752         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1753     }
1754 );
1755
1756 __PACKAGE__->register_method(
1757         method  => "retrieve_prev_circ_chain",
1758         api_name        => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1759         signature => {
1760         desc => q/Given a circulation, this returns a summary of the circulation objects
1761                 that are part of the previous chain of renewals./,
1762         params => [
1763             {desc => 'Authentication token', type => 'string'},
1764             {desc => 'Circ ID', type => 'number'},
1765         ],
1766         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1767     }
1768 );
1769
1770 sub retrieve_prev_circ_chain {
1771     my($self, $conn, $auth, $circ_id) = @_;
1772
1773     my $e = new_editor(authtoken => $auth);
1774     return $e->event unless $e->checkauth;
1775         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1776
1777     if($self->api_name =~ /summary/) {
1778         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1779         my $target_copy = $$first_circ{'target_copy'};
1780         my $usr = $$first_circ{'usr'};
1781         my $last_circ_from_prev_chain = $e->json_query({
1782             'select' => { 'circ' => ['id','usr'] },
1783             'from' => 'circ', 
1784             'where' => {
1785                 target_copy => $target_copy,
1786                 xact_start => { '<' => $$first_circ{'xact_start'} }
1787             },
1788             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1789             'limit' => 1
1790         })->[0];
1791         return undef unless $last_circ_from_prev_chain;
1792         return undef unless $$last_circ_from_prev_chain{'id'};
1793         my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1794         return undef unless $sum;
1795         my $obj = Fieldmapper::action::circ_chain_summary->new;
1796         $obj->$_($sum->{$_}) for keys %$sum;
1797         return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1798
1799     } else {
1800
1801         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1802         my $target_copy = $$first_circ{'target_copy'};
1803         my $last_circ_from_prev_chain = $e->json_query({
1804             'select' => { 'circ' => ['id'] },
1805             'from' => 'circ', 
1806             'where' => {
1807                 target_copy => $target_copy,
1808                 xact_start => { '<' => $$first_circ{'xact_start'} }
1809             },
1810             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1811             'limit' => 1
1812         })->[0];
1813         return undef unless $last_circ_from_prev_chain;
1814         return undef unless $$last_circ_from_prev_chain{'id'};
1815         my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1816
1817         for my $circ_info (@$chain) {
1818             my $circ = Fieldmapper::action::circulation->new;
1819             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1820             $conn->respond($circ);
1821         }
1822     }
1823
1824     return undef;
1825 }
1826
1827
1828 __PACKAGE__->register_method(
1829         method  => "get_copy_due_date",
1830         api_name        => "open-ils.circ.copy.due_date.retrieve",
1831         signature => {
1832         desc => q/
1833             Given a copy ID, returns the due date for the copy if it's 
1834             currently circulating.  Otherwise, returns null.  Note, this is a public 
1835             method requiring no authentication.  Only the due date is exposed.
1836             /,
1837         params => [
1838             {desc => 'Copy ID', type => 'number'}
1839         ],
1840         return => {desc => q/
1841             Due date (ISO date stamp) if the copy is circulating, null otherwise.
1842         /}
1843     }
1844 );
1845
1846 sub get_copy_due_date {
1847     my($self, $conn, $copy_id) = @_;
1848     my $e = new_editor();
1849
1850     my $circ = $e->json_query({
1851         select => {circ => ['due_date']},
1852         from => 'circ',
1853         where => {
1854             target_copy => $copy_id,
1855             checkin_time => undef,
1856             '-or' => [
1857                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1858                 {stop_fines => undef}
1859             ],
1860         },
1861         limit => 1
1862     })->[0] or return undef;
1863
1864     return $circ->{due_date};
1865 }
1866
1867
1868
1869
1870
1871 # {"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}}
1872
1873
1874 1;