]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
LP#1160343: remove unused open-ils.circ settings
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ.pm
1 package OpenILS::Application::Circ;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
5
6 use OpenILS::Application::Circ::Circulate;
7 use OpenILS::Application::Circ::Survey;
8 use OpenILS::Application::Circ::StatCat;
9 use OpenILS::Application::Circ::Holds;
10 use OpenILS::Application::Circ::HoldNotify;
11 use OpenILS::Application::Circ::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;