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