8f439e59973c74e09c66360a91fd53d9f4f5cda4
[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
1007 __PACKAGE__->register_method(
1008     method => 'age_hold_rules',
1009     api_name    =>  'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1010 );
1011
1012 sub age_hold_rules {
1013     my( $self, $conn ) = @_;
1014     return new_editor()->retrieve_all_config_rules_age_hold_protect();
1015 }
1016
1017
1018
1019 __PACKAGE__->register_method(
1020     method => 'copy_details_barcode',
1021     authoritative => 1,
1022     api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1023 sub copy_details_barcode {
1024     my( $self, $conn, $auth, $barcode ) = @_;
1025     my $e = new_editor();
1026     my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1027     return $e->event unless $cid;
1028     return copy_details( $self, $conn, $auth, $cid );
1029 }
1030
1031
1032 __PACKAGE__->register_method(
1033     method => 'copy_details',
1034     api_name => 'open-ils.circ.copy_details.retrieve');
1035
1036 sub copy_details {
1037     my( $self, $conn, $auth, $copy_id ) = @_;
1038     my $e = new_editor(authtoken=>$auth);
1039     return $e->event unless $e->checkauth;
1040
1041     my $flesh = { flesh => 1 };
1042
1043     my $copy = $e->retrieve_asset_copy(
1044         [
1045             $copy_id,
1046             {
1047                 flesh => 2,
1048                 flesh_fields => {
1049                     acp => ['call_number','parts','peer_record_maps','floating'],
1050                     acn => ['record','prefix','suffix','label_class']
1051                 }
1052             }
1053         ]) or return $e->event;
1054
1055
1056     # De-flesh the copy for backwards compatibility
1057     my $mvr;
1058     my $vol = $copy->call_number;
1059     if( ref $vol ) {
1060         $copy->call_number($vol->id);
1061         my $record = $vol->record;
1062         if( ref $record ) {
1063             $vol->record($record->id);
1064             $mvr = $U->record_to_mvr($record);
1065         }
1066     }
1067
1068
1069     my $hold = $e->search_action_hold_request(
1070         { 
1071             current_copy        => $copy_id, 
1072             capture_time        => { "!=" => undef },
1073             fulfillment_time    => undef,
1074             cancel_time         => undef,
1075         }
1076     )->[0];
1077
1078     OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1079
1080     my $transit = $e->search_action_transit_copy(
1081         { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1082
1083     # find the most recent circulation for the requested copy,
1084     # be it active, completed, or aged.
1085     my $circ = $e->search_action_all_circulation([
1086         { target_copy => $copy_id },
1087         {
1088             flesh => 1,
1089             flesh_fields => {
1090                 combcirc => [
1091                     'workstation',
1092                     'checkin_workstation',
1093                     'duration_rule',
1094                     'max_fine_rule',
1095                     'recurring_fine_rule'
1096                 ],
1097             },
1098             order_by => { combcirc => 'xact_start desc' },
1099             limit => 1
1100         }
1101     ])->[0];
1102
1103     return {
1104         copy    => $copy,
1105         hold    => $hold,
1106         transit => $transit,
1107         circ    => $circ,
1108         volume  => $vol,
1109         mvr     => $mvr
1110     };
1111 }
1112
1113
1114
1115
1116 __PACKAGE__->register_method(
1117     method => 'mark_item',
1118     api_name => 'open-ils.circ.mark_item_damaged',
1119     signature   => q/
1120         Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1121         @param authtoken The login session key
1122         @param copy_id The ID of the copy to mark as damaged
1123         @return 1 on success - Event otherwise.
1124         /
1125 );
1126 __PACKAGE__->register_method(
1127     method => 'mark_item',
1128     api_name => 'open-ils.circ.mark_item_missing',
1129     signature   => q/
1130         Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1131         @param authtoken The login session key
1132         @param copy_id The ID of the copy to mark as missing 
1133         @return 1 on success - Event otherwise.
1134         /
1135 );
1136 __PACKAGE__->register_method(
1137     method => 'mark_item',
1138     api_name => 'open-ils.circ.mark_item_bindery',
1139     signature   => q/
1140         Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1141         @param authtoken The login session key
1142         @param copy_id The ID of the copy to mark as bindery
1143         @return 1 on success - Event otherwise.
1144         /
1145 );
1146 __PACKAGE__->register_method(
1147     method => 'mark_item',
1148     api_name => 'open-ils.circ.mark_item_on_order',
1149     signature   => q/
1150         Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1151         @param authtoken The login session key
1152         @param copy_id The ID of the copy to mark as on order 
1153         @return 1 on success - Event otherwise.
1154         /
1155 );
1156 __PACKAGE__->register_method(
1157     method => 'mark_item',
1158     api_name => 'open-ils.circ.mark_item_ill',
1159     signature   => q/
1160         Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1161         @param authtoken The login session key
1162         @param copy_id The ID of the copy to mark as inter-library loan
1163         @return 1 on success - Event otherwise.
1164         /
1165 );
1166 __PACKAGE__->register_method(
1167     method => 'mark_item',
1168     api_name => 'open-ils.circ.mark_item_cataloging',
1169     signature   => q/
1170         Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1171         @param authtoken The login session key
1172         @param copy_id The ID of the copy to mark as cataloging 
1173         @return 1 on success - Event otherwise.
1174         /
1175 );
1176 __PACKAGE__->register_method(
1177     method => 'mark_item',
1178     api_name => 'open-ils.circ.mark_item_reserves',
1179     signature   => q/
1180         Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1181         @param authtoken The login session key
1182         @param copy_id The ID of the copy to mark as reserves
1183         @return 1 on success - Event otherwise.
1184         /
1185 );
1186 __PACKAGE__->register_method(
1187     method => 'mark_item',
1188     api_name => 'open-ils.circ.mark_item_discard',
1189     signature   => q/
1190         Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1191         @param authtoken The login session key
1192         @param copy_id The ID of the copy to mark as discard
1193         @return 1 on success - Event otherwise.
1194         /
1195 );
1196
1197 sub mark_item {
1198     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1199     my $e = new_editor(authtoken=>$auth, xact =>1);
1200     return $e->die_event unless $e->checkauth;
1201     $args ||= {};
1202
1203     my $copy = $e->retrieve_asset_copy([
1204         $copy_id,
1205         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1206             or return $e->die_event;
1207
1208     my $owning_lib = 
1209         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1210             $copy->circ_lib : $copy->call_number->owning_lib;
1211
1212     my $perm = 'MARK_ITEM_MISSING';
1213     my $stat = OILS_COPY_STATUS_MISSING;
1214
1215     if( $self->api_name =~ /damaged/ ) {
1216         $perm = 'MARK_ITEM_DAMAGED';
1217         $stat = OILS_COPY_STATUS_DAMAGED;
1218         my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1219         return $evt if $evt;
1220
1221     } elsif ( $self->api_name =~ /bindery/ ) {
1222         $perm = 'MARK_ITEM_BINDERY';
1223         $stat = OILS_COPY_STATUS_BINDERY;
1224     } elsif ( $self->api_name =~ /on_order/ ) {
1225         $perm = 'MARK_ITEM_ON_ORDER';
1226         $stat = OILS_COPY_STATUS_ON_ORDER;
1227     } elsif ( $self->api_name =~ /ill/ ) {
1228         $perm = 'MARK_ITEM_ILL';
1229         $stat = OILS_COPY_STATUS_ILL;
1230     } elsif ( $self->api_name =~ /cataloging/ ) {
1231         $perm = 'MARK_ITEM_CATALOGING';
1232         $stat = OILS_COPY_STATUS_CATALOGING;
1233     } elsif ( $self->api_name =~ /reserves/ ) {
1234         $perm = 'MARK_ITEM_RESERVES';
1235         $stat = OILS_COPY_STATUS_RESERVES;
1236     } elsif ( $self->api_name =~ /discard/ ) {
1237         $perm = 'MARK_ITEM_DISCARD';
1238         $stat = OILS_COPY_STATUS_DISCARD;
1239     }
1240
1241     # caller may proceed if either perm is allowed
1242     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1243
1244     $copy->status($stat);
1245     $copy->edit_date('now');
1246     $copy->editor($e->requestor->id);
1247
1248     $e->update_asset_copy($copy) or return $e->die_event;
1249
1250     my $holds = $e->search_action_hold_request(
1251         { 
1252             current_copy => $copy->id,
1253             fulfillment_time => undef,
1254             cancel_time => undef,
1255         }
1256     );
1257
1258     $e->commit;
1259
1260     if( $self->api_name =~ /damaged/ ) {
1261         # now that we've committed the changes, create related A/T events
1262         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1263         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1264     }
1265
1266     $logger->debug("resetting holds that target the marked copy");
1267     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1268
1269     return 1;
1270 }
1271
1272 sub handle_mark_damaged {
1273     my($e, $copy, $owning_lib, $args) = @_;
1274
1275     my $apply = $args->{apply_fines} || '';
1276     return undef if $apply eq 'noapply';
1277
1278     my $new_amount = $args->{override_amount};
1279     my $new_btype = $args->{override_btype};
1280     my $new_note = $args->{override_note};
1281
1282     # grab the last circulation
1283     my $circ = $e->search_action_circulation([
1284         {   target_copy => $copy->id}, 
1285         {   limit => 1, 
1286             order_by => {circ => "xact_start DESC"},
1287             flesh => 2,
1288             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1289         }
1290     ])->[0];
1291
1292     return undef unless $circ;
1293
1294     my $charge_price = $U->ou_ancestor_setting_value(
1295         $owning_lib, 'circ.charge_on_damaged', $e);
1296
1297     my $proc_fee = $U->ou_ancestor_setting_value(
1298         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1299
1300     my $void_overdue = $U->ou_ancestor_setting_value(
1301         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1302
1303     return undef unless $charge_price or $proc_fee;
1304
1305     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1306     my $total = $copy_price + $proc_fee;
1307
1308     if($apply) {
1309         
1310         if($new_amount and $new_btype) {
1311
1312             # Allow staff to override the amount to charge for a damaged item
1313             # Consider the case where the item is only partially damaged
1314             # This value is meant to take the place of the item price and
1315             # optional processing fee.
1316
1317             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1318                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1319             return $evt if $evt;
1320
1321         } else {
1322
1323             if($charge_price and $copy_price) {
1324                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1325                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1326                 return $evt if $evt;
1327             }
1328
1329             if($proc_fee) {
1330                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1331                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1332                 return $evt if $evt;
1333             }
1334         }
1335
1336         # the assumption is that you would not void the overdues unless you 
1337         # were also charging for the item and/or applying a processing fee
1338         if($void_overdue) {
1339             my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1340             return $evt if $evt;
1341         }
1342
1343         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1344         return $evt if $evt;
1345
1346         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1347         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1348
1349         my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1350         return $evt2 if $evt2;
1351
1352         return undef;
1353
1354     } else {
1355         return OpenILS::Event->new('DAMAGE_CHARGE', 
1356             payload => {
1357                 circ => $circ,
1358                 charge => $total
1359             }
1360         );
1361     }
1362 }
1363
1364
1365
1366 # ----------------------------------------------------------------------
1367 __PACKAGE__->register_method(
1368     method => 'mark_item_missing_pieces',
1369     api_name => 'open-ils.circ.mark_item_missing_pieces',
1370     signature   => q/
1371         Changes the status of a copy to "damaged" or to a custom status based on the 
1372         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1373         permission.
1374         @param authtoken The login session key
1375         @param copy_id The ID of the copy to mark as damaged
1376         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1377         /
1378 );
1379
1380 sub mark_item_missing_pieces {
1381     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1382     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1383     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1384
1385     my $e2 = new_editor(authtoken=>$auth, xact =>1);
1386     return $e2->die_event unless $e2->checkauth;
1387     $args ||= {};
1388
1389     my $copy = $e2->retrieve_asset_copy([
1390         $copy_id,
1391         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1392             or return $e2->die_event;
1393
1394     my $owning_lib = 
1395         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1396             $copy->circ_lib : $copy->call_number->owning_lib;
1397
1398     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1399
1400     #### grab the last circulation
1401     my $circ = $e2->search_action_circulation([
1402         {   target_copy => $copy->id}, 
1403         {   limit => 1, 
1404             order_by => {circ => "xact_start DESC"}
1405         }
1406     ])->[0];
1407
1408     if (!$circ) {
1409         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1410         $e2->rollback;
1411         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1412     }
1413
1414     my $holds = $e2->search_action_hold_request(
1415         { 
1416             current_copy => $copy->id,
1417             fulfillment_time => undef,
1418             cancel_time => undef,
1419         }
1420     );
1421
1422     $logger->debug("resetting holds that target the marked copy");
1423     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1424
1425     
1426     if (! $e2->commit) {
1427         return $e2->die_event;
1428     }
1429
1430     my $e = new_editor(authtoken=>$auth, xact =>1);
1431     return $e->die_event unless $e->checkauth;
1432
1433     if (! $circ->checkin_time) { # if circ active, attempt renew
1434         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1435         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1436         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1437             $circ = $res->[0]->{payload}{'circ'};
1438             $circ->target_copy( $copy->id );
1439             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1440         } else {
1441             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1442         }
1443     } else {
1444
1445         my $co_params = {
1446             'copy_id'=>$circ->target_copy,
1447             'patron_id'=>$circ->usr,
1448             'skip_deposit_fee'=>1,
1449             'skip_rental_fee'=>1
1450         };
1451
1452         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1453
1454             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1455                 $e, $copy, $e->requestor, 1 );
1456
1457             if ($hold) { # needed for hold? then due now
1458
1459                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1460                 my $due_date = DateTime->now(time_zone => 'local');
1461                 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1462             } else {
1463                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1464             }
1465         }
1466
1467         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1468         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1469         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1470             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1471             $circ = $res->[0]->{payload}{'circ'};
1472         } else {
1473             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1474             $e->rollback;
1475             return $res;
1476         }
1477     }
1478
1479     ### Update the item status
1480
1481     my $custom_stat = $U->ou_ancestor_setting_value(
1482         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1483     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1484
1485     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1486     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1487
1488     $copy->status($stat);
1489     $copy->edit_date('now');
1490     $copy->editor($e->requestor->id);
1491
1492     $e->update_asset_copy($copy) or return $e->die_event;
1493
1494     if ($e->commit) {
1495
1496         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1497         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1498
1499         return OpenILS::Event->new('SUCCESS',
1500             payload => {
1501                 circ => $circ,
1502                 copy => $copy,
1503                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1504                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1505             }
1506         ); 
1507
1508     } else {
1509         return $e->die_event;
1510     }
1511 }
1512
1513
1514
1515
1516
1517 # ----------------------------------------------------------------------
1518 __PACKAGE__->register_method(
1519     method => 'magic_fetch',
1520     api_name => 'open-ils.agent.fetch'
1521 );
1522
1523 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1524
1525 sub magic_fetch {
1526     my( $self, $conn, $auth, $args ) = @_;
1527     my $e = new_editor( authtoken => $auth );
1528     return $e->event unless $e->checkauth;
1529
1530     my $hint = $$args{hint};
1531     my $id  = $$args{id};
1532
1533     # Is the call allowed to fetch this type of object?
1534     return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1535
1536     # Find the class the implements the given hint
1537     my ($class) = grep { 
1538         $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1539
1540     $class =~ s/Fieldmapper:://og;
1541     $class =~ s/::/_/og;
1542     my $method = "retrieve_$class";
1543
1544     my $obj = $e->$method($id) or return $e->event;
1545     return $obj;
1546 }
1547 # ----------------------------------------------------------------------
1548
1549
1550 __PACKAGE__->register_method(
1551     method  => "fleshed_circ_retrieve",
1552     authoritative => 1,
1553     api_name    => "open-ils.circ.fleshed.retrieve",);
1554
1555 sub fleshed_circ_retrieve {
1556     my( $self, $client, $id ) = @_;
1557     my $e = new_editor();
1558     my $circ = $e->retrieve_action_circulation(
1559         [
1560             $id,
1561             { 
1562                 flesh               => 4,
1563                 flesh_fields    => { 
1564                     circ => [ qw/ target_copy / ],
1565                     acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1566                     ascecm => [ qw/ stat_cat stat_cat_entry / ],
1567                     acn => [ qw/ record / ],
1568                 }
1569             }
1570         ]
1571     ) or return $e->event;
1572     
1573     my $copy = $circ->target_copy;
1574     my $vol = $copy->call_number;
1575     my $rec = $circ->target_copy->call_number->record;
1576
1577     $vol->record($rec->id);
1578     $copy->call_number($vol->id);
1579     $circ->target_copy($copy->id);
1580
1581     my $mvr;
1582
1583     if( $rec->id == OILS_PRECAT_RECORD ) {
1584         $rec = undef;
1585         $vol = undef;
1586     } else { 
1587         $mvr = $U->record_to_mvr($rec);
1588         $rec->marc(''); # drop the bulky marc data
1589     }
1590
1591     return {
1592         circ => $circ,
1593         copy => $copy,
1594         volume => $vol,
1595         record => $rec,
1596         mvr => $mvr,
1597     };
1598 }
1599
1600
1601
1602 __PACKAGE__->register_method(
1603     method  => "test_batch_circ_events",
1604     api_name    => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1605 );
1606
1607 #  method for testing the behavior of a given event definition
1608 sub test_batch_circ_events {
1609     my($self, $conn, $auth, $event_def, $barcode) = @_;
1610
1611     my $e = new_editor(authtoken => $auth);
1612     return $e->event unless $e->checkauth;
1613     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1614
1615     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1616         or return $e->event;
1617
1618     my $circ = $e->search_action_circulation(
1619         {target_copy => $copy->id, checkin_time => undef})->[0]
1620         or return $e->event;
1621         
1622     return undef unless $circ;
1623
1624     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1625 }
1626
1627
1628 __PACKAGE__->register_method(
1629     method  => "fire_circ_events", 
1630     api_name    => "open-ils.circ.fire_circ_trigger_events",
1631     signature => q/
1632         General event def runner for circ objects.  If no event def ID
1633         is provided, the hook will be used to find the best event_def
1634         match based on the context org unit
1635     /
1636 );
1637
1638 __PACKAGE__->register_method(
1639     method  => "fire_circ_events", 
1640     api_name    => "open-ils.circ.fire_hold_trigger_events",
1641     signature => q/
1642         General event def runner for hold objects.  If no event def ID
1643         is provided, the hook will be used to find the best event_def
1644         match based on the context org unit
1645     /
1646 );
1647
1648 __PACKAGE__->register_method(
1649     method  => "fire_circ_events", 
1650     api_name    => "open-ils.circ.fire_user_trigger_events",
1651     signature => q/
1652         General event def runner for user objects.  If no event def ID
1653         is provided, the hook will be used to find the best event_def
1654         match based on the context org unit
1655     /
1656 );
1657
1658
1659 sub fire_circ_events {
1660     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1661
1662     my $e = new_editor(authtoken => $auth, xact => 1);
1663     return $e->event unless $e->checkauth;
1664
1665     my $targets;
1666
1667     if($self->api_name =~ /hold/) {
1668         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1669         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1670     } elsif($self->api_name =~ /user/) {
1671         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1672         $targets = $e->batch_retrieve_actor_user($target_ids);
1673     } else {
1674         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1675         $targets = $e->batch_retrieve_action_circulation($target_ids);
1676     }
1677     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1678                   # simply making this method authoritative because of weirdness
1679                   # with transaction handling in A/T code that causes rollback
1680                   # failure down the line if handling many targets
1681
1682     return undef unless @$targets;
1683     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1684 }
1685
1686 __PACKAGE__->register_method(
1687     method  => "user_payments_list",
1688     api_name    => "open-ils.circ.user_payments.filtered.batch",
1689     stream => 1,
1690     signature => {
1691         desc => q/Returns a fleshed, date-limited set of all payments a user
1692                 has made.  By default, ordered by payment date.  Optionally
1693                 ordered by other columns in the top-level "mp" object/,
1694         params => [
1695             {desc => 'Authentication token', type => 'string'},
1696             {desc => 'User ID', type => 'number'},
1697             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1698         ],
1699         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1700             and the related fully-realized payment object (e.g money.cash_payment)/}
1701     }
1702 );
1703
1704 sub user_payments_list {
1705     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1706
1707     my $e = new_editor(authtoken => $auth);
1708     return $e->event unless $e->checkauth;
1709
1710     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1711     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1712
1713     $order_by ||= ['payment_ts'];
1714
1715     # all payments by user, between start_date and end_date
1716     my $payments = $e->json_query({
1717         select => {mp => ['id']}, 
1718         from => {
1719             mp => {
1720                 mbt => {
1721                     fkey => 'xact', field => 'id'}
1722             }
1723         }, 
1724         where => {
1725             '+mbt' => {usr => $user_id}, 
1726             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1727         },
1728         order_by => {mp => $order_by}
1729     });
1730
1731     for my $payment_id (@$payments) {
1732         my $payment = $e->retrieve_money_payment([
1733             $payment_id->{id}, 
1734             {   
1735                 flesh => 2,
1736                 flesh_fields => {
1737                     mp => [
1738                         'xact',
1739                         'cash_payment',
1740                         'credit_card_payment',
1741                         'credit_payment',
1742                         'check_payment',
1743                         'work_payment',
1744                         'forgive_payment',
1745                         'goods_payment'
1746                     ],
1747                     mbt => [
1748                         'circulation', 
1749                         'grocery',
1750                         'reservation'
1751                     ]
1752                 }
1753             }
1754         ]);
1755         $conn->respond($payment);
1756     }
1757
1758     return undef;
1759 }
1760
1761
1762 __PACKAGE__->register_method(
1763     method  => "retrieve_circ_chain",
1764     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ",
1765     stream => 1,
1766     signature => {
1767         desc => q/Given a circulation, this returns all circulation objects
1768                 that are part of the same chain of renewals./,
1769         params => [
1770             {desc => 'Authentication token', type => 'string'},
1771             {desc => 'Circ ID', type => 'number'},
1772         ],
1773         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1774     }
1775 );
1776
1777 __PACKAGE__->register_method(
1778     method  => "retrieve_circ_chain",
1779     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1780     signature => {
1781         desc => q/Given a circulation, this returns a summary of the circulation objects
1782                 that are part of the same chain of renewals./,
1783         params => [
1784             {desc => 'Authentication token', type => 'string'},
1785             {desc => 'Circ ID', type => 'number'},
1786         ],
1787         return => {desc => q/Circulation Chain Summary/}
1788     }
1789 );
1790
1791 sub retrieve_circ_chain {
1792     my($self, $conn, $auth, $circ_id) = @_;
1793
1794     my $e = new_editor(authtoken => $auth);
1795     return $e->event unless $e->checkauth;
1796     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1797
1798     if($self->api_name =~ /summary/) {
1799         return $U->create_circ_chain_summary($e, $circ_id);
1800
1801     } else {
1802
1803         my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
1804
1805         for my $circ_info (@$chain) {
1806             my $circ = Fieldmapper::action::all_circulation->new;
1807             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1808             $conn->respond($circ);
1809         }
1810     }
1811
1812     return undef;
1813 }
1814
1815 __PACKAGE__->register_method(
1816     method  => "retrieve_prev_circ_chain",
1817     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1818     stream => 1,
1819     signature => {
1820         desc => q/Given a circulation, this returns all circulation objects
1821                 that are part of the previous chain of renewals./,
1822         params => [
1823             {desc => 'Authentication token', type => 'string'},
1824             {desc => 'Circ ID', type => 'number'},
1825         ],
1826         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1827     }
1828 );
1829
1830 __PACKAGE__->register_method(
1831     method  => "retrieve_prev_circ_chain",
1832     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1833     signature => {
1834         desc => q/Given a circulation, this returns a summary of the circulation objects
1835                 that are part of the previous chain of renewals./,
1836         params => [
1837             {desc => 'Authentication token', type => 'string'},
1838             {desc => 'Circ ID', type => 'number'},
1839         ],
1840         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1841     }
1842 );
1843
1844 sub retrieve_prev_circ_chain {
1845     my($self, $conn, $auth, $circ_id) = @_;
1846
1847     my $e = new_editor(authtoken => $auth);
1848     return $e->event unless $e->checkauth;
1849     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1850
1851     my $first_circ = 
1852         $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
1853
1854     my $prev_circ = $e->search_action_all_circulation([
1855         {   target_copy => $first_circ->{target_copy},
1856             xact_start => {'<' => $first_circ->{xact_start}}
1857         }, {   
1858             flesh => 1,
1859             flesh_fields => {
1860                 combcirc => [
1861                     'active_circ',
1862                     'aged_circ'
1863                 ]
1864             },
1865             order_by => { combcirc => 'xact_start desc' },
1866             limit => 1 
1867         }
1868     ])->[0];
1869
1870     return undef unless $prev_circ;
1871
1872     my $chain_usr = $prev_circ->usr; # note: may be undef
1873
1874     if ($self->api_name =~ /summary/) {
1875         my $sum = $e->json_query({
1876             from => [
1877                 'action.summarize_all_circ_chain', 
1878                 $prev_circ->id
1879             ]
1880         })->[0];
1881
1882         my $summary = Fieldmapper::action::circ_chain_summary->new;
1883         $summary->$_($sum->{$_}) for keys %$sum;
1884
1885         return {summary => $summary, usr => $chain_usr};
1886     }
1887
1888
1889     my $chain = $e->json_query(
1890         {from => ['action.all_circ_chain', $prev_circ->id]});
1891
1892     for my $circ_info (@$chain) {
1893         my $circ = Fieldmapper::action::all_circulation->new;
1894         $circ->$_($circ_info->{$_}) for keys %$circ_info;
1895         $conn->respond($circ);
1896     }
1897
1898     return undef;
1899 }
1900
1901
1902 __PACKAGE__->register_method(
1903     method  => "get_copy_due_date",
1904     api_name    => "open-ils.circ.copy.due_date.retrieve",
1905     signature => {
1906         desc => q/
1907             Given a copy ID, returns the due date for the copy if it's 
1908             currently circulating.  Otherwise, returns null.  Note, this is a public 
1909             method requiring no authentication.  Only the due date is exposed.
1910             /,
1911         params => [
1912             {desc => 'Copy ID', type => 'number'}
1913         ],
1914         return => {desc => q/
1915             Due date (ISO date stamp) if the copy is circulating, null otherwise.
1916         /}
1917     }
1918 );
1919
1920 sub get_copy_due_date {
1921     my($self, $conn, $copy_id) = @_;
1922     my $e = new_editor();
1923
1924     my $circ = $e->json_query({
1925         select => {circ => ['due_date']},
1926         from => 'circ',
1927         where => {
1928             target_copy => $copy_id,
1929             checkin_time => undef,
1930             '-or' => [
1931                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1932                 {stop_fines => undef}
1933             ],
1934         },
1935         limit => 1
1936     })->[0] or return undef;
1937
1938     return $circ->{due_date};
1939 }
1940
1941
1942
1943
1944
1945 # {"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}}
1946
1947
1948 1;