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