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