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