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