]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Financials.pm
LP#1541559: org setting for OneClickdigital API base URI
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Acq / Financials.pm
1 package OpenILS::Application::Acq::Financials;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
4
5 use OpenSRF::Utils::Logger qw(:logger);
6 use OpenILS::Utils::Fieldmapper;
7 use OpenILS::Utils::CStoreEditor q/:funcs/;
8 use OpenILS::Const qw/:const/;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Event;
11 use OpenILS::Application::AppUtils;
12 use OpenILS::Application::Acq::Lineitem;
13 my $U = 'OpenILS::Application::AppUtils';
14
15 # ----------------------------------------------------------------------------
16 # Funding Sources
17 # ----------------------------------------------------------------------------
18
19 __PACKAGE__->register_method(
20     method => 'create_funding_source',
21     api_name    => 'open-ils.acq.funding_source.create',
22     signature => {
23         desc => 'Creates a new funding_source',
24         params => [
25             {desc => 'Authentication token', type => 'string'},
26             {desc => 'funding source object to create', type => 'object'}
27         ],
28         return => {desc => 'The ID of the new funding_source'}
29     }
30 );
31
32 sub create_funding_source {
33     my($self, $conn, $auth, $funding_source) = @_;
34     my $e = new_editor(xact=>1, authtoken=>$auth);
35     return $e->die_event unless $e->checkauth;
36     return $e->die_event unless $e->allowed('ADMIN_FUNDING_SOURCE', $funding_source->owner);
37     $e->create_acq_funding_source($funding_source) or return $e->die_event;
38     $e->commit;
39     return $funding_source->id;
40 }
41
42
43 __PACKAGE__->register_method(
44     method => 'delete_funding_source',
45     api_name    => 'open-ils.acq.funding_source.delete',
46     signature => {
47         desc => 'Deletes a funding_source',
48         params => [
49             {desc => 'Authentication token', type => 'string'},
50             {desc => 'funding source ID', type => 'number'}
51         ],
52         return => {desc => '1 on success, Event on failure'}
53     }
54 );
55
56 sub delete_funding_source {
57     my($self, $conn, $auth, $funding_source_id) = @_;
58     my $e = new_editor(xact=>1, authtoken=>$auth);
59     return $e->die_event unless $e->checkauth;
60     my $funding_source = $e->retrieve_acq_funding_source($funding_source_id) or return $e->die_event;
61     return $e->die_event unless $e->allowed('ADMIN_FUNDING_SOURCE', $funding_source->owner, $funding_source);
62     $e->delete_acq_funding_source($funding_source) or return $e->die_event;
63     $e->commit;
64     return 1;
65 }
66
67 __PACKAGE__->register_method(
68     method => 'retrieve_funding_source',
69     api_name    => 'open-ils.acq.funding_source.retrieve',
70     authoritative => 1,
71     signature => {
72         desc => 'Retrieves a new funding_source',
73         params => [
74             {desc => 'Authentication token', type => 'string'},
75             {desc => 'funding source ID', type => 'number'}
76         ],
77         return => {desc => 'The funding_source object on success, Event on failure'}
78     }
79 );
80
81 sub retrieve_funding_source {
82     my($self, $conn, $auth, $funding_source_id, $options) = @_;
83     my $e = new_editor(authtoken=>$auth);
84     return $e->event unless $e->checkauth;
85     $options ||= {};
86
87     my $flesh = {flesh => 1, flesh_fields => {acqfs => []}};
88     push(@{$flesh->{flesh_fields}->{acqfs}}, 'credits') if $$options{flesh_credits};
89     push(@{$flesh->{flesh_fields}->{acqfs}}, 'allocations') if $$options{flesh_allocations};
90
91     my $funding_source = $e->retrieve_acq_funding_source([$funding_source_id, $flesh]) or return $e->event;
92
93     return $e->event unless $e->allowed(
94         ['ADMIN_FUNDING_SOURCE','MANAGE_FUNDING_SOURCE', 'VIEW_FUNDING_SOURCE'], 
95         $funding_source->owner, $funding_source); 
96
97     $funding_source->summary(retrieve_funding_source_summary_impl($e, $funding_source))
98         if $$options{flesh_summary};
99     return $funding_source;
100 }
101
102 __PACKAGE__->register_method(
103     method => 'retrieve_org_funding_sources',
104     api_name    => 'open-ils.acq.funding_source.org.retrieve',
105     stream => 1,
106     signature => {
107         desc => 'Retrieves all the funding_sources associated with an org unit that the requestor has access to see',
108         params => [
109             {desc => 'Authentication token', type => 'string'},
110             {desc => 'List of org Unit IDs.  If no IDs are provided, this method returns the 
111                 full set of funding sources this user has permission to view', type => 'number'},
112             {desc => q/Limiting permission.  this permission is used find the work-org tree from which  
113                 the list of orgs is generated if no org ids are provided.  
114                 The default is ADMIN_FUNDING_SOURCE/, type => 'string'},
115         ],
116         return => {desc => 'The funding_source objects on success, empty array otherwise'}
117     }
118 );
119
120 sub retrieve_org_funding_sources {
121     my($self, $conn, $auth, $org_id_list, $options) = @_;
122     my $e = new_editor(authtoken=>$auth);
123     return $e->event unless $e->checkauth;
124     $options ||= {};
125
126     my $limit_perm = ($$options{limit_perm}) ? $$options{limit_perm} : 'ADMIN_FUNDING_SOURCE';
127     return OpenILS::Event->new('BAD_PARAMS') 
128         unless $limit_perm =~ /(ADMIN|MANAGE|VIEW)_FUNDING_SOURCE/;
129
130     my $org_ids = ($org_id_list and @$org_id_list) ? $org_id_list :
131         $U->user_has_work_perm_at($e, $limit_perm, {descendants =>1});
132
133     return [] unless @$org_ids;
134     my $sources = $e->search_acq_funding_source({owner => $org_ids});
135
136     for my $source (@$sources) {
137         $source->summary(retrieve_funding_source_summary_impl($e, $source))
138             if $$options{flesh_summary};
139         $conn->respond($source);
140     }
141
142     return undef;
143 }
144
145 sub retrieve_funding_source_summary_impl {
146     my($e, $source) = @_;
147     my $at = $e->search_acq_funding_source_allocation_total({funding_source => $source->id})->[0];
148     my $b = $e->search_acq_funding_source_balance({funding_source => $source->id})->[0];
149     my $ct = $e->search_acq_funding_source_credit_total({funding_source => $source->id})->[0];
150     return {
151         allocation_total => ($at) ? $at->amount : 0,
152         balance => ($b) ? $b->amount : 0,
153         credit_total => ($ct) ? $ct->amount : 0,
154     };
155 }
156
157
158 __PACKAGE__->register_method(
159     method => 'create_funding_source_credit',
160     api_name    => 'open-ils.acq.funding_source_credit.create',
161     signature => {
162         desc => 'Create a new funding source credit',
163         params => [
164             {desc => 'Authentication token', type => 'string'},
165             {desc => 'funding source credit object', type => 'object'}
166         ],
167         return => {desc => 'The ID of the new funding source credit on success, Event on failure'}
168     }
169 );
170
171 sub create_funding_source_credit {
172     my($self, $conn, $auth, $fs_credit) = @_;
173     my $e = new_editor(authtoken=>$auth, xact=>1);
174     return $e->event unless $e->checkauth;
175
176     my $fs = $e->retrieve_acq_funding_source($fs_credit->funding_source)
177         or return $e->die_event;
178     return $e->die_event unless $e->allowed(['MANAGE_FUNDING_SOURCE'], $fs->owner, $fs); 
179
180     $e->create_acq_funding_source_credit($fs_credit) or return $e->die_event;
181     $e->commit;
182     return $fs_credit->id;
183 }
184
185
186 # ---------------------------------------------------------------
187 # funds
188 # ---------------------------------------------------------------
189
190 __PACKAGE__->register_method(
191     method => 'create_fund',
192     api_name    => 'open-ils.acq.fund.create',
193     signature => {
194         desc => 'Creates a new fund',
195         params => [
196             {desc => 'Authentication token', type => 'string'},
197             {desc => 'fund object to create', type => 'object'}
198         ],
199         return => {desc => 'The ID of the newly created fund object'}
200     }
201 );
202
203 sub create_fund {
204     my($self, $conn, $auth, $fund) = @_;
205     my $e = new_editor(xact=>1, authtoken=>$auth);
206     return $e->die_event unless $e->checkauth;
207     return $e->die_event unless $e->allowed('ADMIN_FUND', $fund->org);
208     $e->create_acq_fund($fund) or return $e->die_event;
209     $e->commit;
210     return $fund->id;
211 }
212
213
214 __PACKAGE__->register_method(
215     method => 'delete_fund',
216     api_name    => 'open-ils.acq.fund.delete',
217     signature => {
218         desc => 'Deletes a fund',
219         params => [
220             {desc => 'Authentication token', type => 'string'},
221             {desc => 'fund ID', type => 'number'}
222         ],
223         return => {desc => '1 on success, Event on failure'}
224     }
225 );
226
227 sub delete_fund {
228     my($self, $conn, $auth, $fund_id) = @_;
229     my $e = new_editor(xact=>1, authtoken=>$auth);
230     return $e->die_event unless $e->checkauth;
231     my $fund = $e->retrieve_acq_fund($fund_id) or return $e->die_event;
232     return $e->die_event unless $e->allowed('ADMIN_FUND', $fund->org, $fund);
233     $e->delete_acq_fund($fund) or return $e->die_event;
234     $e->commit;
235     return 1;
236 }
237
238 __PACKAGE__->register_method(
239     method => 'retrieve_fund',
240     api_name    => 'open-ils.acq.fund.retrieve',
241     authoritative => 1,
242     signature => {
243         desc => 'Retrieves a new fund',
244         params => [
245             {desc => 'Authentication token', type => 'string'},
246             {desc => 'fund ID', type => 'number'}
247         ],
248         return => {desc => 'The fund object on success, Event on failure'}
249     }
250 );
251
252 sub retrieve_fund {
253     my($self, $conn, $auth, $fund_id, $options) = @_;
254     my $e = new_editor(authtoken=>$auth);
255     return $e->event unless $e->checkauth;
256     $options ||= {};
257
258     my $flesh = {flesh => 2, flesh_fields => {acqf => []}};
259     if ($options->{"flesh_tags"}) {
260         push @{$flesh->{"flesh_fields"}->{"acqf"}}, "tags";
261         $flesh->{"flesh_fields"}->{"acqftm"} = ["tag"];
262     }
263     push(@{$flesh->{flesh_fields}->{acqf}}, 'debits') if $$options{flesh_debits};
264     push(@{$flesh->{flesh_fields}->{acqf}}, 'allocations') if $$options{flesh_allocations};
265     push(@{$flesh->{flesh_fields}->{acqfa}}, 'funding_source') if $$options{flesh_allocation_sources};
266
267     my $fund = $e->retrieve_acq_fund([$fund_id, $flesh]) or return $e->event;
268     return $e->event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND', 'VIEW_FUND'], $fund->org, $fund);
269     $fund->summary(retrieve_fund_summary_impl($e, $fund))
270         if $$options{flesh_summary};
271     return $fund;
272 }
273
274 __PACKAGE__->register_method(
275     method => 'retrieve_org_funds',
276     api_name    => 'open-ils.acq.fund.org.retrieve',
277     stream => 1,
278     signature => {
279         desc => 'Retrieves all the funds associated with an org unit',
280         params => [
281             {desc => 'Authentication token', type => 'string'},
282             {desc => 'List of org Unit IDs.  If no IDs are provided, this method returns the 
283                 full set of funding sources this user has permission to view', type => 'number'},
284             {desc => q/Options hash.  
285                 "limit_perm" -- this permission is used find the work-org tree from which  
286                 the list of orgs is generated if no org ids are provided.  The default is ADMIN_FUND.
287                 "flesh_summary" -- if true, the summary field on each fund is fleshed
288                 The default is ADMIN_FUND/, type => 'string'},
289         ],
290         return => {desc => 'The fund objects on success, Event on failure'}
291     }
292 );
293
294 __PACKAGE__->register_method(
295     method => 'retrieve_org_funds',
296     api_name    => 'open-ils.acq.fund.org.years.retrieve');
297
298
299 sub retrieve_org_funds {
300     my($self, $conn, $auth, $filter, $options) = @_;
301     my $e = new_editor(authtoken=>$auth);
302     return $e->event unless $e->checkauth;
303     $filter ||= {};
304     $options ||= {};
305
306     my $limit_perm = ($$options{limit_perm}) ? $$options{limit_perm} : 'ADMIN_FUND';
307     return OpenILS::Event->new('BAD_PARAMS') 
308         unless $limit_perm =~ /(ADMIN|MANAGE|VIEW)_(ACQ_)?FUND/;
309
310     $filter->{org}  = $filter->{org} || 
311         $U->user_has_work_perm_at($e, $limit_perm, {descendants =>1});
312     return undef unless @{$filter->{org}};
313
314     my $query = [
315         $filter,
316         {
317             limit => $$options{limit} || 50,
318             offset => $$options{offset} || 0,
319             order_by => $$options{order_by} || {acqf => 'name'}
320         }
321     ];
322
323     if($self->api_name =~ /years/) {
324         # return the distinct set of fund years covered by the selected funds
325         my $data = $e->json_query({
326             select => {
327                 acqf => [{column => 'year', transform => 'distinct'}]
328             }, 
329             from => 'acqf', 
330             where => $filter}
331         );
332
333         return [map { $_->{year} } @$data];
334     }
335
336     my $funds = $e->search_acq_fund($query);
337
338     for my $fund (@$funds) {
339         $fund->summary(retrieve_fund_summary_impl($e, $fund))
340             if $$options{flesh_summary};
341         $conn->respond($fund);
342     }
343
344     return undef;
345 }
346
347 __PACKAGE__->register_method(
348     method => 'retrieve_fund_summary',
349     api_name    => 'open-ils.acq.fund.summary.retrieve',
350     authoritative => 1,
351     signature => {
352         desc => 'Returns a summary of credits/debits/encumbrances for a fund',
353         params => [
354             {desc => 'Authentication token', type => 'string'},
355             {desc => 'fund id', type => 'number' }
356         ],
357         return => {desc => 'A hash of summary information, Event on failure'}
358     }
359 );
360
361 sub retrieve_fund_summary {
362     my($self, $conn, $auth, $fund_id) = @_;
363     my $e = new_editor(authtoken=>$auth);
364     return $e->event unless $e->checkauth;
365     my $fund = $e->retrieve_acq_fund($fund_id) or return $e->event;
366     return $e->event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
367     return retrieve_fund_summary_impl($e, $fund);
368 }
369
370
371 sub retrieve_fund_summary_impl {
372     my($e, $fund) = @_;
373
374     my $at = $e->search_acq_fund_allocation_total({fund => $fund->id})->[0];
375     my $dt = $e->search_acq_fund_debit_total({fund => $fund->id})->[0];
376     my $et = $e->search_acq_fund_encumbrance_total({fund => $fund->id})->[0];
377     my $st = $e->search_acq_fund_spent_total({fund => $fund->id})->[0];
378     my $cb = $e->search_acq_fund_combined_balance({fund => $fund->id})->[0];
379     my $sb = $e->search_acq_fund_spent_balance({fund => $fund->id})->[0];
380
381     return {
382         allocation_total => ($at) ? $at->amount : 0,
383         debit_total => ($dt) ? $dt->amount : 0,
384         encumbrance_total => ($et) ? $et->amount : 0,
385         spent_total => ($st) ? $st->amount : 0,
386         combined_balance => ($cb) ? $cb->amount : 0,
387         spent_balance => ($sb) ? $sb->amount : 0,
388     };
389 }
390
391 __PACKAGE__->register_method(
392     method => 'transfer_money_between_funds',
393     api_name    => 'open-ils.acq.funds.transfer_money',
394     signature => {
395         desc => 'Method for transfering money between funds',
396         params => [
397             {desc => 'Authentication token', type => 'string'},
398             {desc => 'Originating fund ID', type => 'number'},
399             {desc => 'Amount of money to transfer away from the originating fund, in the same currency as said fund', type => 'number'},
400             {desc => 'Destination fund ID', type => 'number'},
401             {desc => 'Amount of money to transfer to the destination fund, in the same currency as said fund.  If null, uses the same amount specified with the Originating Fund, and attempts a currency conversion if appropriate.', type => 'number'},
402             {desc => 'Transfer Note', type => 'string'}
403         ],
404         return => {desc => '1 on success, Event on failure'}
405     }
406 );
407
408 sub transfer_money_between_funds {
409     my($self, $conn, $auth, $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $note) = @_;
410     my $e = new_editor(xact=>1, authtoken=>$auth);
411     return $e->die_event unless $e->checkauth;
412     my $ofund = $e->retrieve_acq_fund($ofund_id) or return $e->event;
413     return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $ofund->org, $ofund);
414     my $dfund = $e->retrieve_acq_fund($dfund_id) or return $e->event;
415     return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $dfund->org, $dfund);
416
417     if (!defined $dfund_amount) {
418
419         if ($ofund->currency_type ne $dfund->currency_type) {
420
421             $dfund_amount = $e->json_query({
422                 from => [
423                     'acq.exchange_ratio',
424                     $ofund->currency_type,
425                     $dfund->currency_type,
426                     $ofund_amount
427                 ]
428             })->[0]->{'acq.exchange_ratio'};
429
430         } else {
431
432             $dfund_amount = $ofund_amount;
433         }
434
435     } else {
436         return $e->die_event unless $e->allowed("ACQ_XFER_MANUAL_DFUND_AMOUNT");
437     }
438
439     $e->json_query({
440         from => [
441             'acq.transfer_fund',
442             $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $e->requestor->id, $note
443         ]
444     });
445
446     $e->commit;
447
448     return 1;
449 }
450
451
452
453 # ---------------------------------------------------------------
454 # fund Allocations
455 # ---------------------------------------------------------------
456
457 __PACKAGE__->register_method(
458     method => 'create_fund_alloc',
459     api_name    => 'open-ils.acq.fund_allocation.create',
460     signature => {
461         desc => 'Creates a new fund_allocation',
462         params => [
463             {desc => 'Authentication token', type => 'string'},
464             {desc => 'fund allocation object to create', type => 'object'}
465         ],
466         return => {desc => 'The ID of the new fund_allocation'}
467     }
468 );
469
470 sub create_fund_alloc {
471     my($self, $conn, $auth, $fund_alloc) = @_;
472     my $e = new_editor(xact=>1, authtoken=>$auth);
473     return $e->die_event unless $e->checkauth;
474
475     # this action is equivalent to both debiting a funding source and crediting a fund
476
477     my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
478         or return $e->die_event;
479     return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner);
480
481     my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
482     return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
483
484     $fund_alloc->allocator($e->requestor->id);
485     $e->create_acq_fund_allocation($fund_alloc) or return $e->die_event;
486     $e->commit;
487     return $fund_alloc->id;
488 }
489
490
491 __PACKAGE__->register_method(
492     method => 'delete_fund_alloc',
493     api_name    => 'open-ils.acq.fund_allocation.delete',
494     signature => {
495         desc => 'Deletes a fund_allocation',
496         params => [
497             {desc => 'Authentication token', type => 'string'},
498             {desc => 'fund Alocation ID', type => 'number'}
499         ],
500         return => {desc => '1 on success, Event on failure'}
501     }
502 );
503
504 sub delete_fund_alloc {
505     my($self, $conn, $auth, $fund_alloc_id) = @_;
506     my $e = new_editor(xact=>1, authtoken=>$auth);
507     return $e->die_event unless $e->checkauth;
508
509     my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->die_event;
510
511     my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
512         or return $e->die_event;
513     return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
514
515     my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
516     return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
517
518     $e->delete_acq_fund_allocation($fund_alloc) or return $e->die_event;
519     $e->commit;
520     return 1;
521 }
522
523 __PACKAGE__->register_method(
524     method => 'retrieve_fund_alloc',
525     api_name    => 'open-ils.acq.fund_allocation.retrieve',
526     authoritative => 1,
527     signature => {
528         desc => 'Retrieves a new fund_allocation',
529         params => [
530             {desc => 'Authentication token', type => 'string'},
531             {desc => 'fund Allocation ID', type => 'number'}
532         ],
533         return => {desc => 'The fund allocation object on success, Event on failure'}
534     }
535 );
536
537 sub retrieve_fund_alloc {
538     my($self, $conn, $auth, $fund_alloc_id) = @_;
539     my $e = new_editor(authtoken=>$auth);
540     return $e->event unless $e->checkauth;
541     my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
542
543     my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
544         or return $e->die_event;
545     return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
546
547     my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
548     return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
549
550     return $fund_alloc;
551 }
552
553
554 __PACKAGE__->register_method(
555     method => 'retrieve_funding_source_allocations',
556     api_name    => 'open-ils.acq.funding_source.allocations.retrieve',
557     authoritative => 1,
558     signature => {
559         desc => 'Retrieves a new fund_allocation',
560         params => [
561             {desc => 'Authentication token', type => 'string'},
562             {desc => 'fund Allocation ID', type => 'number'}
563         ],
564         return => {desc => 'The fund allocation object on success, Event on failure'}
565     }
566 );
567
568 sub retrieve_funding_source_allocations {
569     my($self, $conn, $auth, $fund_alloc_id) = @_;
570     my $e = new_editor(authtoken=>$auth);
571     return $e->event unless $e->checkauth;
572     my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
573
574     my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
575         or return $e->die_event;
576     return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
577
578     my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
579     return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
580
581     return $fund_alloc;
582 }
583
584 # ----------------------------------------------------------------------------
585 # Currency
586 # ----------------------------------------------------------------------------
587
588 __PACKAGE__->register_method(
589     method => 'retrieve_all_currency_type',
590     api_name    => 'open-ils.acq.currency_type.all.retrieve',
591     stream => 1,
592     signature => {
593         desc => 'Retrieves all currency_type objects',
594         params => [
595             {desc => 'Authentication token', type => 'string'},
596         ],
597         return => {desc => 'List of currency_type objects', type => 'list'}
598     }
599 );
600
601 sub retrieve_all_currency_type {
602     my($self, $conn, $auth, $fund_alloc_id) = @_;
603     my $e = new_editor(authtoken=>$auth);
604     return $e->event unless $e->checkauth;
605     return $e->event unless $e->allowed('GENERAL_ACQ');
606     $conn->respond($_) for @{$e->retrieve_all_acq_currency_type()};
607 }
608
609 __PACKAGE__->register_method(
610     method => 'create_lineitem_assets',
611     api_name    => 'open-ils.acq.lineitem.assets.create',
612     signature => {
613         desc => q/Creates the bibliographic data, volume, and copies associated with a lineitem./,
614         params => [
615             {desc => 'Authentication token', type => 'string'},
616             {desc => 'The lineitem id', type => 'number'},
617             {desc => q/Options hash./}
618         ],
619         return => {desc => 'ID of newly created bib record, Event on error'}
620     }
621 );
622
623 sub create_lineitem_assets {
624     my($self, $conn, $auth, $li_id, $options) = @_;
625     my $e = new_editor(authtoken=>$auth, xact=>1);
626     return $e->die_event unless $e->checkauth;
627     my ($count, $resp) = create_lineitem_assets_impl($e, $li_id, $options);
628     return $resp if $resp;
629     $e->commit;
630     return $count;
631 }
632
633 sub create_lineitem_assets_impl {
634     my($e, $li_id, $options) = @_;
635     $options ||= {};
636     my $evt;
637
638     my $li = $e->retrieve_acq_lineitem([
639         $li_id,
640         {   flesh => 1,
641             flesh_fields => {jub => ['purchase_order', 'attributes']}
642         }
643     ]) or return (undef, $e->die_event);
644
645     # -----------------------------------------------------------------
646     # first, create the bib record if necessary
647     # -----------------------------------------------------------------
648     unless($li->eg_bib_id) {
649
650        my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
651             $e, $li->marc); #$rec->bib_source
652
653         if($U->event_code($record)) {
654             $e->rollback;
655             return (undef, $record);
656         }
657
658         $li->editor($e->requestor->id);
659         $li->edit_time('now');
660         $li->eg_bib_id($record->id);
661         $e->update_acq_lineitem($li) or return (undef, $e->die_event);
662     }
663
664     my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
665
666     # -----------------------------------------------------------------
667     # for each lineitem_detail, create the volume if necessary, create 
668     # a copy, and link them all together.
669     # -----------------------------------------------------------------
670     my %volcache;
671     for my $li_detail_id (@{$li_details}) {
672
673         my $li_detail = $e->retrieve_acq_lineitem_detail($li_detail_id)
674             or return (undef, $e->die_event);
675
676         # Create the volume object if necessary
677         my $volume = $volcache{$li_detail->cn_label};
678         unless($volume and $volume->owning_lib == $li_detail->owning_lib) {
679             ($volume, $evt) =
680                 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
681                     $e, $li_detail->cn_label, $li->eg_bib_id, $li_detail->owning_lib);
682             return (undef, $evt) if $evt;
683             $volcache{$volume->id} = $volume;
684         }
685
686         my $copy = Fieldmapper::asset::copy->new;
687         $copy->isnew(1);
688         $copy->loan_duration(2);
689         $copy->fine_level(2);
690         $copy->status(OILS_COPY_STATUS_ON_ORDER);
691         $copy->barcode($li_detail->barcode);
692         $copy->location($li_detail->location);
693         $copy->call_number($volume->id);
694         $copy->circ_lib($volume->owning_lib);
695         $copy->circ_modifier($$options{circ_modifier} || 'book');
696
697         $evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $volume, $copy);
698         return (undef, $evt) if $evt;
699  
700         $li_detail->eg_copy_id($copy->id);
701         $e->update_acq_lineitem_detail($li_detail) or return (undef, $e->die_event);
702     }
703
704     return (scalar @{$li_details});
705 }
706
707
708
709
710 sub create_purchase_order_impl {
711     my($e, $p_order) = @_;
712
713     $p_order->creator($e->requestor->id);
714     $p_order->editor($e->requestor->id);
715     $p_order->owner($e->requestor->id);
716     $p_order->edit_time('now');
717
718     return $e->die_event unless 
719         $e->allowed('CREATE_PURCHASE_ORDER', $p_order->ordering_agency);
720
721     my $provider = $e->retrieve_acq_provider($p_order->provider)
722         or return $e->die_event;
723     return $e->die_event unless 
724         $e->allowed('MANAGE_PROVIDER', $provider->owner, $provider);
725
726     $e->create_acq_purchase_order($p_order) or return $e->die_event;
727     return undef;
728 }
729
730
731 __PACKAGE__->register_method(
732     method => 'retrieve_all_user_purchase_order',
733     api_name    => 'open-ils.acq.purchase_order.user.all.retrieve',
734     stream => 1,
735     signature => {
736         desc => 'Retrieves a purchase order',
737         params => [
738             {desc => 'Authentication token', type => 'string'},
739             {desc => 'purchase_order to retrieve', type => 'number'},
740             {desc => q/Options hash.  flesh_lineitems: to get the lineitems and lineitem_attrs; 
741                 clear_marc: to clear the MARC data from the lineitem (for reduced bandwidth);
742                 limit: number of items to return ,defaults to 50;
743                 offset: offset in the list of items to return
744                 order_by: sort the result, provide one or more colunm names, separated by commas,
745                 optionally followed by ASC or DESC as a single string 
746                 li_limit : number of lineitems to return if fleshing line items;
747                 li_offset : lineitem offset if fleshing line items
748                 li_order_by : lineitem sort definition if fleshing line items
749                 flesh_lineitem_detail_count : flesh lineitem_detail_count field
750                 /,
751                 type => 'hash'}
752         ],
753         return => {desc => 'The purchase order, Event on failure'}
754     }
755 );
756
757 sub retrieve_all_user_purchase_order {
758     my($self, $conn, $auth, $options) = @_;
759     my $e = new_editor(authtoken=>$auth);
760     return $e->event unless $e->checkauth;
761     $options ||= {};
762
763     # grab purchase orders I have 
764     my $perm_orgs = $U->user_has_work_perm_at($e, 'MANAGE_PROVIDER', {descendants =>1});
765     return OpenILS::Event->new('PERM_FAILURE', ilsperm => 'MANAGE_PROVIDER')
766         unless @$perm_orgs;
767     my $provider_ids = $e->search_acq_provider({owner => $perm_orgs}, {idlist=>1});
768     my $po_ids = $e->search_acq_purchase_order({provider => $provider_ids}, {idlist=>1});
769
770     # grab my purchase orders
771     push(@$po_ids, @{$e->search_acq_purchase_order({owner => $e->requestor->id}, {idlist=>1})});
772
773     return undef unless @$po_ids;
774
775     # now get the db to limit/sort for us
776     $po_ids = $e->search_acq_purchase_order(
777         [   {id => $po_ids}, {
778                 limit => $$options{limit} || 50,
779                 offset => $$options{offset} || 0,
780                 order_by => {acqpo => $$options{order_by} || 'create_time'}
781             }
782         ],
783         {idlist => 1}
784     );
785
786     $conn->respond(retrieve_purchase_order_impl($e, $_, $options)) for @$po_ids;
787     return undef;
788 }
789
790
791 __PACKAGE__->register_method(
792     method => 'search_purchase_order',
793     api_name    => 'open-ils.acq.purchase_order.search',
794     stream => 1,
795     signature => {
796         desc => 'Search for a purchase order',
797         params => [
798             {desc => 'Authentication token', type => 'string'},
799             {desc => q/Search hash.  Search fields include id, provider/, type => 'hash'}
800         ],
801         return => {desc => 'A stream of POs'}
802     }
803 );
804
805 sub search_purchase_order {
806     my($self, $conn, $auth, $search, $options) = @_;
807     my $e = new_editor(authtoken=>$auth);
808     return $e->event unless $e->checkauth;
809     my $po_ids = $e->search_acq_purchase_order($search, {idlist=>1});
810     for my $po_id (@$po_ids) {
811         $conn->respond($e->retrieve_acq_purchase_order($po_id))
812             unless po_perm_failure($e, $po_id);
813     }
814
815     return undef;
816 }
817
818
819
820 __PACKAGE__->register_method(
821         method    => 'retrieve_purchase_order',
822         api_name  => 'open-ils.acq.purchase_order.retrieve',
823         stream    => 1,
824         authoritative => 1,
825         signature => {
826                       desc      => 'Retrieves a purchase order',
827                       params    => [
828                                     {desc => 'Authentication token', type => 'string'},
829                                     {desc => 'purchase_order to retrieve', type => 'number'},
830                                     {desc => q/Options hash.  flesh_lineitems, to get the lineitems and lineitem_attrs;
831                 clear_marc, to clear the MARC data from the lineitem (for reduced bandwidth)
832                 li_limit : number of lineitems to return if fleshing line items;
833                 li_offset : lineitem offset if fleshing line items
834                 li_order_by : lineitem sort definition if fleshing line items,
835                 flesh_po_items : po_item objects
836                 /,
837                                      type => 'hash'}
838                                    ],
839                       return => {desc => 'The purchase order, Event on failure'}
840                      }
841 );
842
843 sub retrieve_purchase_order {
844     my($self, $conn, $auth, $po_id, $options) = @_;
845     my $e = new_editor(authtoken=>$auth);
846     return $e->event unless $e->checkauth;
847
848     $po_id = [ $po_id ] unless ref $po_id;
849     for ( @{$po_id} ) {
850         my $rv;
851         if ( po_perm_failure($e, $_) )
852           { $rv = $e->event }
853         else
854           { $rv =  retrieve_purchase_order_impl($e, $_, $options) }
855
856         $conn->respond($rv);
857     }
858
859     return undef;
860 }
861
862
863 # if the user does not have permission to perform actions on this PO, return the perm failure event
864 sub po_perm_failure {
865     my($e, $po_id, $fund_id) = @_;
866     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
867     return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency, $po);
868     return undef;
869 }
870
871 sub build_price_summary {
872     my ($e, $po_id) = @_;
873
874     # amounts for lineitems / lineitem_details
875     my $li_data = $e->json_query({
876         select => {
877             jub => [
878                 'estimated_unit_price', 
879                 {column => 'id', alias => 'li_id'}
880             ],
881             acqlid => [
882                 # lineitem_detail.id is needed to ensure we have one 
883                 # "row" of data for every copy, regardless of whether
884                 # a fund_debit exists for each copy.
885                 {column => 'id', alias => 'lid_id'}
886             ],
887             acqfdeb => [
888                 'encumbrance', 
889                 {column => 'amount', alias => 'debit_amount'}
890             ]
891         }, 
892         from => {
893             jub => {
894                 acqlid => {
895                     fkey => 'id',
896                     field => 'lineitem',
897                     join => {
898                         acqfdeb => {
899                             type => 'left',
900                             fkey => 'fund_debit',
901                             field => 'id'
902                         }
903                     }
904                 }
905             }
906         },
907         where => {'+jub' => {purchase_order => $po_id}}
908     });
909
910     # amounts for po_item's
911     my $item_data = $e->json_query({
912         select => {
913             acqpoi => ['estimated_cost'],
914             acqfdeb => [
915                 'encumbrance', 
916                 {column => 'amount', alias => 'debit_amount'}
917             ]
918         },
919         from => {
920             acqpoi => {
921                 acqfdeb => {
922                     type => 'left',
923                     fkey => 'fund_debit',
924                     field => 'id'
925                 }
926             }
927         },
928         where => {'+acqpoi' => {purchase_order => $po_id}}
929     });
930
931     # debits for invoice items linked to "blanket" po_items are 
932     # considered part of the PO.  We are not duplicating debits
933     # here with po_item debits, because blanket po_item debits
934     # plus related invoice_item debits are cumulitive.
935     my $inv_data = $e->json_query({
936         select => {
937             acqii => [
938                 'amount_paid',
939                 {column => 'id', alias => 'item_id'}
940             ],
941             aiit => ['blanket'],
942             acqfdeb => [
943                 'encumbrance', 
944                 {column => 'amount', alias => 'debit_amount'}
945             ]
946         },
947         from => {
948             acqii => {
949                 acqfdeb => {
950                     type => 'left',
951                     fkey => 'fund_debit',
952                     field => 'id'
953                 }, 
954                 aiit => {}
955             }
956         },
957         where => {
958             '+acqii' => {purchase_order => $po_id},
959             '+aiit' => {blanket => 't'}
960         }
961     });
962                    
963     # sum amounts debited (for activated PO's) and amounts estimated 
964     # (for pending PO's) for all lineitem_details and po_items.
965
966     my ($enc, $spent, $estimated) = (0, 0, 0);
967
968     for my $deb (@$li_data, @$item_data, @$inv_data) {
969
970         if (defined $deb->{debit_amount}) { # could be $0
971             # we have a debit, treat it as authoritative.
972
973             # estimated amount includes all amounts encumbered or spent
974             $estimated += $deb->{debit_amount};
975
976             if($U->is_true($deb->{encumbrance})) {
977                 $enc += $deb->{debit_amount};
978             } else {
979                 $spent += $deb->{debit_amount};
980             }
981
982         } else {
983             # PO is not activated, so sum estimated costs.
984             # There will be one $deb object for every lineitem_detail 
985             # and po_item.  Adding the estimated costs for all gives 
986             # us the total esimated amount.
987
988             $estimated += (
989                 $deb->{estimated_unit_price} || 
990                 $deb->{estimated_cost} || 
991                 $deb->{amount_paid} || 0
992             );
993         }
994     }
995
996     return ($enc, $spent, $estimated);
997 }
998
999
1000 sub retrieve_purchase_order_impl {
1001     my($e, $po_id, $options) = @_;
1002
1003     my $flesh = {"flesh" => 1, "flesh_fields" => {"acqpo" => []}};
1004
1005     $options ||= {};
1006     unless ($options->{"no_flesh_cancel_reason"}) {
1007         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "cancel_reason";
1008     }
1009     if ($options->{"flesh_notes"}) {
1010         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
1011     }
1012     if ($options->{"flesh_provider"}) {
1013         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider";
1014     }
1015
1016     push (@{$flesh->{flesh_fields}->{acqpo}}, 'po_items') if $options->{flesh_po_items};
1017
1018     my $args = (@{$flesh->{"flesh_fields"}->{"acqpo"}}) ?
1019         [$po_id, $flesh] : $po_id;
1020
1021     my $po = $e->retrieve_acq_purchase_order($args)
1022         or return $e->event;
1023
1024     if($$options{flesh_lineitems}) {
1025
1026         my $flesh_fields = { jub => ['attributes'] };
1027         $flesh_fields->{jub}->[1] = 'lineitem_details' if $$options{flesh_lineitem_details};
1028         $flesh_fields->{acqlid} = ['fund_debit'] if $$options{flesh_fund_debit};
1029
1030         my $items = $e->search_acq_lineitem([
1031             {purchase_order => $po_id},
1032             {
1033                 flesh => 3,
1034                 flesh_fields => $flesh_fields,
1035                 limit => $$options{li_limit} || 50,
1036                 offset => $$options{li_offset} || 0,
1037                 order_by => {jub => $$options{li_order_by} || 'create_time'}
1038             }
1039         ]);
1040
1041         if($$options{clear_marc}) {
1042             $_->clear_marc for @$items;
1043         }
1044
1045         $po->lineitems($items);
1046         $po->lineitem_count(scalar(@$items));
1047
1048     } elsif( $$options{flesh_lineitem_ids} ) {
1049         $po->lineitems($e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1}));
1050
1051     } elsif( $$options{flesh_lineitem_count} ) {
1052
1053         my $items = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist=>1});
1054         $po->lineitem_count(scalar(@$items));
1055     }
1056
1057     if($$options{flesh_price_summary}) {
1058         my ($enc, $spent, $estimated) = build_price_summary($e, $po_id);
1059         $po->amount_encumbered($enc);
1060         $po->amount_spent($spent);
1061         $po->amount_estimated($estimated);
1062     }
1063
1064     return $po;
1065 }
1066
1067
1068 __PACKAGE__->register_method(
1069     method => 'format_po',
1070     api_name    => 'open-ils.acq.purchase_order.format'
1071 );
1072
1073 sub format_po {
1074     my($self, $conn, $auth, $po_id, $format) = @_;
1075     my $e = new_editor(authtoken=>$auth);
1076     return $e->event unless $e->checkauth;
1077
1078     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
1079     return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1080
1081     my $hook = "format.po.$format";
1082     return $U->fire_object_event(undef, $hook, $po, $po->ordering_agency);
1083 }
1084
1085 __PACKAGE__->register_method(
1086     method => 'format_lineitem',
1087     api_name    => 'open-ils.acq.lineitem.format'
1088 );
1089
1090 sub format_lineitem {
1091     my($self, $conn, $auth, $li_id, $format, $user_data) = @_;
1092     my $e = new_editor(authtoken=>$auth);
1093     return $e->event unless $e->checkauth;
1094
1095     my $li = $e->retrieve_acq_lineitem($li_id) or return $e->event;
1096
1097     my $context_org;
1098     if (defined $li->purchase_order) {
1099         my $po = $e->retrieve_acq_purchase_order($li->purchase_order) or return $e->die_event;
1100         return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1101         $context_org = $po->ordering_agency;
1102     } else {
1103         my $pl = $e->retrieve_acq_picklist($li->picklist) or return $e->die_event;
1104         if($e->requestor->id != $pl->owner) {
1105             return $e->event unless
1106                 $e->allowed('VIEW_PICKLIST', $pl->org_unit, $pl);
1107         }
1108         $context_org = $pl->org_unit;
1109     }
1110
1111     my $hook = "format.acqli.$format";
1112     return $U->fire_object_event(undef, $hook, $li, $context_org, 'print-on-demand', $user_data);
1113 }
1114
1115 __PACKAGE__->register_method (
1116     method        => 'po_events',
1117     api_name    => 'open-ils.acq.purchase_order.events.owner',
1118     stream      => 1,
1119     signature => q/
1120         Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1121         @param authtoken Login session key
1122         @param owner Id or array of id's for the purchase order Owner field.  Filters the events to just those pertaining to PO's meeting this criteria.
1123         @param options Object for tweaking the selection criteria and fleshing options.
1124     /
1125 );
1126
1127 __PACKAGE__->register_method (
1128     method        => 'po_events',
1129     api_name    => 'open-ils.acq.purchase_order.events.ordering_agency',
1130     stream      => 1,
1131     signature => q/
1132         Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1133         @param authtoken Login session key
1134         @param owner Id or array of id's for the purchase order Ordering Agency field.  Filters the events to just those pertaining to PO's meeting this criteria.
1135         @param options Object for tweaking the selection criteria and fleshing options.
1136     /
1137 );
1138
1139 __PACKAGE__->register_method (
1140     method        => 'po_events',
1141     api_name    => 'open-ils.acq.purchase_order.events.id',
1142     stream      => 1,
1143     signature => q/
1144         Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1145         @param authtoken Login session key
1146         @param owner Id or array of id's for the purchase order Id field.  Filters the events to just those pertaining to PO's meeting this criteria.
1147         @param options Object for tweaking the selection criteria and fleshing options.
1148     /
1149 );
1150
1151 sub po_events {
1152     my($self, $conn, $auth, $search_value, $options) = @_;
1153     my $e = new_editor(authtoken => $auth);
1154     return $e->event unless $e->checkauth;
1155
1156     (my $search_field = $self->api_name) =~ s/.*\.([_a-z]+)$/$1/;
1157     my $obj_type = 'acqpo';
1158
1159     if ($search_field eq 'ordering_agency') {
1160         $search_value = $U->get_org_descendants($search_value);
1161     }
1162
1163     my $query = {
1164         "select"=>{"atev"=>["id"]}, 
1165         "from"=>"atev", 
1166         "where"=>{
1167             "target"=>{
1168                 "in"=>{
1169                     "select"=>{$obj_type=>["id"]}, 
1170                     "from"=>$obj_type,
1171                     "where"=>{$search_field=>$search_value}
1172                 }
1173             }, 
1174             "event_def"=>{
1175                 "in"=>{
1176                     "select"=>{atevdef=>["id"]},
1177                     "from"=>"atevdef",
1178                     "where"=>{
1179                         "hook"=>"format.po.jedi"
1180                     }
1181                 }
1182             },
1183             "state"=>"pending" 
1184         },
1185         "order_by"=>[{"class"=>"atev", "field"=>"run_time", "direction"=>"desc"}]
1186     };
1187
1188     if ($options && defined $options->{state}) {
1189         $query->{'where'}{'state'} = $options->{state}
1190     }
1191
1192     if ($options && defined $options->{start_time}) {
1193         $query->{'where'}{'start_time'} = $options->{start_time};
1194     }
1195
1196     if ($options && defined $options->{order_by}) {
1197         $query->{'order_by'} = $options->{order_by};
1198     }
1199     my $po_events = $e->json_query($query);
1200
1201     my $flesh_fields = { 'atev' => [ 'event_def' ] };
1202     my $flesh_depth = 1;
1203
1204     for my $id (@$po_events) {
1205         my $event = $e->retrieve_action_trigger_event([
1206             $id->{id},
1207             {flesh => $flesh_depth, flesh_fields => $flesh_fields}
1208         ]);
1209         if (! $event) { next; }
1210
1211         my $po = retrieve_purchase_order_impl(
1212             $e,
1213             $event->target(),
1214             {flesh_lineitem_count=>1,flesh_price_summary=>1}
1215         );
1216
1217         if ($e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() )) {
1218             $event->target( $po );
1219             $conn->respond($event);
1220         }
1221     }
1222
1223     return undef;
1224 }
1225
1226 __PACKAGE__->register_method (
1227     method      => 'update_po_events',
1228     api_name    => 'open-ils.acq.purchase_order.event.cancel.batch',
1229     stream      => 1,
1230 );
1231 __PACKAGE__->register_method (
1232     method      => 'update_po_events',
1233     api_name    => 'open-ils.acq.purchase_order.event.reset.batch',
1234     stream      => 1,
1235 );
1236
1237 sub update_po_events {
1238     my($self, $conn, $auth, $event_ids) = @_;
1239     my $e = new_editor(xact => 1, authtoken => $auth);
1240     return $e->die_event unless $e->checkauth;
1241
1242     my $x = 1;
1243     for my $id (@$event_ids) {
1244
1245         # do a little dance to determine what libraries we are ultimately affecting
1246         my $event = $e->retrieve_action_trigger_event([
1247             $id,
1248             {   flesh => 2,
1249                 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
1250             }
1251         ]) or return $e->die_event;
1252
1253         my $po = retrieve_purchase_order_impl(
1254             $e,
1255             $event->target(),
1256             {}
1257         );
1258
1259         return $e->die_event unless $e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() );
1260
1261         if($self->api_name =~ /cancel/) {
1262             $event->state('invalid');
1263         } elsif($self->api_name =~ /reset/) {
1264             $event->clear_start_time;
1265             $event->clear_update_time;
1266             $event->state('pending');
1267         }
1268
1269         $e->update_action_trigger_event($event) or return $e->die_event;
1270         $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
1271     }
1272
1273     $e->commit;
1274     return {complete => 1};
1275 }
1276
1277
1278 __PACKAGE__->register_method (
1279     method      => 'process_fiscal_rollover',
1280     api_name    => 'open-ils.acq.fiscal_rollover.combined',
1281     stream      => 1,
1282     signature => {
1283         desc => q/
1284             Performs a combined fiscal fund rollover process.
1285
1286             Creates a new series of funds for the following year, copying the old years 
1287             funds that are marked as propagable. They apply to the funds belonging to 
1288             either an org unit or to an org unit and all of its dependent org units. 
1289             The procedures may be run repeatedly; if any fund has already been propagated, 
1290             both the old and the new funds will be left alone.
1291
1292             Closes out any applicable funds (by org unit or by org unit and dependents) 
1293             that are marked as propagable. If such a fund has not already been propagated 
1294             to the new year, it will be propagated at closing time.
1295
1296             If a fund is marked as subject to rollover, any unspent balance in the old year's 
1297             fund (including money encumbered but not spent) is transferred to the new year's 
1298             fund. Otherwise it is deallocated back to the funding source(s).
1299
1300             In either case, any encumbrance debits are transferred to the new fund, along 
1301             with the corresponding lineitem details. The old year's fund is marked as inactive 
1302             so that new debits may not be charged to it.
1303         /,
1304         params => [
1305             {desc => 'Authentication token', type => 'string'},
1306             {desc => 'Fund Year to roll over', type => 'integer'},
1307             {desc => 'Org unit ID', type => 'integer'},
1308             {desc => 'Include Descendant Orgs (boolean)', type => 'integer'},
1309             {desc => 'Option hash: limit, offset, encumb_only', type => 'object'},
1310         ],
1311         return => {desc => 'Returns a stream of all related funds for the next year including fund summary for each'}
1312     }
1313
1314 );
1315
1316 __PACKAGE__->register_method (
1317     method      => 'process_fiscal_rollover',
1318     api_name    => 'open-ils.acq.fiscal_rollover.combined.dry_run',
1319     stream      => 1,
1320     signature => {
1321         desc => q/
1322             @see open-ils.acq.fiscal_rollover.combined
1323             This is the dry-run version.  The action is performed,
1324             new fund information is returned, then all changes are rolled back.
1325         /
1326     }
1327
1328 );
1329
1330 __PACKAGE__->register_method (
1331     method      => 'process_fiscal_rollover',
1332     api_name    => 'open-ils.acq.fiscal_rollover.propagate',
1333     stream      => 1,
1334     signature => {
1335         desc => q/
1336             @see open-ils.acq.fiscal_rollover.combined
1337             This version performs fund propagation only.  I.e, creation of
1338             the following year's funds.  It does not rollover over balances, encumbrances, 
1339             or mark the previous year's funds as complete.
1340         /
1341     }
1342 );
1343
1344 __PACKAGE__->register_method (
1345     method      => 'process_fiscal_rollover',
1346     api_name    => 'open-ils.acq.fiscal_rollover.propagate.dry_run',
1347     stream      => 1,
1348     signature => { desc => q/ 
1349         @see open-ils.acq.fiscal_rollover.propagate 
1350         This is the dry-run version.  The action is performed,
1351         new fund information is returned, then all changes are rolled back.
1352     / }
1353 );
1354
1355
1356
1357 sub process_fiscal_rollover {
1358     my( $self, $conn, $auth, $year, $org_id, $descendants, $options ) = @_;
1359
1360     my $e = new_editor(xact=>1, authtoken=>$auth);
1361     return $e->die_event unless $e->checkauth;
1362     return $e->die_event unless $e->allowed('ADMIN_FUND', $org_id);
1363     $options ||= {};
1364
1365     my $combined = ($self->api_name =~ /combined/); 
1366     my $encumb_only = $U->is_true($options->{encumb_only}) ? 't' : 'f';
1367
1368     my $org_ids = ($descendants) ? 
1369         [   
1370             map 
1371             { $_->{id} } # fetch my descendants
1372             @{$e->json_query({from => ['actor.org_unit_descendants', $org_id]})}
1373         ]
1374         : [$org_id];
1375
1376     # Create next year's funds
1377     # Note, it's safe to run this more than once.
1378     # IOW, it will not create duplicate new funds.
1379     $e->json_query({
1380         from => [
1381             ($descendants) ? 
1382                 'acq.propagate_funds_by_org_tree' :
1383                 'acq.propagate_funds_by_org_unit',
1384             $year, $e->requestor->id, $org_id
1385         ]
1386     });
1387
1388     if($combined) {
1389
1390         # Roll the uncumbrances over to next year's funds
1391         # Mark the funds for $year as inactive
1392
1393         $e->json_query({
1394             from => [
1395                 ($descendants) ? 
1396                     'acq.rollover_funds_by_org_tree' :
1397                     'acq.rollover_funds_by_org_unit',
1398                 $year, $e->requestor->id, $org_id, $encumb_only
1399             ]
1400         });
1401     }
1402
1403     # Fetch all funds for the specified org units for the subsequent year
1404     my $fund_ids = $e->search_acq_fund(
1405         [{  year => int($year) + 1, 
1406             org => $org_ids,
1407             propagate => 't' }], 
1408         {idlist => 1}
1409     );
1410
1411     foreach (@$fund_ids) {
1412         my $fund = $e->retrieve_acq_fund($_) or return $e->die_event;
1413         $fund->summary(retrieve_fund_summary_impl($e, $fund));
1414
1415         my $amount = 0;
1416         if($combined and $U->is_true($fund->rollover)) {
1417             # see how much money was rolled over
1418
1419             my $sum = $e->json_query({
1420                 select => {acqftr => [{column => 'dest_amount', transform => 'sum'}]}, 
1421                 from => 'acqftr', 
1422                 where => {dest_fund => $fund->id, note => { like => 'Rollover%' } }
1423             })->[0];
1424
1425             $amount = $sum->{dest_amount} if $sum;
1426         }
1427
1428         $conn->respond({fund => $fund, rollover_amount => $amount});
1429     }
1430
1431     $self->api_name =~ /dry_run/ and $e->rollback or $e->commit;
1432     return undef;
1433 }
1434
1435 __PACKAGE__->register_method(
1436     method => 'org_fiscal_year',
1437     api_name    => 'open-ils.acq.org_unit.current_fiscal_year',
1438     signature => {
1439         desc => q/
1440             Returns the current fiscal year for the given org unit.
1441             If no fiscal year is configured, the current calendar
1442             year is returned.
1443         /,
1444         params => [
1445             {desc => 'Authentication token', type => 'string'},
1446             {desc => 'Org unit ID', type => 'number'}
1447         ],
1448         return => {desc => 'Year as a string (e.g. "2012")'}
1449     }
1450 );
1451
1452 sub org_fiscal_year {
1453     my($self, $conn, $auth, $org_id) = @_;
1454
1455     my $e = new_editor(authtoken => $auth);
1456     return $e->event unless $e->checkauth;
1457
1458     my $year = $e->json_query({
1459         select => {acqfy => ['year']},
1460         from => {acqfy => {acqfc => {join => 'aou'}}},
1461         where => {
1462             '+acqfy' => {
1463                 year_begin => {'<=' => 'now'},
1464                 year_end => {'>=' => 'now'},
1465             },
1466             '+aou' => {id => $org_id}
1467         }
1468     })->[0];
1469
1470     return $year ? $year->{year} : DateTime->now->year;
1471 }
1472
1473 1;
1474