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