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