acdcd31cb89cfcff8405f1a5c4a0c7637b50635d
[working/Evergreen.git] / Open-ILS / src / perlmods / 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)_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         my $ratio = 1;
419         if ($ofund->currency_type ne $dfund->currency_type) {
420             my $exchange_rate = $e->json_query({
421                 "select"=>{"acqexr"=>["ratio"]}, 
422                 "from"=>"acqexr", 
423                 "where"=>{
424                     "from_currency"=>$ofund->currency_type,
425                     "to_currency"=>$dfund->currency_type
426                 }
427             });
428             if (scalar(@$exchange_rate)<1) {
429                 $logger->error('Unable to find exchange rate for ' . $ofund->currency_type . ' to ' . $dfund->currency_type);
430                 return $e->die_event;
431             }
432             $ratio = @{$exchange_rate}[0]->{ratio};
433         }
434         $dfund_amount = $ofund_amount * $ratio;
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         signature => {
825                       desc      => 'Retrieves a purchase order',
826                       params    => [
827                                     {desc => 'Authentication token', type => 'string'},
828                                     {desc => 'purchase_order to retrieve', type => 'number'},
829                                     {desc => q/Options hash.  flesh_lineitems, to get the lineitems and lineitem_attrs;
830                 clear_marc, to clear the MARC data from the lineitem (for reduced bandwidth)
831                 li_limit : number of lineitems to return if fleshing line items;
832                 li_offset : lineitem offset if fleshing line items
833                 li_order_by : lineitem sort definition if fleshing line items,
834                 flesh_po_items : po_item objects
835                 /,
836                                      type => 'hash'}
837                                    ],
838                       return => {desc => 'The purchase order, Event on failure'}
839                      }
840 );
841
842 sub retrieve_purchase_order {
843     my($self, $conn, $auth, $po_id, $options) = @_;
844     my $e = new_editor(authtoken=>$auth);
845     return $e->event unless $e->checkauth;
846
847     $po_id = [ $po_id ] unless ref $po_id;
848     for ( @{$po_id} ) {
849         my $rv;
850         if ( po_perm_failure($e, $_) )
851           { $rv = $e->event }
852         else
853           { $rv =  retrieve_purchase_order_impl($e, $_, $options) }
854
855         $conn->respond($rv);
856     }
857
858     return undef;
859 }
860
861
862 # if the user does not have permission to perform actions on this PO, return the perm failure event
863 sub po_perm_failure {
864     my($e, $po_id, $fund_id) = @_;
865     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
866     return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency, $po);
867     return undef;
868 }
869
870 sub build_price_summary {
871     my ($e, $po_id) = @_;
872
873     # TODO: Add summary value for estimated amount (pre-encumber)
874
875     # fetch the fund debits for this purchase order
876     my $debits = $e->json_query({
877         "select" => {"acqfdeb" => [qw/encumbrance amount/]},
878         "from" => {
879             "acqlid" => {
880                 "jub" => {
881                     "fkey" => "lineitem",
882                     "field" => "id",
883                     "join" => {
884                         "acqpo" => {
885                             "fkey" => "purchase_order", "field" => "id"
886                         }
887                     }
888                 },
889                 "acqfdeb" => {"fkey" => "fund_debit", "field" => "id"}
890             }
891         },
892         "where" => {"+acqpo" => {"id" => $po_id}}
893     });
894
895     # add any debits for non-bib po_items
896     push(@$debits, @{
897         $e->json_query({
898             "select" => {"acqfdeb" => [qw/encumbrance amount/]},
899             "from" => {acqpoi => 'acqfdeb'},
900             "where" => {"+acqpoi" => {"purchase_order" => $po_id}}
901         })
902     });
903
904     my ($enc, $spent) = (0, 0);
905     for my $deb (@$debits) {
906         if($U->is_true($deb->{encumbrance})) {
907             $enc += $deb->{amount};
908         } else {
909             $spent += $deb->{amount};
910         }
911     }
912     ($enc, $spent);
913 }
914
915
916 sub retrieve_purchase_order_impl {
917     my($e, $po_id, $options) = @_;
918
919     my $flesh = {"flesh" => 1, "flesh_fields" => {"acqpo" => []}};
920
921     $options ||= {};
922     unless ($options->{"no_flesh_cancel_reason"}) {
923         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "cancel_reason";
924     }
925     if ($options->{"flesh_notes"}) {
926         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
927     }
928     if ($options->{"flesh_provider"}) {
929         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider";
930     }
931
932     push (@{$flesh->{flesh_fields}->{acqpo}}, 'po_items') if $options->{flesh_po_items};
933
934     my $args = (@{$flesh->{"flesh_fields"}->{"acqpo"}}) ?
935         [$po_id, $flesh] : $po_id;
936
937     my $po = $e->retrieve_acq_purchase_order($args)
938         or return $e->event;
939
940     if($$options{flesh_lineitems}) {
941
942         my $flesh_fields = { jub => ['attributes'] };
943         $flesh_fields->{jub}->[1] = 'lineitem_details' if $$options{flesh_lineitem_details};
944         $flesh_fields->{acqlid} = ['fund_debit'] if $$options{flesh_fund_debit};
945
946         my $items = $e->search_acq_lineitem([
947             {purchase_order => $po_id},
948             {
949                 flesh => 3,
950                 flesh_fields => $flesh_fields,
951                 limit => $$options{li_limit} || 50,
952                 offset => $$options{li_offset} || 0,
953                 order_by => {jub => $$options{li_order_by} || 'create_time'}
954             }
955         ]);
956
957         if($$options{clear_marc}) {
958             $_->clear_marc for @$items;
959         }
960
961         $po->lineitems($items);
962         $po->lineitem_count(scalar(@$items));
963
964     } elsif( $$options{flesh_lineitem_ids} ) {
965         $po->lineitems($e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1}));
966
967     } elsif( $$options{flesh_lineitem_count} ) {
968
969         my $items = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist=>1});
970         $po->lineitem_count(scalar(@$items));
971     }
972
973     if($$options{flesh_price_summary}) {
974         my ($enc, $spent) = build_price_summary($e, $po_id);
975         $po->amount_encumbered($enc);
976         $po->amount_spent($spent);
977     }
978
979     return $po;
980 }
981
982
983 __PACKAGE__->register_method(
984         method => 'format_po',
985         api_name        => 'open-ils.acq.purchase_order.format'
986 );
987
988 sub format_po {
989     my($self, $conn, $auth, $po_id, $format) = @_;
990     my $e = new_editor(authtoken=>$auth);
991     return $e->event unless $e->checkauth;
992
993     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
994     return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
995
996     my $hook = "format.po.$format";
997     return $U->fire_object_event(undef, $hook, $po, $po->ordering_agency);
998 }
999
1000 __PACKAGE__->register_method(
1001         method => 'format_lineitem',
1002         api_name        => 'open-ils.acq.lineitem.format'
1003 );
1004
1005 sub format_lineitem {
1006     my($self, $conn, $auth, $li_id, $format, $user_data) = @_;
1007     my $e = new_editor(authtoken=>$auth);
1008     return $e->event unless $e->checkauth;
1009
1010     my $li = $e->retrieve_acq_lineitem($li_id) or return $e->event;
1011
1012     my $context_org;
1013     if (defined $li->purchase_order) {
1014         my $po = $e->retrieve_acq_purchase_order($li->purchase_order) or return $e->die_event;
1015         return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1016         $context_org = $po->ordering_agency;
1017     } else {
1018         my $pl = $e->retrieve_acq_picklist($li->picklist) or return $e->die_event;
1019         if($e->requestor->id != $pl->owner) {
1020             return $e->event unless
1021                 $e->allowed('VIEW_PICKLIST', $pl->org_unit, $pl);
1022         }
1023         $context_org = $pl->org_unit;
1024     }
1025
1026     my $hook = "format.acqli.$format";
1027     return $U->fire_object_event(undef, $hook, $li, $context_org, 'print-on-demand', $user_data);
1028 }
1029
1030 __PACKAGE__->register_method (
1031     method        => 'po_events',
1032     api_name    => 'open-ils.acq.purchase_order.events.owner',
1033     stream      => 1,
1034     signature => q/
1035         Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1036         @param authtoken Login session key
1037         @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.
1038         @param options Object for tweaking the selection criteria and fleshing options.
1039     /
1040 );
1041
1042 __PACKAGE__->register_method (
1043     method        => 'po_events',
1044     api_name    => 'open-ils.acq.purchase_order.events.ordering_agency',
1045     stream      => 1,
1046     signature => q/
1047         Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1048         @param authtoken Login session key
1049         @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.
1050         @param options Object for tweaking the selection criteria and fleshing options.
1051     /
1052 );
1053
1054 __PACKAGE__->register_method (
1055     method        => 'po_events',
1056     api_name    => 'open-ils.acq.purchase_order.events.id',
1057     stream      => 1,
1058     signature => q/
1059         Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1060         @param authtoken Login session key
1061         @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.
1062         @param options Object for tweaking the selection criteria and fleshing options.
1063     /
1064 );
1065
1066 sub po_events {
1067     my($self, $conn, $auth, $search_value, $options) = @_;
1068     my $e = new_editor(authtoken => $auth);
1069     return $e->event unless $e->checkauth;
1070
1071     (my $search_field = $self->api_name) =~ s/.*\.([_a-z]+)$/$1/;
1072     my $obj_type = 'acqpo';
1073
1074     if ($search_field eq 'ordering_agency') {
1075         $search_value = $U->get_org_descendants($search_value);
1076     }
1077
1078     my $query = {
1079         "select"=>{"atev"=>["id"]}, 
1080         "from"=>"atev", 
1081         "where"=>{
1082             "target"=>{
1083                 "in"=>{
1084                     "select"=>{$obj_type=>["id"]}, 
1085                     "from"=>$obj_type,
1086                     "where"=>{$search_field=>$search_value}
1087                 }
1088             }, 
1089             "event_def"=>{
1090                 "in"=>{
1091                     "select"=>{atevdef=>["id"]},
1092                     "from"=>"atevdef",
1093                     "where"=>{
1094                         "hook"=>"format.po.jedi"
1095                     }
1096                 }
1097             },
1098             "state"=>"pending" 
1099         },
1100         "order_by"=>[{"class"=>"atev", "field"=>"run_time", "direction"=>"desc"}]
1101     };
1102
1103     if ($options && defined $options->{state}) {
1104         $query->{'where'}{'state'} = $options->{state}
1105     }
1106
1107     if ($options && defined $options->{start_time}) {
1108         $query->{'where'}{'start_time'} = $options->{start_time};
1109     }
1110
1111     if ($options && defined $options->{order_by}) {
1112         $query->{'order_by'} = $options->{order_by};
1113     }
1114     my $po_events = $e->json_query($query);
1115
1116     my $flesh_fields = { 'atev' => [ 'event_def' ] };
1117     my $flesh_depth = 1;
1118
1119     for my $id (@$po_events) {
1120         my $event = $e->retrieve_action_trigger_event([
1121             $id->{id},
1122             {flesh => $flesh_depth, flesh_fields => $flesh_fields}
1123         ]);
1124         if (! $event) { next; }
1125
1126         my $po = retrieve_purchase_order_impl(
1127             $e,
1128             $event->target(),
1129             {flesh_lineitem_count=>1,flesh_price_summary=>1}
1130         );
1131
1132         if ($e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() )) {
1133             $event->target( $po );
1134             $conn->respond($event);
1135         }
1136     }
1137
1138     return undef;
1139 }
1140
1141 __PACKAGE__->register_method (
1142         method          => 'update_po_events',
1143     api_name    => 'open-ils.acq.purchase_order.event.cancel.batch',
1144     stream      => 1,
1145 );
1146 __PACKAGE__->register_method (
1147         method          => 'update_po_events',
1148     api_name    => 'open-ils.acq.purchase_order.event.reset.batch',
1149     stream      => 1,
1150 );
1151
1152 sub update_po_events {
1153     my($self, $conn, $auth, $event_ids) = @_;
1154     my $e = new_editor(xact => 1, authtoken => $auth);
1155     return $e->die_event unless $e->checkauth;
1156
1157     my $x = 1;
1158     for my $id (@$event_ids) {
1159
1160         # do a little dance to determine what libraries we are ultimately affecting
1161         my $event = $e->retrieve_action_trigger_event([
1162             $id,
1163             {   flesh => 2,
1164                 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
1165             }
1166         ]) or return $e->die_event;
1167
1168         my $po = retrieve_purchase_order_impl(
1169             $e,
1170             $event->target(),
1171             {}
1172         );
1173
1174         return $e->die_event unless $e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() );
1175
1176         if($self->api_name =~ /cancel/) {
1177             $event->state('invalid');
1178         } elsif($self->api_name =~ /reset/) {
1179             $event->clear_start_time;
1180             $event->clear_update_time;
1181             $event->state('pending');
1182         }
1183
1184         $e->update_action_trigger_event($event) or return $e->die_event;
1185         $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
1186     }
1187
1188     $e->commit;
1189     return {complete => 1};
1190 }
1191
1192
1193 __PACKAGE__->register_method (
1194         method          => 'process_fiscal_rollover',
1195     api_name    => 'open-ils.acq.fiscal_rollover.combined',
1196     stream      => 1,
1197         signature => {
1198         desc => q/
1199             Performs a combined fiscal fund rollover process.
1200
1201             Creates a new series of funds for the following year, copying the old years 
1202             funds that are marked as propagable. They apply to the funds belonging to 
1203             either an org unit or to an org unit and all of its dependent org units. 
1204             The procedures may be run repeatedly; if any fund has already been propagated, 
1205             both the old and the new funds will be left alone.
1206
1207             Closes out any applicable funds (by org unit or by org unit and dependents) 
1208             that are marked as propagable. If such a fund has not already been propagated 
1209             to the new year, it will be propagated at closing time.
1210
1211             If a fund is marked as subject to rollover, any unspent balance in the old year's 
1212             fund (including money encumbered but not spent) is transferred to the new year's 
1213             fund. Otherwise it is deallocated back to the funding source(s).
1214
1215             In either case, any encumbrance debits are transferred to the new fund, along 
1216             with the corresponding lineitem details. The old year's fund is marked as inactive 
1217             so that new debits may not be charged to it.
1218         /,
1219         params => [
1220             {desc => 'Authentication token', type => 'string'},
1221             {desc => 'Fund Year to roll over', type => 'integer'},
1222             {desc => 'Org unit ID', type => 'integer'},
1223             {desc => 'Include Descendant Orgs (boolean)', type => 'integer'},
1224         ],
1225         return => {desc => 'Returns a stream of all related funds for the next year including fund summary for each'}
1226     }
1227
1228 );
1229
1230 __PACKAGE__->register_method (
1231         method          => 'process_fiscal_rollover',
1232     api_name    => 'open-ils.acq.fiscal_rollover.combined.dry_run',
1233     stream      => 1,
1234         signature => {
1235         desc => q/
1236             @see open-ils.acq.fiscal_rollover.combined
1237             This is the dry-run version.  The action is performed,
1238             new fund information is returned, then all changes are rolled back.
1239         /
1240     }
1241
1242 );
1243
1244 __PACKAGE__->register_method (
1245         method          => 'process_fiscal_rollover',
1246     api_name    => 'open-ils.acq.fiscal_rollover.propagate',
1247     stream      => 1,
1248         signature => {
1249         desc => q/
1250             @see open-ils.acq.fiscal_rollover.combined
1251             This version performs fund propagation only.  I.e, creation of
1252             the following year's funds.  It does not rollover over balances, encumbrances, 
1253             or mark the previous year's funds as complete.
1254         /
1255     }
1256 );
1257
1258 __PACKAGE__->register_method (
1259         method          => 'process_fiscal_rollover',
1260     api_name    => 'open-ils.acq.fiscal_rollover.propagate.dry_run',
1261     stream      => 1,
1262         signature => { desc => q/ 
1263         @see open-ils.acq.fiscal_rollover.propagate 
1264         This is the dry-run version.  The action is performed,
1265         new fund information is returned, then all changes are rolled back.
1266     / }
1267 );
1268
1269
1270
1271 sub process_fiscal_rollover {
1272     my( $self, $conn, $auth, $year, $org_id, $descendants, $options ) = @_;
1273
1274     my $e = new_editor(xact=>1, authtoken=>$auth);
1275     return $e->die_event unless $e->checkauth;
1276     return $e->die_event unless $e->allowed('ADMIN_FUND', $org_id);
1277     $options ||= {};
1278
1279     my $combined = ($self->api_name =~ /combined/); 
1280
1281     my $org_ids = ($descendants) ? 
1282         [   
1283             map 
1284             { $_->{id} } # fetch my descendants
1285             @{$e->json_query({from => ['actor.org_unit_descendants', $org_id]})}
1286         ]
1287         : [$org_id];
1288
1289     # Create next year's funds
1290     # Note, it's safe to run this more than once.
1291     # IOW, it will not create duplicate new funds.
1292     $e->json_query({
1293         from => [
1294             ($descendants) ? 
1295                 'acq.propagate_funds_by_org_tree' :
1296                 'acq.propagate_funds_by_org_unit',
1297             $year, $e->requestor->id, $org_id
1298         ]
1299     });
1300
1301     if($combined) {
1302
1303         # Roll the uncumbrances over to next year's funds
1304         # Mark the funds for $year as inactive
1305
1306         $e->json_query({
1307             from => [
1308                 ($descendants) ? 
1309                     'acq.rollover_funds_by_org_tree' :
1310                     'acq.rollover_funds_by_org_unit',
1311                 $year, $e->requestor->id, $org_id
1312             ]
1313         });
1314     }
1315
1316     # Fetch all funds for the specified org units for the subsequent year
1317     my $fund_ids = $e->search_acq_fund([
1318         {
1319             year => int($year) + 1, 
1320             org => $org_ids,
1321             propagate => 't'
1322         }, {
1323             limit => $$options{limit} || 20,
1324             offset => $$options{offset} || 0,
1325         }
1326         ], 
1327         {idlist => 1}
1328     );
1329
1330     foreach (@$fund_ids) {
1331         my $fund = $e->retrieve_acq_fund($_) or return $e->die_event;
1332         $fund->summary(retrieve_fund_summary_impl($e, $fund));
1333
1334         my $amount = 0;
1335         if($combined and $U->is_true($fund->rollover)) {
1336             # see how much money was rolled over
1337
1338             my $sum = $e->json_query({
1339                 select => {acqftr => [{column => 'dest_amount', transform => 'sum'}]}, 
1340                 from => 'acqftr', 
1341                 where => {dest_fund => $fund->id, note => 'Rollover'}
1342             })->[0];
1343
1344             $amount = $sum->{dest_amount} if $sum;
1345         }
1346
1347         $conn->respond({fund => $fund, rollover_amount => $amount});
1348     }
1349
1350     $self->api_name =~ /dry_run/ and $e->rollback or $e->commit;
1351     return undef;
1352 }
1353
1354
1355 1;
1356