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