LP#1187035 Remove OpenILS::Utils::Editor part 2.
[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, $range ) = @_; 
746
747     return $U->simplereq(
748         'open-ils.storage',
749         'open-ils.storage.asset.copy.circ_count',
750         $copyid, $range
751     );
752 }
753
754
755
756 __PACKAGE__->register_method(
757     method      => 'fetch_notes',
758     authoritative   => 1,
759     api_name        => 'open-ils.circ.copy_note.retrieve.all',
760     signature   => q/
761         Returns an array of copy note objects.  
762         @param args A named hash of parameters including:
763             authtoken   : Required if viewing non-public notes
764             itemid      : The id of the item whose notes we want to retrieve
765             pub         : True if all the caller wants are public notes
766         @return An array of note objects
767     /);
768
769 __PACKAGE__->register_method(
770     method      => 'fetch_notes',
771     api_name        => 'open-ils.circ.call_number_note.retrieve.all',
772     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
773
774 __PACKAGE__->register_method(
775     method      => 'fetch_notes',
776     api_name        => 'open-ils.circ.title_note.retrieve.all',
777     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
778
779
780 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
781 sub fetch_notes {
782     my( $self, $connection, $args ) = @_;
783
784     my $id = $$args{itemid};
785     my $authtoken = $$args{authtoken};
786     my( $r, $evt);
787
788     if( $self->api_name =~ /copy/ ) {
789         if( $$args{pub} ) {
790             return $U->cstorereq(
791                 'open-ils.cstore.direct.asset.copy_note.search.atomic',
792                 { owning_copy => $id, pub => 't' } );
793         } else {
794             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
795             return $evt if $evt;
796             return $U->cstorereq(
797                 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
798         }
799
800     } elsif( $self->api_name =~ /call_number/ ) {
801         if( $$args{pub} ) {
802             return $U->cstorereq(
803                 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
804                 { call_number => $id, pub => 't' } );
805         } else {
806             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
807             return $evt if $evt;
808             return $U->cstorereq(
809                 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
810         }
811
812     } elsif( $self->api_name =~ /title/ ) {
813         if( $$args{pub} ) {
814             return $U->cstorereq(
815                 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
816                 { record => $id, pub => 't' } );
817         } else {
818             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
819             return $evt if $evt;
820             return $U->cstorereq(
821                 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
822         }
823     }
824
825     return undef;
826 }
827
828 __PACKAGE__->register_method(
829     method  => 'has_notes',
830     api_name    => 'open-ils.circ.copy.has_notes');
831 __PACKAGE__->register_method(
832     method  => 'has_notes',
833     api_name    => 'open-ils.circ.call_number.has_notes');
834 __PACKAGE__->register_method(
835     method  => 'has_notes',
836     api_name    => 'open-ils.circ.title.has_notes');
837
838
839 sub has_notes {
840     my( $self, $conn, $authtoken, $id ) = @_;
841     my $editor = new_editor(authtoken => $authtoken);
842     return $editor->event unless $editor->checkauth;
843
844     my $n = $editor->search_asset_copy_note(
845         {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
846
847     $n = $editor->search_asset_call_number_note(
848         {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
849
850     $n = $editor->search_biblio_record_note(
851         {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
852
853     return scalar @$n;
854 }
855
856
857
858 __PACKAGE__->register_method(
859     method      => 'create_copy_note',
860     api_name        => 'open-ils.circ.copy_note.create',
861     signature   => q/
862         Creates a new copy note
863         @param authtoken The login session key
864         @param note The note object to create
865         @return The id of the new note object
866     /);
867
868 sub create_copy_note {
869     my( $self, $connection, $authtoken, $note ) = @_;
870
871     my $e = new_editor(xact=>1, authtoken=>$authtoken);
872     return $e->event unless $e->checkauth;
873     my $copy = $e->retrieve_asset_copy(
874         [
875             $note->owning_copy,
876             {   flesh => 1,
877                 flesh_fields => { 'acp' => ['call_number'] }
878             }
879         ]
880     );
881
882     return $e->event unless 
883         $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
884
885     $note->create_date('now');
886     $note->creator($e->requestor->id);
887     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
888     $note->clear_id;
889
890     $e->create_asset_copy_note($note) or return $e->event;
891     $e->commit;
892     return $note->id;
893 }
894
895
896 __PACKAGE__->register_method(
897     method      => 'delete_copy_note',
898     api_name        =>  'open-ils.circ.copy_note.delete',
899     signature   => q/
900         Deletes an existing copy note
901         @param authtoken The login session key
902         @param noteid The id of the note to delete
903         @return 1 on success - Event otherwise.
904         /);
905 sub delete_copy_note {
906     my( $self, $conn, $authtoken, $noteid ) = @_;
907
908     my $e = new_editor(xact=>1, authtoken=>$authtoken);
909     return $e->die_event unless $e->checkauth;
910
911     my $note = $e->retrieve_asset_copy_note([
912         $noteid,
913         { flesh => 2,
914             flesh_fields => {
915                 'acpn' => [ 'owning_copy' ],
916                 'acp' => [ 'call_number' ],
917             }
918         }
919     ]) or return $e->die_event;
920
921     if( $note->creator ne $e->requestor->id ) {
922         return $e->die_event unless 
923             $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
924     }
925
926     $e->delete_asset_copy_note($note) or return $e->die_event;
927     $e->commit;
928     return 1;
929 }
930
931
932 __PACKAGE__->register_method(
933     method => 'age_hold_rules',
934     api_name    =>  'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
935 );
936
937 sub age_hold_rules {
938     my( $self, $conn ) = @_;
939     return new_editor()->retrieve_all_config_rules_age_hold_protect();
940 }
941
942
943
944 __PACKAGE__->register_method(
945     method => 'copy_details_barcode',
946     authoritative => 1,
947     api_name => 'open-ils.circ.copy_details.retrieve.barcode');
948 sub copy_details_barcode {
949     my( $self, $conn, $auth, $barcode ) = @_;
950     my $e = new_editor();
951     my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
952     return $e->event unless $cid;
953     return copy_details( $self, $conn, $auth, $cid );
954 }
955
956
957 __PACKAGE__->register_method(
958     method => 'copy_details',
959     api_name => 'open-ils.circ.copy_details.retrieve');
960
961 sub copy_details {
962     my( $self, $conn, $auth, $copy_id ) = @_;
963     my $e = new_editor(authtoken=>$auth);
964     return $e->event unless $e->checkauth;
965
966     my $flesh = { flesh => 1 };
967
968     my $copy = $e->retrieve_asset_copy(
969         [
970             $copy_id,
971             {
972                 flesh => 2,
973                 flesh_fields => {
974                     acp => ['call_number','parts','peer_record_maps','floating'],
975                     acn => ['record','prefix','suffix','label_class']
976                 }
977             }
978         ]) or return $e->event;
979
980
981     # De-flesh the copy for backwards compatibility
982     my $mvr;
983     my $vol = $copy->call_number;
984     if( ref $vol ) {
985         $copy->call_number($vol->id);
986         my $record = $vol->record;
987         if( ref $record ) {
988             $vol->record($record->id);
989             $mvr = $U->record_to_mvr($record);
990         }
991     }
992
993
994     my $hold = $e->search_action_hold_request(
995         { 
996             current_copy        => $copy_id, 
997             capture_time        => { "!=" => undef },
998             fulfillment_time    => undef,
999             cancel_time         => undef,
1000         }
1001     )->[0];
1002
1003     OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1004
1005     my $transit = $e->search_action_transit_copy(
1006         { target_copy => $copy_id, dest_recv_time => undef } )->[0];
1007
1008     # find the latest circ, open or closed
1009     my $circ = $e->search_action_circulation(
1010         [
1011             { target_copy => $copy_id },
1012             { 
1013                 flesh => 1,
1014                 flesh_fields => {
1015                     circ => [
1016                         'workstation',
1017                         'checkin_workstation', 
1018                         'duration_rule', 
1019                         'max_fine_rule', 
1020                         'recurring_fine_rule'
1021                     ]
1022                 },
1023                 order_by => { circ => 'xact_start desc' }, 
1024                 limit => 1 
1025             }
1026         ]
1027     )->[0];
1028
1029
1030     return {
1031         copy        => $copy,
1032         hold        => $hold,
1033         transit => $transit,
1034         circ        => $circ,
1035         volume  => $vol,
1036         mvr     => $mvr,
1037     };
1038 }
1039
1040
1041
1042
1043 __PACKAGE__->register_method(
1044     method => 'mark_item',
1045     api_name => 'open-ils.circ.mark_item_damaged',
1046     signature   => q/
1047         Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1048         @param authtoken The login session key
1049         @param copy_id The ID of the copy to mark as damaged
1050         @return 1 on success - Event otherwise.
1051         /
1052 );
1053 __PACKAGE__->register_method(
1054     method => 'mark_item',
1055     api_name => 'open-ils.circ.mark_item_missing',
1056     signature   => q/
1057         Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1058         @param authtoken The login session key
1059         @param copy_id The ID of the copy to mark as missing 
1060         @return 1 on success - Event otherwise.
1061         /
1062 );
1063 __PACKAGE__->register_method(
1064     method => 'mark_item',
1065     api_name => 'open-ils.circ.mark_item_bindery',
1066     signature   => q/
1067         Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1068         @param authtoken The login session key
1069         @param copy_id The ID of the copy to mark as bindery
1070         @return 1 on success - Event otherwise.
1071         /
1072 );
1073 __PACKAGE__->register_method(
1074     method => 'mark_item',
1075     api_name => 'open-ils.circ.mark_item_on_order',
1076     signature   => q/
1077         Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1078         @param authtoken The login session key
1079         @param copy_id The ID of the copy to mark as on order 
1080         @return 1 on success - Event otherwise.
1081         /
1082 );
1083 __PACKAGE__->register_method(
1084     method => 'mark_item',
1085     api_name => 'open-ils.circ.mark_item_ill',
1086     signature   => q/
1087         Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1088         @param authtoken The login session key
1089         @param copy_id The ID of the copy to mark as inter-library loan
1090         @return 1 on success - Event otherwise.
1091         /
1092 );
1093 __PACKAGE__->register_method(
1094     method => 'mark_item',
1095     api_name => 'open-ils.circ.mark_item_cataloging',
1096     signature   => q/
1097         Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1098         @param authtoken The login session key
1099         @param copy_id The ID of the copy to mark as cataloging 
1100         @return 1 on success - Event otherwise.
1101         /
1102 );
1103 __PACKAGE__->register_method(
1104     method => 'mark_item',
1105     api_name => 'open-ils.circ.mark_item_reserves',
1106     signature   => q/
1107         Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1108         @param authtoken The login session key
1109         @param copy_id The ID of the copy to mark as reserves
1110         @return 1 on success - Event otherwise.
1111         /
1112 );
1113 __PACKAGE__->register_method(
1114     method => 'mark_item',
1115     api_name => 'open-ils.circ.mark_item_discard',
1116     signature   => q/
1117         Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1118         @param authtoken The login session key
1119         @param copy_id The ID of the copy to mark as discard
1120         @return 1 on success - Event otherwise.
1121         /
1122 );
1123
1124 sub mark_item {
1125     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1126     my $e = new_editor(authtoken=>$auth, xact =>1);
1127     return $e->die_event unless $e->checkauth;
1128     $args ||= {};
1129
1130     my $copy = $e->retrieve_asset_copy([
1131         $copy_id,
1132         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1133             or return $e->die_event;
1134
1135     my $owning_lib = 
1136         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1137             $copy->circ_lib : $copy->call_number->owning_lib;
1138
1139     my $perm = 'MARK_ITEM_MISSING';
1140     my $stat = OILS_COPY_STATUS_MISSING;
1141
1142     if( $self->api_name =~ /damaged/ ) {
1143         $perm = 'MARK_ITEM_DAMAGED';
1144         $stat = OILS_COPY_STATUS_DAMAGED;
1145         my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1146         return $evt if $evt;
1147
1148     } elsif ( $self->api_name =~ /bindery/ ) {
1149         $perm = 'MARK_ITEM_BINDERY';
1150         $stat = OILS_COPY_STATUS_BINDERY;
1151     } elsif ( $self->api_name =~ /on_order/ ) {
1152         $perm = 'MARK_ITEM_ON_ORDER';
1153         $stat = OILS_COPY_STATUS_ON_ORDER;
1154     } elsif ( $self->api_name =~ /ill/ ) {
1155         $perm = 'MARK_ITEM_ILL';
1156         $stat = OILS_COPY_STATUS_ILL;
1157     } elsif ( $self->api_name =~ /cataloging/ ) {
1158         $perm = 'MARK_ITEM_CATALOGING';
1159         $stat = OILS_COPY_STATUS_CATALOGING;
1160     } elsif ( $self->api_name =~ /reserves/ ) {
1161         $perm = 'MARK_ITEM_RESERVES';
1162         $stat = OILS_COPY_STATUS_RESERVES;
1163     } elsif ( $self->api_name =~ /discard/ ) {
1164         $perm = 'MARK_ITEM_DISCARD';
1165         $stat = OILS_COPY_STATUS_DISCARD;
1166     }
1167
1168     # caller may proceed if either perm is allowed
1169     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1170
1171     $copy->status($stat);
1172     $copy->edit_date('now');
1173     $copy->editor($e->requestor->id);
1174
1175     $e->update_asset_copy($copy) or return $e->die_event;
1176
1177     my $holds = $e->search_action_hold_request(
1178         { 
1179             current_copy => $copy->id,
1180             fulfillment_time => undef,
1181             cancel_time => undef,
1182         }
1183     );
1184
1185     $e->commit;
1186
1187     if( $self->api_name =~ /damaged/ ) {
1188         # now that we've committed the changes, create related A/T events
1189         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1190         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1191     }
1192
1193     $logger->debug("resetting holds that target the marked copy");
1194     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1195
1196     return 1;
1197 }
1198
1199 sub handle_mark_damaged {
1200     my($e, $copy, $owning_lib, $args) = @_;
1201
1202     my $apply = $args->{apply_fines} || '';
1203     return undef if $apply eq 'noapply';
1204
1205     my $new_amount = $args->{override_amount};
1206     my $new_btype = $args->{override_btype};
1207     my $new_note = $args->{override_note};
1208
1209     # grab the last circulation
1210     my $circ = $e->search_action_circulation([
1211         {   target_copy => $copy->id}, 
1212         {   limit => 1, 
1213             order_by => {circ => "xact_start DESC"},
1214             flesh => 2,
1215             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1216         }
1217     ])->[0];
1218
1219     return undef unless $circ;
1220
1221     my $charge_price = $U->ou_ancestor_setting_value(
1222         $owning_lib, 'circ.charge_on_damaged', $e);
1223
1224     my $proc_fee = $U->ou_ancestor_setting_value(
1225         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1226
1227     my $void_overdue = $U->ou_ancestor_setting_value(
1228         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1229
1230     return undef unless $charge_price or $proc_fee;
1231
1232     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1233     my $total = $copy_price + $proc_fee;
1234
1235     if($apply) {
1236         
1237         if($new_amount and $new_btype) {
1238
1239             # Allow staff to override the amount to charge for a damaged item
1240             # Consider the case where the item is only partially damaged
1241             # This value is meant to take the place of the item price and
1242             # optional processing fee.
1243
1244             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1245                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1246             return $evt if $evt;
1247
1248         } else {
1249
1250             if($charge_price and $copy_price) {
1251                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1252                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1253                 return $evt if $evt;
1254             }
1255
1256             if($proc_fee) {
1257                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1258                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1259                 return $evt if $evt;
1260             }
1261         }
1262
1263         # the assumption is that you would not void the overdues unless you 
1264         # were also charging for the item and/or applying a processing fee
1265         if($void_overdue) {
1266             my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
1267             return $evt if $evt;
1268         }
1269
1270         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1271         return $evt if $evt;
1272
1273         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1274         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1275
1276         my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1277         return $evt2 if $evt2;
1278
1279         return undef;
1280
1281     } else {
1282         return OpenILS::Event->new('DAMAGE_CHARGE', 
1283             payload => {
1284                 circ => $circ,
1285                 charge => $total
1286             }
1287         );
1288     }
1289 }
1290
1291
1292
1293 # ----------------------------------------------------------------------
1294 __PACKAGE__->register_method(
1295     method => 'mark_item_missing_pieces',
1296     api_name => 'open-ils.circ.mark_item_missing_pieces',
1297     signature   => q/
1298         Changes the status of a copy to "damaged" or to a custom status based on the 
1299         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1300         permission.
1301         @param authtoken The login session key
1302         @param copy_id The ID of the copy to mark as damaged
1303         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1304         /
1305 );
1306
1307 sub mark_item_missing_pieces {
1308     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1309     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1310     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1311
1312     my $e2 = new_editor(authtoken=>$auth, xact =>1);
1313     return $e2->die_event unless $e2->checkauth;
1314     $args ||= {};
1315
1316     my $copy = $e2->retrieve_asset_copy([
1317         $copy_id,
1318         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1319             or return $e2->die_event;
1320
1321     my $owning_lib = 
1322         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1323             $copy->circ_lib : $copy->call_number->owning_lib;
1324
1325     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1326
1327     #### grab the last circulation
1328     my $circ = $e2->search_action_circulation([
1329         {   target_copy => $copy->id}, 
1330         {   limit => 1, 
1331             order_by => {circ => "xact_start DESC"}
1332         }
1333     ])->[0];
1334
1335     if (!$circ) {
1336         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1337         $e2->rollback;
1338         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1339     }
1340
1341     my $holds = $e2->search_action_hold_request(
1342         { 
1343             current_copy => $copy->id,
1344             fulfillment_time => undef,
1345             cancel_time => undef,
1346         }
1347     );
1348
1349     $logger->debug("resetting holds that target the marked copy");
1350     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1351
1352     
1353     if (! $e2->commit) {
1354         return $e2->die_event;
1355     }
1356
1357     my $e = new_editor(authtoken=>$auth, xact =>1);
1358     return $e->die_event unless $e->checkauth;
1359
1360     if (! $circ->checkin_time) { # if circ active, attempt renew
1361         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1362         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1363         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1364             $circ = $res->[0]->{payload}{'circ'};
1365             $circ->target_copy( $copy->id );
1366             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1367         } else {
1368             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1369         }
1370     } else {
1371
1372         my $co_params = {
1373             'copy_id'=>$circ->target_copy,
1374             'patron_id'=>$circ->usr,
1375             'skip_deposit_fee'=>1,
1376             'skip_rental_fee'=>1
1377         };
1378
1379         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1380
1381             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1382                 $e, $copy, $e->requestor, 1 );
1383
1384             if ($hold) { # needed for hold? then due now
1385
1386                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1387                 my $due_date = DateTime->now(time_zone => 'local');
1388                 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1389             } else {
1390                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1391             }
1392         }
1393
1394         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1395         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1396         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1397             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1398             $circ = $res->[0]->{payload}{'circ'};
1399         } else {
1400             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1401             $e->rollback;
1402             return $res;
1403         }
1404     }
1405
1406     ### Update the item status
1407
1408     my $custom_stat = $U->ou_ancestor_setting_value(
1409         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1410     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1411
1412     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1413     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1414
1415     $copy->status($stat);
1416     $copy->edit_date('now');
1417     $copy->editor($e->requestor->id);
1418
1419     $e->update_asset_copy($copy) or return $e->die_event;
1420
1421     if ($e->commit) {
1422
1423         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1424         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1425
1426         return OpenILS::Event->new('SUCCESS',
1427             payload => {
1428                 circ => $circ,
1429                 copy => $copy,
1430                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1431                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1432             }
1433         ); 
1434
1435     } else {
1436         return $e->die_event;
1437     }
1438 }
1439
1440
1441
1442
1443
1444 # ----------------------------------------------------------------------
1445 __PACKAGE__->register_method(
1446     method => 'magic_fetch',
1447     api_name => 'open-ils.agent.fetch'
1448 );
1449
1450 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1451
1452 sub magic_fetch {
1453     my( $self, $conn, $auth, $args ) = @_;
1454     my $e = new_editor( authtoken => $auth );
1455     return $e->event unless $e->checkauth;
1456
1457     my $hint = $$args{hint};
1458     my $id  = $$args{id};
1459
1460     # Is the call allowed to fetch this type of object?
1461     return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1462
1463     # Find the class the implements the given hint
1464     my ($class) = grep { 
1465         $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1466
1467     $class =~ s/Fieldmapper:://og;
1468     $class =~ s/::/_/og;
1469     my $method = "retrieve_$class";
1470
1471     my $obj = $e->$method($id) or return $e->event;
1472     return $obj;
1473 }
1474 # ----------------------------------------------------------------------
1475
1476
1477 __PACKAGE__->register_method(
1478     method  => "fleshed_circ_retrieve",
1479     authoritative => 1,
1480     api_name    => "open-ils.circ.fleshed.retrieve",);
1481
1482 sub fleshed_circ_retrieve {
1483     my( $self, $client, $id ) = @_;
1484     my $e = new_editor();
1485     my $circ = $e->retrieve_action_circulation(
1486         [
1487             $id,
1488             { 
1489                 flesh               => 4,
1490                 flesh_fields    => { 
1491                     circ => [ qw/ target_copy / ],
1492                     acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1493                     ascecm => [ qw/ stat_cat stat_cat_entry / ],
1494                     acn => [ qw/ record / ],
1495                 }
1496             }
1497         ]
1498     ) or return $e->event;
1499     
1500     my $copy = $circ->target_copy;
1501     my $vol = $copy->call_number;
1502     my $rec = $circ->target_copy->call_number->record;
1503
1504     $vol->record($rec->id);
1505     $copy->call_number($vol->id);
1506     $circ->target_copy($copy->id);
1507
1508     my $mvr;
1509
1510     if( $rec->id == OILS_PRECAT_RECORD ) {
1511         $rec = undef;
1512         $vol = undef;
1513     } else { 
1514         $mvr = $U->record_to_mvr($rec);
1515         $rec->marc(''); # drop the bulky marc data
1516     }
1517
1518     return {
1519         circ => $circ,
1520         copy => $copy,
1521         volume => $vol,
1522         record => $rec,
1523         mvr => $mvr,
1524     };
1525 }
1526
1527
1528
1529 __PACKAGE__->register_method(
1530     method  => "test_batch_circ_events",
1531     api_name    => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1532 );
1533
1534 #  method for testing the behavior of a given event definition
1535 sub test_batch_circ_events {
1536     my($self, $conn, $auth, $event_def, $barcode) = @_;
1537
1538     my $e = new_editor(authtoken => $auth);
1539     return $e->event unless $e->checkauth;
1540     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1541
1542     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1543         or return $e->event;
1544
1545     my $circ = $e->search_action_circulation(
1546         {target_copy => $copy->id, checkin_time => undef})->[0]
1547         or return $e->event;
1548         
1549     return undef unless $circ;
1550
1551     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1552 }
1553
1554
1555 __PACKAGE__->register_method(
1556     method  => "fire_circ_events", 
1557     api_name    => "open-ils.circ.fire_circ_trigger_events",
1558     signature => q/
1559         General event def runner for circ objects.  If no event def ID
1560         is provided, the hook will be used to find the best event_def
1561         match based on the context org unit
1562     /
1563 );
1564
1565 __PACKAGE__->register_method(
1566     method  => "fire_circ_events", 
1567     api_name    => "open-ils.circ.fire_hold_trigger_events",
1568     signature => q/
1569         General event def runner for hold objects.  If no event def ID
1570         is provided, the hook will be used to find the best event_def
1571         match based on the context org unit
1572     /
1573 );
1574
1575 __PACKAGE__->register_method(
1576     method  => "fire_circ_events", 
1577     api_name    => "open-ils.circ.fire_user_trigger_events",
1578     signature => q/
1579         General event def runner for user objects.  If no event def ID
1580         is provided, the hook will be used to find the best event_def
1581         match based on the context org unit
1582     /
1583 );
1584
1585
1586 sub fire_circ_events {
1587     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1588
1589     my $e = new_editor(authtoken => $auth, xact => 1);
1590     return $e->event unless $e->checkauth;
1591
1592     my $targets;
1593
1594     if($self->api_name =~ /hold/) {
1595         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1596         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1597     } elsif($self->api_name =~ /user/) {
1598         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1599         $targets = $e->batch_retrieve_actor_user($target_ids);
1600     } else {
1601         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1602         $targets = $e->batch_retrieve_action_circulation($target_ids);
1603     }
1604     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1605                   # simply making this method authoritative because of weirdness
1606                   # with transaction handling in A/T code that causes rollback
1607                   # failure down the line if handling many targets
1608
1609     return undef unless @$targets;
1610     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1611 }
1612
1613 __PACKAGE__->register_method(
1614     method  => "user_payments_list",
1615     api_name    => "open-ils.circ.user_payments.filtered.batch",
1616     stream => 1,
1617     signature => {
1618         desc => q/Returns a fleshed, date-limited set of all payments a user
1619                 has made.  By default, ordered by payment date.  Optionally
1620                 ordered by other columns in the top-level "mp" object/,
1621         params => [
1622             {desc => 'Authentication token', type => 'string'},
1623             {desc => 'User ID', type => 'number'},
1624             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1625         ],
1626         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1627             and the related fully-realized payment object (e.g money.cash_payment)/}
1628     }
1629 );
1630
1631 sub user_payments_list {
1632     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1633
1634     my $e = new_editor(authtoken => $auth);
1635     return $e->event unless $e->checkauth;
1636
1637     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1638     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1639
1640     $order_by ||= ['payment_ts'];
1641
1642     # all payments by user, between start_date and end_date
1643     my $payments = $e->json_query({
1644         select => {mp => ['id']}, 
1645         from => {
1646             mp => {
1647                 mbt => {
1648                     fkey => 'xact', field => 'id'}
1649             }
1650         }, 
1651         where => {
1652             '+mbt' => {usr => $user_id}, 
1653             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1654         },
1655         order_by => {mp => $order_by}
1656     });
1657
1658     for my $payment_id (@$payments) {
1659         my $payment = $e->retrieve_money_payment([
1660             $payment_id->{id}, 
1661             {   
1662                 flesh => 2,
1663                 flesh_fields => {
1664                     mp => [
1665                         'xact',
1666                         'cash_payment',
1667                         'credit_card_payment',
1668                         'credit_payment',
1669                         'check_payment',
1670                         'work_payment',
1671                         'forgive_payment',
1672                         'goods_payment'
1673                     ],
1674                     mbt => [
1675                         'circulation', 
1676                         'grocery',
1677                         'reservation'
1678                     ]
1679                 }
1680             }
1681         ]);
1682         $conn->respond($payment);
1683     }
1684
1685     return undef;
1686 }
1687
1688
1689 __PACKAGE__->register_method(
1690     method  => "retrieve_circ_chain",
1691     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ",
1692     stream => 1,
1693     signature => {
1694         desc => q/Given a circulation, this returns all circulation objects
1695                 that are part of the same chain of renewals./,
1696         params => [
1697             {desc => 'Authentication token', type => 'string'},
1698             {desc => 'Circ ID', type => 'number'},
1699         ],
1700         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1701     }
1702 );
1703
1704 __PACKAGE__->register_method(
1705     method  => "retrieve_circ_chain",
1706     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1707     signature => {
1708         desc => q/Given a circulation, this returns a summary of the circulation objects
1709                 that are part of the same chain of renewals./,
1710         params => [
1711             {desc => 'Authentication token', type => 'string'},
1712             {desc => 'Circ ID', type => 'number'},
1713         ],
1714         return => {desc => q/Circulation Chain Summary/}
1715     }
1716 );
1717
1718 sub retrieve_circ_chain {
1719     my($self, $conn, $auth, $circ_id) = @_;
1720
1721     my $e = new_editor(authtoken => $auth);
1722     return $e->event unless $e->checkauth;
1723     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1724
1725     if($self->api_name =~ /summary/) {
1726         return $U->create_circ_chain_summary($e, $circ_id);
1727
1728     } else {
1729
1730         my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
1731
1732         for my $circ_info (@$chain) {
1733             my $circ = Fieldmapper::action::circulation->new;
1734             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1735             $conn->respond($circ);
1736         }
1737     }
1738
1739     return undef;
1740 }
1741
1742 __PACKAGE__->register_method(
1743     method  => "retrieve_prev_circ_chain",
1744     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1745     stream => 1,
1746     signature => {
1747         desc => q/Given a circulation, this returns all circulation objects
1748                 that are part of the previous chain of renewals./,
1749         params => [
1750             {desc => 'Authentication token', type => 'string'},
1751             {desc => 'Circ ID', type => 'number'},
1752         ],
1753         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1754     }
1755 );
1756
1757 __PACKAGE__->register_method(
1758     method  => "retrieve_prev_circ_chain",
1759     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1760     signature => {
1761         desc => q/Given a circulation, this returns a summary of the circulation objects
1762                 that are part of the previous chain of renewals./,
1763         params => [
1764             {desc => 'Authentication token', type => 'string'},
1765             {desc => 'Circ ID', type => 'number'},
1766         ],
1767         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1768     }
1769 );
1770
1771 sub retrieve_prev_circ_chain {
1772     my($self, $conn, $auth, $circ_id) = @_;
1773
1774     my $e = new_editor(authtoken => $auth);
1775     return $e->event unless $e->checkauth;
1776     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1777
1778     if($self->api_name =~ /summary/) {
1779         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1780         my $target_copy = $$first_circ{'target_copy'};
1781         my $usr = $$first_circ{'usr'};
1782         my $last_circ_from_prev_chain = $e->json_query({
1783             'select' => { 'circ' => ['id','usr'] },
1784             'from' => 'circ', 
1785             'where' => {
1786                 target_copy => $target_copy,
1787                 xact_start => { '<' => $$first_circ{'xact_start'} }
1788             },
1789             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1790             'limit' => 1
1791         })->[0];
1792         return undef unless $last_circ_from_prev_chain;
1793         return undef unless $$last_circ_from_prev_chain{'id'};
1794         my $sum = $e->json_query({from => ['action.summarize_circ_chain', $$last_circ_from_prev_chain{'id'}]})->[0];
1795         return undef unless $sum;
1796         my $obj = Fieldmapper::action::circ_chain_summary->new;
1797         $obj->$_($sum->{$_}) for keys %$sum;
1798         return { 'summary' => $obj, 'usr' => $$last_circ_from_prev_chain{'usr'} };
1799
1800     } else {
1801
1802         my $first_circ = $e->json_query({from => ['action.circ_chain', $circ_id]})->[0];
1803         my $target_copy = $$first_circ{'target_copy'};
1804         my $last_circ_from_prev_chain = $e->json_query({
1805             'select' => { 'circ' => ['id'] },
1806             'from' => 'circ', 
1807             'where' => {
1808                 target_copy => $target_copy,
1809                 xact_start => { '<' => $$first_circ{'xact_start'} }
1810             },
1811             'order_by' => [{ 'class'=>'circ', 'field'=>'xact_start', 'direction'=>'desc' }],
1812             'limit' => 1
1813         })->[0];
1814         return undef unless $last_circ_from_prev_chain;
1815         return undef unless $$last_circ_from_prev_chain{'id'};
1816         my $chain = $e->json_query({from => ['action.circ_chain', $$last_circ_from_prev_chain{'id'}]});
1817
1818         for my $circ_info (@$chain) {
1819             my $circ = Fieldmapper::action::circulation->new;
1820             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1821             $conn->respond($circ);
1822         }
1823     }
1824
1825     return undef;
1826 }
1827
1828
1829 __PACKAGE__->register_method(
1830     method  => "get_copy_due_date",
1831     api_name    => "open-ils.circ.copy.due_date.retrieve",
1832     signature => {
1833         desc => q/
1834             Given a copy ID, returns the due date for the copy if it's 
1835             currently circulating.  Otherwise, returns null.  Note, this is a public 
1836             method requiring no authentication.  Only the due date is exposed.
1837             /,
1838         params => [
1839             {desc => 'Copy ID', type => 'number'}
1840         ],
1841         return => {desc => q/
1842             Due date (ISO date stamp) if the copy is circulating, null otherwise.
1843         /}
1844     }
1845 );
1846
1847 sub get_copy_due_date {
1848     my($self, $conn, $copy_id) = @_;
1849     my $e = new_editor();
1850
1851     my $circ = $e->json_query({
1852         select => {circ => ['due_date']},
1853         from => 'circ',
1854         where => {
1855             target_copy => $copy_id,
1856             checkin_time => undef,
1857             '-or' => [
1858                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
1859                 {stop_fines => undef}
1860             ],
1861         },
1862         limit => 1
1863     })->[0] or return undef;
1864
1865     return $circ->{due_date};
1866 }
1867
1868
1869
1870
1871
1872 # {"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}}
1873
1874
1875 1;