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