1 package OpenILS::Application::Acq::Financials;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
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;
11 use OpenILS::Application::AppUtils;
12 use OpenILS::Application::Acq::Lineitem;
13 my $U = 'OpenILS::Application::AppUtils';
15 # ----------------------------------------------------------------------------
17 # ----------------------------------------------------------------------------
19 __PACKAGE__->register_method(
20 method => 'create_funding_source',
21 api_name => 'open-ils.acq.funding_source.create',
23 desc => 'Creates a new funding_source',
25 {desc => 'Authentication token', type => 'string'},
26 {desc => 'funding source object to create', type => 'object'}
28 return => {desc => 'The ID of the new funding_source'}
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;
39 return $funding_source->id;
43 __PACKAGE__->register_method(
44 method => 'delete_funding_source',
45 api_name => 'open-ils.acq.funding_source.delete',
47 desc => 'Deletes a funding_source',
49 {desc => 'Authentication token', type => 'string'},
50 {desc => 'funding source ID', type => 'number'}
52 return => {desc => '1 on success, Event on failure'}
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;
67 __PACKAGE__->register_method(
68 method => 'retrieve_funding_source',
69 api_name => 'open-ils.acq.funding_source.retrieve',
71 desc => 'Retrieves a new funding_source',
73 {desc => 'Authentication token', type => 'string'},
74 {desc => 'funding source ID', type => 'number'}
76 return => {desc => 'The funding_source object on success, Event on failure'}
80 sub retrieve_funding_source {
81 my($self, $conn, $auth, $funding_source_id, $options) = @_;
82 my $e = new_editor(authtoken=>$auth);
83 return $e->event unless $e->checkauth;
86 my $flesh = {flesh => 1, flesh_fields => {acqfs => []}};
87 push(@{$flesh->{flesh_fields}->{acqfs}}, 'credits') if $$options{flesh_credits};
88 push(@{$flesh->{flesh_fields}->{acqfs}}, 'allocations') if $$options{flesh_allocations};
90 my $funding_source = $e->retrieve_acq_funding_source([$funding_source_id, $flesh]) or return $e->event;
92 return $e->event unless $e->allowed(
93 ['ADMIN_FUNDING_SOURCE','MANAGE_FUNDING_SOURCE', 'VIEW_FUNDING_SOURCE'],
94 $funding_source->owner, $funding_source);
96 $funding_source->summary(retrieve_funding_source_summary_impl($e, $funding_source))
97 if $$options{flesh_summary};
98 return $funding_source;
101 __PACKAGE__->register_method(
102 method => 'retrieve_org_funding_sources',
103 api_name => 'open-ils.acq.funding_source.org.retrieve',
106 desc => 'Retrieves all the funding_sources associated with an org unit that the requestor has access to see',
108 {desc => 'Authentication token', type => 'string'},
109 {desc => 'List of org Unit IDs. If no IDs are provided, this method returns the
110 full set of funding sources this user has permission to view', type => 'number'},
111 {desc => q/Limiting permission. this permission is used find the work-org tree from which
112 the list of orgs is generated if no org ids are provided.
113 The default is ADMIN_FUNDING_SOURCE/, type => 'string'},
115 return => {desc => 'The funding_source objects on success, empty array otherwise'}
119 sub retrieve_org_funding_sources {
120 my($self, $conn, $auth, $org_id_list, $options) = @_;
121 my $e = new_editor(authtoken=>$auth);
122 return $e->event unless $e->checkauth;
125 my $limit_perm = ($$options{limit_perm}) ? $$options{limit_perm} : 'ADMIN_FUNDING_SOURCE';
126 return OpenILS::Event->new('BAD_PARAMS')
127 unless $limit_perm =~ /(ADMIN|MANAGE|VIEW)_FUNDING_SOURCE/;
129 my $org_ids = ($org_id_list and @$org_id_list) ? $org_id_list :
130 $U->user_has_work_perm_at($e, $limit_perm, {descendants =>1});
132 return [] unless @$org_ids;
133 my $sources = $e->search_acq_funding_source({owner => $org_ids});
135 for my $source (@$sources) {
136 $source->summary(retrieve_funding_source_summary_impl($e, $source))
137 if $$options{flesh_summary};
138 $conn->respond($source);
144 sub retrieve_funding_source_summary_impl {
145 my($e, $source) = @_;
146 my $at = $e->search_acq_funding_source_allocation_total({funding_source => $source->id})->[0];
147 my $b = $e->search_acq_funding_source_balance({funding_source => $source->id})->[0];
148 my $ct = $e->search_acq_funding_source_credit_total({funding_source => $source->id})->[0];
150 allocation_total => ($at) ? $at->amount : 0,
151 balance => ($b) ? $b->amount : 0,
152 credit_total => ($ct) ? $ct->amount : 0,
157 __PACKAGE__->register_method(
158 method => 'create_funding_source_credit',
159 api_name => 'open-ils.acq.funding_source_credit.create',
161 desc => 'Create a new funding source credit',
163 {desc => 'Authentication token', type => 'string'},
164 {desc => 'funding source credit object', type => 'object'}
166 return => {desc => 'The ID of the new funding source credit on success, Event on failure'}
170 sub create_funding_source_credit {
171 my($self, $conn, $auth, $fs_credit) = @_;
172 my $e = new_editor(authtoken=>$auth, xact=>1);
173 return $e->event unless $e->checkauth;
175 my $fs = $e->retrieve_acq_funding_source($fs_credit->funding_source)
176 or return $e->die_event;
177 return $e->die_event unless $e->allowed(['MANAGE_FUNDING_SOURCE'], $fs->owner, $fs);
179 $e->create_acq_funding_source_credit($fs_credit) or return $e->die_event;
181 return $fs_credit->id;
185 # ---------------------------------------------------------------
187 # ---------------------------------------------------------------
189 __PACKAGE__->register_method(
190 method => 'create_fund',
191 api_name => 'open-ils.acq.fund.create',
193 desc => 'Creates a new fund',
195 {desc => 'Authentication token', type => 'string'},
196 {desc => 'fund object to create', type => 'object'}
198 return => {desc => 'The ID of the newly created fund object'}
203 my($self, $conn, $auth, $fund) = @_;
204 my $e = new_editor(xact=>1, authtoken=>$auth);
205 return $e->die_event unless $e->checkauth;
206 return $e->die_event unless $e->allowed('ADMIN_FUND', $fund->org);
207 $e->create_acq_fund($fund) or return $e->die_event;
213 __PACKAGE__->register_method(
214 method => 'delete_fund',
215 api_name => 'open-ils.acq.fund.delete',
217 desc => 'Deletes a fund',
219 {desc => 'Authentication token', type => 'string'},
220 {desc => 'fund ID', type => 'number'}
222 return => {desc => '1 on success, Event on failure'}
227 my($self, $conn, $auth, $fund_id) = @_;
228 my $e = new_editor(xact=>1, authtoken=>$auth);
229 return $e->die_event unless $e->checkauth;
230 my $fund = $e->retrieve_acq_fund($fund_id) or return $e->die_event;
231 return $e->die_event unless $e->allowed('ADMIN_FUND', $fund->org, $fund);
232 $e->delete_acq_fund($fund) or return $e->die_event;
237 __PACKAGE__->register_method(
238 method => 'retrieve_fund',
239 api_name => 'open-ils.acq.fund.retrieve',
241 desc => 'Retrieves a new fund',
243 {desc => 'Authentication token', type => 'string'},
244 {desc => 'fund ID', type => 'number'}
246 return => {desc => 'The fund object on success, Event on failure'}
251 my($self, $conn, $auth, $fund_id, $options) = @_;
252 my $e = new_editor(authtoken=>$auth);
253 return $e->event unless $e->checkauth;
256 my $flesh = {flesh => 2, flesh_fields => {acqf => []}};
257 push(@{$flesh->{flesh_fields}->{acqf}}, 'debits') if $$options{flesh_debits};
258 push(@{$flesh->{flesh_fields}->{acqf}}, 'allocations') if $$options{flesh_allocations};
259 push(@{$flesh->{flesh_fields}->{acqfa}}, 'funding_source') if $$options{flesh_allocation_sources};
261 my $fund = $e->retrieve_acq_fund([$fund_id, $flesh]) or return $e->event;
262 return $e->event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND', 'VIEW_FUND'], $fund->org, $fund);
263 $fund->summary(retrieve_fund_summary_impl($e, $fund))
264 if $$options{flesh_summary};
268 __PACKAGE__->register_method(
269 method => 'retrieve_org_funds',
270 api_name => 'open-ils.acq.fund.org.retrieve',
273 desc => 'Retrieves all the funds associated with an org unit',
275 {desc => 'Authentication token', type => 'string'},
276 {desc => 'List of org Unit IDs. If no IDs are provided, this method returns the
277 full set of funding sources this user has permission to view', type => 'number'},
278 {desc => q/Options hash.
279 "limit_perm" -- this permission is used find the work-org tree from which
280 the list of orgs is generated if no org ids are provided. The default is ADMIN_FUND.
281 "flesh_summary" -- if true, the summary field on each fund is fleshed
282 The default is ADMIN_FUND/, type => 'string'},
284 return => {desc => 'The fund objects on success, Event on failure'}
288 __PACKAGE__->register_method(
289 method => 'retrieve_org_funds',
290 api_name => 'open-ils.acq.fund.org.years.retrieve');
293 sub retrieve_org_funds {
294 my($self, $conn, $auth, $filter, $options) = @_;
295 my $e = new_editor(authtoken=>$auth);
296 return $e->event unless $e->checkauth;
300 my $limit_perm = ($$options{limit_perm}) ? $$options{limit_perm} : 'ADMIN_FUND';
301 return OpenILS::Event->new('BAD_PARAMS')
302 unless $limit_perm =~ /(ADMIN|MANAGE|VIEW)_FUND/;
304 $filter->{org} = $filter->{org} ||
305 $U->user_has_work_perm_at($e, $limit_perm, {descendants =>1});
306 return undef unless @{$filter->{org}};
311 limit => $$options{limit} || 50,
312 offset => $$options{offset} || 0,
313 order_by => $$options{order_by} || {acqf => 'name'}
317 if($self->api_name =~ /years/) {
318 # return the distinct set of fund years covered by the selected funds
319 my $data = $e->json_query({
321 acqf => [{column => 'year', transform => 'distinct'}]
327 return [map { $_->{year} } @$data];
330 my $funds = $e->search_acq_fund($query);
332 for my $fund (@$funds) {
333 $fund->summary(retrieve_fund_summary_impl($e, $fund))
334 if $$options{flesh_summary};
335 $conn->respond($fund);
341 __PACKAGE__->register_method(
342 method => 'retrieve_fund_summary',
343 api_name => 'open-ils.acq.fund.summary.retrieve',
345 desc => 'Returns a summary of credits/debits/encumbrances for a fund',
347 {desc => 'Authentication token', type => 'string'},
348 {desc => 'fund id', type => 'number' }
350 return => {desc => 'A hash of summary information, Event on failure'}
354 sub retrieve_fund_summary {
355 my($self, $conn, $auth, $fund_id) = @_;
356 my $e = new_editor(authtoken=>$auth);
357 return $e->event unless $e->checkauth;
358 my $fund = $e->retrieve_acq_fund($fund_id) or return $e->event;
359 return $e->event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
360 return retrieve_fund_summary_impl($e, $fund);
364 sub retrieve_fund_summary_impl {
367 my $at = $e->search_acq_fund_allocation_total({fund => $fund->id})->[0];
368 my $dt = $e->search_acq_fund_debit_total({fund => $fund->id})->[0];
369 my $et = $e->search_acq_fund_encumbrance_total({fund => $fund->id})->[0];
370 my $st = $e->search_acq_fund_spent_total({fund => $fund->id})->[0];
371 my $cb = $e->search_acq_fund_combined_balance({fund => $fund->id})->[0];
372 my $sb = $e->search_acq_fund_spent_balance({fund => $fund->id})->[0];
375 allocation_total => ($at) ? $at->amount : 0,
376 debit_total => ($dt) ? $dt->amount : 0,
377 encumbrance_total => ($et) ? $et->amount : 0,
378 spent_total => ($st) ? $st->amount : 0,
379 combined_balance => ($cb) ? $cb->amount : 0,
380 spent_balance => ($sb) ? $sb->amount : 0,
385 # ---------------------------------------------------------------
387 # ---------------------------------------------------------------
389 __PACKAGE__->register_method(
390 method => 'create_fund_alloc',
391 api_name => 'open-ils.acq.fund_allocation.create',
393 desc => 'Creates a new fund_allocation',
395 {desc => 'Authentication token', type => 'string'},
396 {desc => 'fund allocation object to create', type => 'object'}
398 return => {desc => 'The ID of the new fund_allocation'}
402 sub create_fund_alloc {
403 my($self, $conn, $auth, $fund_alloc) = @_;
404 my $e = new_editor(xact=>1, authtoken=>$auth);
405 return $e->die_event unless $e->checkauth;
407 # this action is equivalent to both debiting a funding source and crediting a fund
409 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
410 or return $e->die_event;
411 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner);
413 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
414 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
416 $fund_alloc->allocator($e->requestor->id);
417 $e->create_acq_fund_allocation($fund_alloc) or return $e->die_event;
419 return $fund_alloc->id;
423 __PACKAGE__->register_method(
424 method => 'delete_fund_alloc',
425 api_name => 'open-ils.acq.fund_allocation.delete',
427 desc => 'Deletes a fund_allocation',
429 {desc => 'Authentication token', type => 'string'},
430 {desc => 'fund Alocation ID', type => 'number'}
432 return => {desc => '1 on success, Event on failure'}
436 sub delete_fund_alloc {
437 my($self, $conn, $auth, $fund_alloc_id) = @_;
438 my $e = new_editor(xact=>1, authtoken=>$auth);
439 return $e->die_event unless $e->checkauth;
441 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->die_event;
443 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
444 or return $e->die_event;
445 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
447 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
448 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
450 $e->delete_acq_fund_allocation($fund_alloc) or return $e->die_event;
455 __PACKAGE__->register_method(
456 method => 'retrieve_fund_alloc',
457 api_name => 'open-ils.acq.fund_allocation.retrieve',
459 desc => 'Retrieves a new fund_allocation',
461 {desc => 'Authentication token', type => 'string'},
462 {desc => 'fund Allocation ID', type => 'number'}
464 return => {desc => 'The fund allocation object on success, Event on failure'}
468 sub retrieve_fund_alloc {
469 my($self, $conn, $auth, $fund_alloc_id) = @_;
470 my $e = new_editor(authtoken=>$auth);
471 return $e->event unless $e->checkauth;
472 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
474 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
475 or return $e->die_event;
476 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
478 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
479 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
485 __PACKAGE__->register_method(
486 method => 'retrieve_funding_source_allocations',
487 api_name => 'open-ils.acq.funding_source.allocations.retrieve',
489 desc => 'Retrieves a new fund_allocation',
491 {desc => 'Authentication token', type => 'string'},
492 {desc => 'fund Allocation ID', type => 'number'}
494 return => {desc => 'The fund allocation object on success, Event on failure'}
498 sub retrieve_funding_source_allocations {
499 my($self, $conn, $auth, $fund_alloc_id) = @_;
500 my $e = new_editor(authtoken=>$auth);
501 return $e->event unless $e->checkauth;
502 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
504 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
505 or return $e->die_event;
506 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
508 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
509 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
514 # ----------------------------------------------------------------------------
516 # ----------------------------------------------------------------------------
518 __PACKAGE__->register_method(
519 method => 'retrieve_all_currency_type',
520 api_name => 'open-ils.acq.currency_type.all.retrieve',
523 desc => 'Retrieves all currency_type objects',
525 {desc => 'Authentication token', type => 'string'},
527 return => {desc => 'List of currency_type objects', type => 'list'}
531 sub retrieve_all_currency_type {
532 my($self, $conn, $auth, $fund_alloc_id) = @_;
533 my $e = new_editor(authtoken=>$auth);
534 return $e->event unless $e->checkauth;
535 return $e->event unless $e->allowed('GENERAL_ACQ');
536 $conn->respond($_) for @{$e->retrieve_all_acq_currency_type()};
539 sub currency_conversion_impl {
540 my($src_currency, $dest_currency, $amount) = @_;
541 my $result = new_editor()->json_query({
544 params => [$dest_currency, $amount],
545 transform => 'acq.exchange_ratio',
550 where => {code => $src_currency},
554 return $result->[0]->{value};
558 __PACKAGE__->register_method(
559 method => 'create_lineitem_assets',
560 api_name => 'open-ils.acq.lineitem.assets.create',
562 desc => q/Creates the bibliographic data, volume, and copies associated with a lineitem./,
564 {desc => 'Authentication token', type => 'string'},
565 {desc => 'The lineitem id', type => 'number'},
566 {desc => q/Options hash./}
568 return => {desc => 'ID of newly created bib record, Event on error'}
572 sub create_lineitem_assets {
573 my($self, $conn, $auth, $li_id, $options) = @_;
574 my $e = new_editor(authtoken=>$auth, xact=>1);
575 return $e->die_event unless $e->checkauth;
576 my ($count, $resp) = create_lineitem_assets_impl($e, $li_id, $options);
577 return $resp if $resp;
582 sub create_lineitem_assets_impl {
583 my($e, $li_id, $options) = @_;
587 my $li = $e->retrieve_acq_lineitem([
590 flesh_fields => {jub => ['purchase_order', 'attributes']}
592 ]) or return (undef, $e->die_event);
594 # -----------------------------------------------------------------
595 # first, create the bib record if necessary
596 # -----------------------------------------------------------------
597 unless($li->eg_bib_id) {
599 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
600 $e, $li->marc); #$rec->bib_source
602 if($U->event_code($record)) {
604 return (undef, $record);
607 $li->editor($e->requestor->id);
608 $li->edit_time('now');
609 $li->eg_bib_id($record->id);
610 $e->update_acq_lineitem($li) or return (undef, $e->die_event);
613 my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
615 # -----------------------------------------------------------------
616 # for each lineitem_detail, create the volume if necessary, create
617 # a copy, and link them all together.
618 # -----------------------------------------------------------------
620 for my $li_detail_id (@{$li_details}) {
622 my $li_detail = $e->retrieve_acq_lineitem_detail($li_detail_id)
623 or return (undef, $e->die_event);
625 # Create the volume object if necessary
626 my $volume = $volcache{$li_detail->cn_label};
627 unless($volume and $volume->owning_lib == $li_detail->owning_lib) {
629 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
630 $e, $li_detail->cn_label, $li->eg_bib_id, $li_detail->owning_lib);
631 return (undef, $evt) if $evt;
632 $volcache{$volume->id} = $volume;
635 my $copy = Fieldmapper::asset::copy->new;
637 $copy->loan_duration(2);
638 $copy->fine_level(2);
639 $copy->status(OILS_COPY_STATUS_ON_ORDER);
640 $copy->barcode($li_detail->barcode);
641 $copy->location($li_detail->location);
642 $copy->call_number($volume->id);
643 $copy->circ_lib($volume->owning_lib);
644 $copy->circ_modifier($$options{circ_modifier} || 'book');
646 $evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $volume, $copy);
647 return (undef, $evt) if $evt;
649 $li_detail->eg_copy_id($copy->id);
650 $e->update_acq_lineitem_detail($li_detail) or return (undef, $e->die_event);
653 return (scalar @{$li_details});
659 sub create_purchase_order_impl {
660 my($e, $p_order) = @_;
662 $p_order->creator($e->requestor->id);
663 $p_order->editor($e->requestor->id);
664 $p_order->owner($e->requestor->id);
665 $p_order->edit_time('now');
667 return $e->die_event unless
668 $e->allowed('CREATE_PURCHASE_ORDER', $p_order->ordering_agency);
670 my $provider = $e->retrieve_acq_provider($p_order->provider)
671 or return $e->die_event;
672 return $e->die_event unless
673 $e->allowed('MANAGE_PROVIDER', $provider->owner, $provider);
675 $e->create_acq_purchase_order($p_order) or return $e->die_event;
680 # returns (price, type), where type=1 is local, type=2 is provider, type=3 is marc
683 my $attrs = $li->attributes;
684 my ($marc_estimated, $local_estimated, $local_actual, $prov_estimated, $prov_actual);
686 for my $attr (@$attrs) {
687 if($attr->attr_name eq 'estimated_price') {
688 $local_estimated = $attr->attr_value
689 if $attr->attr_type eq 'lineitem_local_attr_definition';
690 $prov_estimated = $attr->attr_value
691 if $attr->attr_type eq 'lineitem_prov_attr_definition';
692 $marc_estimated = $attr->attr_value
693 if $attr->attr_type eq 'lineitem_marc_attr_definition';
695 } elsif($attr->attr_name eq 'actual_price') {
696 $local_actual = $attr->attr_value
697 if $attr->attr_type eq 'lineitem_local_attr_definition';
698 $prov_actual = $attr->attr_value
699 if $attr->attr_type eq 'lineitem_prov_attr_definition';
703 return ($local_actual, 1) if $local_actual;
704 return ($prov_actual, 2) if $prov_actual;
705 return ($local_estimated, 1) if $local_estimated;
706 return ($prov_estimated, 2) if $prov_estimated;
707 return ($marc_estimated, 3);
711 __PACKAGE__->register_method(
712 method => 'create_purchase_order_debits',
713 api_name => 'open-ils.acq.purchase_order.debits.create',
715 desc => 'Creates debits associated with a PO',
717 {desc => 'Authentication token', type => 'string'},
718 {desc => 'purchase_order whose debits to create', type => 'number'},
719 {desc => 'arguments hash. Options include: encumbrance=bool', type => 'object'},
721 return => {desc => 'The total amount of all created debits, Event on error'}
725 sub create_purchase_order_debits {
726 my($self, $conn, $auth, $po_id, $args) = @_;
727 my $e = new_editor(xact=>1, authtoken=>$auth);
728 return $e->die_event unless $e->checkauth;
730 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
732 my $li_ids = $e->search_acq_lineitem(
733 {purchase_order => $po_id},
737 for my $li_id (@$li_ids) {
738 my $li = $e->retrieve_acq_lineitem([
741 flesh_fields => {jub => ['attributes']},
745 my ($total, $evt) = create_li_debit_impl($e, $li);
752 sub create_li_debit_impl {
753 my($e, $li, $args) = @_;
756 my ($price, $ptype) = get_li_price($li);
760 return (undef, OpenILS::Event->new('ACQ_LINEITEM_NO_PRICE', payload => $li->id));
763 unless($li->provider) {
765 return (undef, OpenILS::Event->new('ACQ_LINEITEM_NO_PROVIDER', payload => $li->id));
768 my $lid_ids = $e->search_acq_lineitem_detail(
769 {lineitem => $li->id},
774 for my $lid_id (@$lid_ids) {
776 my $lid = $e->retrieve_acq_lineitem_detail([
779 flesh_fields => {acqlid => ['fund']}
783 my $debit = Fieldmapper::acq::fund_debit->new;
784 $debit->fund($lid->fund->id);
785 $debit->origin_amount($price);
787 if($ptype == 2) { # price from vendor
788 $debit->origin_currency_type($li->provider->currency_type);
789 $debit->amount(currency_conversion_impl(
790 $li->provider->currency_type, $lid->fund->currency_type, $price));
792 $debit->origin_currency_type($lid->fund->currency_type);
793 $debit->amount($price);
796 $debit->encumbrance($args->{encumbrance});
797 $debit->debit_type('purchase');
798 $e->create_acq_fund_debit($debit) or return (undef, $e->die_event);
800 # point the lineitem detail at the fund debit object
801 $lid->fund_debit($debit->id);
802 $lid->fund($lid->fund->id);
803 $e->update_acq_lineitem_detail($lid) or return (undef, $e->die_event);
804 $total += $debit->amount;
811 __PACKAGE__->register_method(
812 method => 'retrieve_all_user_purchase_order',
813 api_name => 'open-ils.acq.purchase_order.user.all.retrieve',
816 desc => 'Retrieves a purchase order',
818 {desc => 'Authentication token', type => 'string'},
819 {desc => 'purchase_order to retrieve', type => 'number'},
820 {desc => q/Options hash. flesh_lineitems: to get the lineitems and lineitem_attrs;
821 clear_marc: to clear the MARC data from the lineitem (for reduced bandwidth);
822 limit: number of items to return ,defaults to 50;
823 offset: offset in the list of items to return
824 order_by: sort the result, provide one or more colunm names, separated by commas,
825 optionally followed by ASC or DESC as a single string
826 li_limit : number of lineitems to return if fleshing line items;
827 li_offset : lineitem offset if fleshing line items
828 li_order_by : lineitem sort definition if fleshing line items
829 flesh_lineitem_detail_count : flesh lineitem_detail_count field
833 return => {desc => 'The purchase order, Event on failure'}
837 sub retrieve_all_user_purchase_order {
838 my($self, $conn, $auth, $options) = @_;
839 my $e = new_editor(authtoken=>$auth);
840 return $e->event unless $e->checkauth;
843 # grab purchase orders I have
844 my $perm_orgs = $U->user_has_work_perm_at($e, 'MANAGE_PROVIDER', {descendants =>1});
845 return OpenILS::Event->new('PERM_FAILURE', ilsperm => 'MANAGE_PROVIDER')
847 my $provider_ids = $e->search_acq_provider({owner => $perm_orgs}, {idlist=>1});
848 my $po_ids = $e->search_acq_purchase_order({provider => $provider_ids}, {idlist=>1});
850 # grab my purchase orders
851 push(@$po_ids, @{$e->search_acq_purchase_order({owner => $e->requestor->id}, {idlist=>1})});
853 return undef unless @$po_ids;
855 # now get the db to limit/sort for us
856 $po_ids = $e->search_acq_purchase_order(
858 limit => $$options{limit} || 50,
859 offset => $$options{offset} || 0,
860 order_by => {acqpo => $$options{order_by} || 'create_time'}
866 $conn->respond(retrieve_purchase_order_impl($e, $_, $options)) for @$po_ids;
871 __PACKAGE__->register_method(
872 method => 'search_purchase_order',
873 api_name => 'open-ils.acq.purchase_order.search',
876 desc => 'Search for a purchase order',
878 {desc => 'Authentication token', type => 'string'},
879 {desc => q/Search hash. Search fields include id, provider/, type => 'hash'}
881 return => {desc => 'A stream of POs'}
885 sub search_purchase_order {
886 my($self, $conn, $auth, $search, $options) = @_;
887 my $e = new_editor(authtoken=>$auth);
888 return $e->event unless $e->checkauth;
889 my $po_ids = $e->search_acq_purchase_order($search, {idlist=>1});
890 for my $po_id (@$po_ids) {
891 $conn->respond($e->retrieve_acq_purchase_order($po_id))
892 unless po_perm_failure($e, $po_id);
900 __PACKAGE__->register_method(
901 method => 'retrieve_purchase_order',
902 api_name => 'open-ils.acq.purchase_order.retrieve',
905 desc => 'Retrieves a purchase order',
907 {desc => 'Authentication token', type => 'string'},
908 {desc => 'purchase_order to retrieve', type => 'number'},
909 {desc => q/Options hash. flesh_lineitems, to get the lineitems and lineitem_attrs;
910 clear_marc, to clear the MARC data from the lineitem (for reduced bandwidth)
911 li_limit : number of lineitems to return if fleshing line items;
912 li_offset : lineitem offset if fleshing line items
913 li_order_by : lineitem sort definition if fleshing line items
917 return => {desc => 'The purchase order, Event on failure'}
921 sub retrieve_purchase_order {
922 my($self, $conn, $auth, $po_id, $options) = @_;
923 my $e = new_editor(authtoken=>$auth);
924 return $e->event unless $e->checkauth;
926 $po_id = [ $po_id ] unless ref $po_id;
929 if ( po_perm_failure($e, $_) )
932 { $rv = retrieve_purchase_order_impl($e, $_, $options) }
939 # if the user does not have permission to perform actions on this PO, return the perm failure event
940 sub po_perm_failure {
941 my($e, $po_id, $fund_id) = @_;
942 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
943 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency, $po);
947 sub retrieve_purchase_order_impl {
948 my($e, $po_id, $options) = @_;
951 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
953 if($$options{flesh_lineitems}) {
955 my $items = $e->search_acq_lineitem([
956 {purchase_order => $po_id},
960 jub => ['attributes']
962 limit => $$options{li_limit} || 50,
963 offset => $$options{li_offset} || 0,
964 order_by => {jub => $$options{li_order_by} || 'create_time'}
968 if($$options{clear_marc}) {
969 $_->clear_marc for @$items;
972 $po->lineitems($items);
973 $po->lineitem_count(scalar(@$items));
975 } elsif( $$options{flesh_lineitem_count} ) {
977 my $items = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist=>1});
978 $po->lineitem_count(scalar(@$items));
981 if($$options{flesh_price_summary}) {
983 # fetch the fund debits for this purchase order
984 my $debits = $e->json_query({
985 select => {acqfdeb => ["encumbrance", "amount"]},
988 jub => {fkey => "lineitem", field => "id",
989 join => {acqpo => {fkey => "purchase_order", field => "id"}}
991 acqfdeb => {fkey => "fund_debit", field =>"id"}
994 where => {'+acqpo' => {id => $po_id}}
999 for my $deb (@$debits) {
1000 if($U->is_true($deb->{encumbrance})) {
1001 $enc += $deb->{amount};
1003 $spent += $deb->{amount};
1007 $po->amount_encumbered($enc);
1008 $po->amount_spent($spent);
1015 __PACKAGE__->register_method(
1016 method => 'format_po',
1017 api_name => 'open-ils.acq.purchase_order.format'
1021 my($self, $conn, $auth, $po_id, $format) = @_;
1022 my $e = new_editor(authtoken=>$auth);
1023 return $e->event unless $e->checkauth;
1025 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
1026 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1028 my $hook = "format.po.$format";
1029 return $U->fire_object_event(undef, $hook, $po, $po->ordering_agency);
1032 __PACKAGE__->register_method (
1033 method => 'po_events',
1034 api_name => 'open-ils.acq.purchase_order.events.owner',
1037 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1038 @param authtoken Login session key
1039 @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.
1040 @param options Object for tweaking the selection criteria and fleshing options.
1044 __PACKAGE__->register_method (
1045 method => 'po_events',
1046 api_name => 'open-ils.acq.purchase_order.events.ordering_agency',
1049 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1050 @param authtoken Login session key
1051 @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.
1052 @param options Object for tweaking the selection criteria and fleshing options.
1056 __PACKAGE__->register_method (
1057 method => 'po_events',
1058 api_name => 'open-ils.acq.purchase_order.events.id',
1061 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1062 @param authtoken Login session key
1063 @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.
1064 @param options Object for tweaking the selection criteria and fleshing options.
1069 my($self, $conn, $auth, $search_value, $options) = @_;
1070 my $e = new_editor(authtoken => $auth);
1071 return $e->event unless $e->checkauth;
1073 (my $search_field = $self->api_name) =~ s/.*\.([_a-z]+)$/$1/;
1074 my $obj_type = 'acqpo';
1077 "select"=>{"atev"=>["id"]},
1082 "select"=>{$obj_type=>["id"]},
1084 "where"=>{$search_field=>$search_value}
1089 "select"=>{atevdef=>["id"]},
1092 "hook"=>"format.po.jedi"
1100 if (defined $options->{state}) {
1101 $query->{'where'}{'state'} = $options->{state}
1104 if (defined $options->{start_time}) {
1105 $query->{'where'}{'start_time'} = $options->{start_time};
1108 my $po_events = $e->json_query($query);
1110 my $flesh_fields = $options->{flesh_fields} || {};
1111 my $flesh_depth = $options->{flesh_depth} || 1;
1112 $flesh_fields->{atev} = ['event_def'] unless $flesh_fields->{atev};
1114 for my $id (@$po_events) {
1115 my $event = $e->retrieve_action_trigger_event([
1117 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
1119 if (! $event) { next; }
1121 my $po = retrieve_purchase_order_impl(
1124 {flesh_lineitem_count=>1,flesh_price_summary=>1}
1127 if ($e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() )) {
1128 $event->target( $po );
1129 $conn->respond($event);
1136 __PACKAGE__->register_method (
1137 method => 'update_po_events',
1138 api_name => 'open-ils.acq.purchase_order.event.cancel.batch',
1141 __PACKAGE__->register_method (
1142 method => 'update_po_events',
1143 api_name => 'open-ils.acq.purchase_order.event.reset.batch',
1147 sub update_po_events {
1148 my($self, $conn, $auth, $event_ids) = @_;
1149 my $e = new_editor(xact => 1, authtoken => $auth);
1150 return $e->die_event unless $e->checkauth;
1153 for my $id (@$event_ids) {
1155 # do a little dance to determine what libraries we are ultimately affecting
1156 my $event = $e->retrieve_action_trigger_event([
1159 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
1161 ]) or return $e->die_event;
1163 my $po = retrieve_purchase_order_impl(
1169 return $e->die_event unless $e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() );
1171 if($self->api_name =~ /cancel/) {
1172 $event->state('invalid');
1173 } elsif($self->api_name =~ /reset/) {
1174 $event->clear_start_time;
1175 $event->clear_update_time;
1176 $event->state('pending');
1179 $e->update_action_trigger_event($event) or return $e->die_event;
1180 $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
1184 return {complete => 1};
1188 __PACKAGE__->register_method (
1189 method => 'process_fiscal_rollover',
1190 api_name => 'open-ils.acq.fiscal_rollover.combined',
1194 Performs a combined fiscal fund rollover process.
1196 Creates a new series of funds for the following year, copying the old years
1197 funds that are marked as propagable. They apply to the funds belonging to
1198 either an org unit or to an org unit and all of its dependent org units.
1199 The procedures may be run repeatedly; if any fund has already been propagated,
1200 both the old and the new funds will be left alone.
1202 Closes out any applicable funds (by org unit or by org unit and dependents)
1203 that are marked as propagable. If such a fund has not already been propagated
1204 to the new year, it will be propagated at closing time.
1206 If a fund is marked as subject to rollover, any unspent balance in the old year's
1207 fund (including money encumbered but not spent) is transferred to the new year's
1208 fund. Otherwise it is deallocated back to the funding source(s).
1210 In either case, any encumbrance debits are transferred to the new fund, along
1211 with the corresponding lineitem details. The old year's fund is marked as inactive
1212 so that new debits may not be charged to it.
1215 {desc => 'Authentication token', type => 'string'},
1216 {desc => 'Fund Year to roll over', type => 'integer'},
1217 {desc => 'Org unit ID', type => 'integer'},
1218 {desc => 'Include Descendant Orgs (boolean)', type => 'integer'},
1220 return => {desc => 'Returns a stream of all related funds for the next year including fund summary for each'}
1225 __PACKAGE__->register_method (
1226 method => 'process_fiscal_rollover',
1227 api_name => 'open-ils.acq.fiscal_rollover.combined.dry_run',
1231 @see open-ils.acq.fiscal_rollover.combined
1232 This is the dry-run version. The action is performed,
1233 new fund information is returned, then all changes are rolled back.
1239 __PACKAGE__->register_method (
1240 method => 'process_fiscal_rollover',
1241 api_name => 'open-ils.acq.fiscal_rollover.propagate',
1245 @see open-ils.acq.fiscal_rollover.combined
1246 This version performs fund propagation only. I.e, creation of
1247 the following year's funds. It does not rollover over balances, encumbrances,
1248 or mark the previous year's funds as complete.
1253 __PACKAGE__->register_method (
1254 method => 'process_fiscal_rollover',
1255 api_name => 'open-ils.acq.fiscal_rollover.propagate.dry_run',
1257 signature => { desc => q/
1258 @see open-ils.acq.fiscal_rollover.propagate
1259 This is the dry-run version. The action is performed,
1260 new fund information is returned, then all changes are rolled back.
1266 sub process_fiscal_rollover {
1267 my( $self, $conn, $auth, $year, $org_id, $descendants ) = @_;
1269 my $e = new_editor(xact=>1, authtoken=>$auth);
1270 return $e->die_event unless $e->checkauth;
1271 return $e->die_event unless $e->allowed('ADMIN_FUND', $org_id);
1273 my $org_ids = ($descendants) ?
1276 { $_->{id} } # fetch my descendants
1277 @{$e->json_query({from => ['actor.org_unit_descendants', $org_id]})}
1281 # Create next year's funds
1282 # Note, it's safe to run this more than once.
1283 # IOW, it will not create duplicate new funds.
1287 'acq.propagate_funds_by_org_tree' :
1288 'acq.propagate_funds_by_org_unit',
1289 $year, $e->requestor->id, $org_id
1293 if($self->api_name =~ /combined/) {
1295 # Roll the uncumbrances over to next year's funds
1296 # Mark the funds for $year as inactive
1301 'acq.rollover_funds_by_org_tree' :
1302 'acq.rollover_funds_by_org_unit',
1303 $year, $e->requestor->id, $org_id
1308 # Fetch all funds for the specified org units for return to call w/ summary
1309 my $fund_ids = $e->search_acq_fund({year => int($year) + 1, org => $org_ids});
1311 foreach (@$fund_ids) {
1312 my $fund = $e->retrieve_acq_fund($_) or return $e->die_event;
1313 $fund->summary(retrieve_fund_summary_impl($e, $fund));
1314 $conn->respond($fund);
1317 $self->api_name =~ /dry_run/ and $e->rollback or $e->commit;