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',
72 desc => 'Retrieves a new funding_source',
74 {desc => 'Authentication token', type => 'string'},
75 {desc => 'funding source ID', type => 'number'}
77 return => {desc => 'The funding_source object on success, Event on failure'}
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;
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};
91 my $funding_source = $e->retrieve_acq_funding_source([$funding_source_id, $flesh]) or return $e->event;
93 return $e->event unless $e->allowed(
94 ['ADMIN_FUNDING_SOURCE','MANAGE_FUNDING_SOURCE', 'VIEW_FUNDING_SOURCE'],
95 $funding_source->owner, $funding_source);
97 $funding_source->summary(retrieve_funding_source_summary_impl($e, $funding_source))
98 if $$options{flesh_summary};
99 return $funding_source;
102 __PACKAGE__->register_method(
103 method => 'retrieve_org_funding_sources',
104 api_name => 'open-ils.acq.funding_source.org.retrieve',
107 desc => 'Retrieves all the funding_sources associated with an org unit that the requestor has access to see',
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'},
116 return => {desc => 'The funding_source objects on success, empty array otherwise'}
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;
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/;
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});
133 return [] unless @$org_ids;
134 my $sources = $e->search_acq_funding_source({owner => $org_ids});
136 for my $source (@$sources) {
137 $source->summary(retrieve_funding_source_summary_impl($e, $source))
138 if $$options{flesh_summary};
139 $conn->respond($source);
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];
151 allocation_total => ($at) ? $at->amount : 0,
152 balance => ($b) ? $b->amount : 0,
153 credit_total => ($ct) ? $ct->amount : 0,
158 __PACKAGE__->register_method(
159 method => 'create_funding_source_credit',
160 api_name => 'open-ils.acq.funding_source_credit.create',
162 desc => 'Create a new funding source credit',
164 {desc => 'Authentication token', type => 'string'},
165 {desc => 'funding source credit object', type => 'object'}
167 return => {desc => 'The ID of the new funding source credit on success, Event on failure'}
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;
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);
180 $e->create_acq_funding_source_credit($fs_credit) or return $e->die_event;
182 return $fs_credit->id;
186 # ---------------------------------------------------------------
188 # ---------------------------------------------------------------
190 __PACKAGE__->register_method(
191 method => 'create_fund',
192 api_name => 'open-ils.acq.fund.create',
194 desc => 'Creates a new fund',
196 {desc => 'Authentication token', type => 'string'},
197 {desc => 'fund object to create', type => 'object'}
199 return => {desc => 'The ID of the newly created fund object'}
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;
214 __PACKAGE__->register_method(
215 method => 'delete_fund',
216 api_name => 'open-ils.acq.fund.delete',
218 desc => 'Deletes a fund',
220 {desc => 'Authentication token', type => 'string'},
221 {desc => 'fund ID', type => 'number'}
223 return => {desc => '1 on success, Event on failure'}
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;
238 __PACKAGE__->register_method(
239 method => 'retrieve_fund',
240 api_name => 'open-ils.acq.fund.retrieve',
243 desc => 'Retrieves a new fund',
245 {desc => 'Authentication token', type => 'string'},
246 {desc => 'fund ID', type => 'number'}
248 return => {desc => 'The fund object on success, Event on failure'}
253 my($self, $conn, $auth, $fund_id, $options) = @_;
254 my $e = new_editor(authtoken=>$auth);
255 return $e->event unless $e->checkauth;
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"];
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};
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};
274 __PACKAGE__->register_method(
275 method => 'retrieve_org_funds',
276 api_name => 'open-ils.acq.fund.org.retrieve',
279 desc => 'Retrieves all the funds associated with an org unit',
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'},
290 return => {desc => 'The fund objects on success, Event on failure'}
294 __PACKAGE__->register_method(
295 method => 'retrieve_org_funds',
296 api_name => 'open-ils.acq.fund.org.years.retrieve');
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;
306 my $limit_perm = ($$options{limit_perm}) ? $$options{limit_perm} : 'ADMIN_FUND';
307 return OpenILS::Event->new('BAD_PARAMS')
308 unless $limit_perm =~ /(ADMIN|MANAGE|VIEW)_(ACQ_)?FUND/;
310 $filter->{org} = $filter->{org} ||
311 $U->user_has_work_perm_at($e, $limit_perm, {descendants =>1});
312 return undef unless @{$filter->{org}};
317 limit => $$options{limit} || 50,
318 offset => $$options{offset} || 0,
319 order_by => $$options{order_by} || {acqf => 'name'}
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({
327 acqf => [{column => 'year', transform => 'distinct'}]
332 acqf => {"year" => {"direction" => "desc"}}
336 return [map { $_->{year} } @$data];
339 my $funds = $e->search_acq_fund($query);
341 for my $fund (@$funds) {
342 $fund->summary(retrieve_fund_summary_impl($e, $fund))
343 if $$options{flesh_summary};
344 $conn->respond($fund);
350 __PACKAGE__->register_method(
351 method => 'retrieve_fund_summary',
352 api_name => 'open-ils.acq.fund.summary.retrieve',
355 desc => 'Returns a summary of credits/debits/encumbrances for a fund',
357 {desc => 'Authentication token', type => 'string'},
358 {desc => 'fund id', type => 'number' }
360 return => {desc => 'A hash of summary information, Event on failure'}
364 sub retrieve_fund_summary {
365 my($self, $conn, $auth, $fund_id) = @_;
366 my $e = new_editor(authtoken=>$auth);
367 return $e->event unless $e->checkauth;
368 my $fund = $e->retrieve_acq_fund($fund_id) or return $e->event;
369 return $e->event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
370 return retrieve_fund_summary_impl($e, $fund);
374 sub retrieve_fund_summary_impl {
377 my $at = $e->search_acq_fund_allocation_total({fund => $fund->id})->[0];
378 my $dt = $e->search_acq_fund_debit_total({fund => $fund->id})->[0];
379 my $et = $e->search_acq_fund_encumbrance_total({fund => $fund->id})->[0];
380 my $st = $e->search_acq_fund_spent_total({fund => $fund->id})->[0];
381 my $cb = $e->search_acq_fund_combined_balance({fund => $fund->id})->[0];
382 my $sb = $e->search_acq_fund_spent_balance({fund => $fund->id})->[0];
385 allocation_total => ($at) ? $at->amount : 0,
386 debit_total => ($dt) ? $dt->amount : 0,
387 encumbrance_total => ($et) ? $et->amount : 0,
388 spent_total => ($st) ? $st->amount : 0,
389 combined_balance => ($cb) ? $cb->amount : 0,
390 spent_balance => ($sb) ? $sb->amount : 0,
394 __PACKAGE__->register_method(
395 method => 'transfer_money_between_funds',
396 api_name => 'open-ils.acq.funds.transfer_money',
398 desc => 'Method for transfering money between funds',
400 {desc => 'Authentication token', type => 'string'},
401 {desc => 'Originating fund ID', type => 'number'},
402 {desc => 'Amount of money to transfer away from the originating fund, in the same currency as said fund', type => 'number'},
403 {desc => 'Destination fund ID', type => 'number'},
404 {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'},
405 {desc => 'Transfer Note', type => 'string'}
407 return => {desc => '1 on success, Event on failure'}
411 sub transfer_money_between_funds {
412 my($self, $conn, $auth, $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $note) = @_;
413 my $e = new_editor(xact=>1, authtoken=>$auth);
414 return $e->die_event unless $e->checkauth;
415 my $ofund = $e->retrieve_acq_fund($ofund_id) or return $e->event;
416 return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $ofund->org, $ofund);
417 my $dfund = $e->retrieve_acq_fund($dfund_id) or return $e->event;
418 return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $dfund->org, $dfund);
420 if (!defined $dfund_amount) {
422 if ($ofund->currency_type ne $dfund->currency_type) {
424 $dfund_amount = $e->json_query({
426 'acq.exchange_ratio',
427 $ofund->currency_type,
428 $dfund->currency_type,
431 })->[0]->{'acq.exchange_ratio'};
435 $dfund_amount = $ofund_amount;
439 return $e->die_event unless $e->allowed("ACQ_XFER_MANUAL_DFUND_AMOUNT");
445 $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $e->requestor->id, $note
456 # ---------------------------------------------------------------
458 # ---------------------------------------------------------------
460 __PACKAGE__->register_method(
461 method => 'create_fund_alloc',
462 api_name => 'open-ils.acq.fund_allocation.create',
464 desc => 'Creates a new fund_allocation',
466 {desc => 'Authentication token', type => 'string'},
467 {desc => 'fund allocation object to create', type => 'object'}
469 return => {desc => 'The ID of the new fund_allocation'}
473 sub create_fund_alloc {
474 my($self, $conn, $auth, $fund_alloc) = @_;
475 my $e = new_editor(xact=>1, authtoken=>$auth);
476 return $e->die_event unless $e->checkauth;
478 # this action is equivalent to both debiting a funding source and crediting a fund
480 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
481 or return $e->die_event;
482 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner);
484 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
485 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
487 $fund_alloc->allocator($e->requestor->id);
488 $e->create_acq_fund_allocation($fund_alloc) or return $e->die_event;
490 return $fund_alloc->id;
494 __PACKAGE__->register_method(
495 method => 'delete_fund_alloc',
496 api_name => 'open-ils.acq.fund_allocation.delete',
498 desc => 'Deletes a fund_allocation',
500 {desc => 'Authentication token', type => 'string'},
501 {desc => 'fund Alocation ID', type => 'number'}
503 return => {desc => '1 on success, Event on failure'}
507 sub delete_fund_alloc {
508 my($self, $conn, $auth, $fund_alloc_id) = @_;
509 my $e = new_editor(xact=>1, authtoken=>$auth);
510 return $e->die_event unless $e->checkauth;
512 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->die_event;
514 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
515 or return $e->die_event;
516 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
518 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
519 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
521 $e->delete_acq_fund_allocation($fund_alloc) or return $e->die_event;
526 __PACKAGE__->register_method(
527 method => 'retrieve_fund_alloc',
528 api_name => 'open-ils.acq.fund_allocation.retrieve',
531 desc => 'Retrieves a new fund_allocation',
533 {desc => 'Authentication token', type => 'string'},
534 {desc => 'fund Allocation ID', type => 'number'}
536 return => {desc => 'The fund allocation object on success, Event on failure'}
540 sub retrieve_fund_alloc {
541 my($self, $conn, $auth, $fund_alloc_id) = @_;
542 my $e = new_editor(authtoken=>$auth);
543 return $e->event unless $e->checkauth;
544 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
546 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
547 or return $e->die_event;
548 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
550 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
551 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
557 __PACKAGE__->register_method(
558 method => 'retrieve_funding_source_allocations',
559 api_name => 'open-ils.acq.funding_source.allocations.retrieve',
562 desc => 'Retrieves a new fund_allocation',
564 {desc => 'Authentication token', type => 'string'},
565 {desc => 'fund Allocation ID', type => 'number'}
567 return => {desc => 'The fund allocation object on success, Event on failure'}
571 sub retrieve_funding_source_allocations {
572 my($self, $conn, $auth, $fund_alloc_id) = @_;
573 my $e = new_editor(authtoken=>$auth);
574 return $e->event unless $e->checkauth;
575 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
577 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
578 or return $e->die_event;
579 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
581 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
582 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
587 # ----------------------------------------------------------------------------
589 # ----------------------------------------------------------------------------
591 __PACKAGE__->register_method(
592 method => 'retrieve_all_currency_type',
593 api_name => 'open-ils.acq.currency_type.all.retrieve',
596 desc => 'Retrieves all currency_type objects',
598 {desc => 'Authentication token', type => 'string'},
600 return => {desc => 'List of currency_type objects', type => 'list'}
604 sub retrieve_all_currency_type {
605 my($self, $conn, $auth, $fund_alloc_id) = @_;
606 my $e = new_editor(authtoken=>$auth);
607 return $e->event unless $e->checkauth;
608 return $e->event unless $e->allowed('GENERAL_ACQ');
609 $conn->respond($_) for @{$e->retrieve_all_acq_currency_type()};
612 __PACKAGE__->register_method(
613 method => 'create_lineitem_assets',
614 api_name => 'open-ils.acq.lineitem.assets.create',
616 desc => q/Creates the bibliographic data, volume, and copies associated with a lineitem./,
618 {desc => 'Authentication token', type => 'string'},
619 {desc => 'The lineitem id', type => 'number'},
620 {desc => q/Options hash./}
622 return => {desc => 'ID of newly created bib record, Event on error'}
626 sub create_lineitem_assets {
627 my($self, $conn, $auth, $li_id, $options) = @_;
628 my $e = new_editor(authtoken=>$auth, xact=>1);
629 return $e->die_event unless $e->checkauth;
630 my ($count, $resp) = create_lineitem_assets_impl($e, $li_id, $options);
631 return $resp if $resp;
636 sub create_lineitem_assets_impl {
637 my($e, $li_id, $options) = @_;
641 my $li = $e->retrieve_acq_lineitem([
644 flesh_fields => {jub => ['purchase_order', 'attributes']}
646 ]) or return (undef, $e->die_event);
648 # -----------------------------------------------------------------
649 # first, create the bib record if necessary
650 # -----------------------------------------------------------------
651 unless($li->eg_bib_id) {
653 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
654 $e, $li->marc); #$rec->bib_source
656 if($U->event_code($record)) {
658 return (undef, $record);
661 $li->editor($e->requestor->id);
662 $li->edit_time('now');
663 $li->eg_bib_id($record->id);
664 $e->update_acq_lineitem($li) or return (undef, $e->die_event);
667 my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
669 # -----------------------------------------------------------------
670 # for each lineitem_detail, create the volume if necessary, create
671 # a copy, and link them all together.
672 # -----------------------------------------------------------------
674 for my $li_detail_id (@{$li_details}) {
676 my $li_detail = $e->retrieve_acq_lineitem_detail($li_detail_id)
677 or return (undef, $e->die_event);
679 # Create the volume object if necessary
680 my $volume = $volcache{$li_detail->cn_label};
681 unless($volume and $volume->owning_lib == $li_detail->owning_lib) {
683 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
684 $e, $li_detail->cn_label, $li->eg_bib_id, $li_detail->owning_lib);
685 return (undef, $evt) if $evt;
686 $volcache{$volume->id} = $volume;
689 my $copy = Fieldmapper::asset::copy->new;
691 $copy->loan_duration(2);
692 $copy->fine_level(2);
693 $copy->status(OILS_COPY_STATUS_ON_ORDER);
694 $copy->barcode($li_detail->barcode);
695 $copy->location($li_detail->location);
696 $copy->call_number($volume->id);
697 $copy->circ_lib($volume->owning_lib);
698 $copy->circ_modifier($$options{circ_modifier} || 'book');
700 $evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $volume, $copy);
701 return (undef, $evt) if $evt;
703 $li_detail->eg_copy_id($copy->id);
704 $e->update_acq_lineitem_detail($li_detail) or return (undef, $e->die_event);
707 return (scalar @{$li_details});
713 sub create_purchase_order_impl {
714 my($e, $p_order) = @_;
716 $p_order->creator($e->requestor->id);
717 $p_order->editor($e->requestor->id);
718 $p_order->owner($e->requestor->id);
719 $p_order->edit_time('now');
721 return $e->die_event unless
722 $e->allowed('CREATE_PURCHASE_ORDER', $p_order->ordering_agency);
724 my $provider = $e->retrieve_acq_provider($p_order->provider)
725 or return $e->die_event;
726 return $e->die_event unless
727 $e->allowed('MANAGE_PROVIDER', $provider->owner, $provider);
729 $e->create_acq_purchase_order($p_order) or return $e->die_event;
734 __PACKAGE__->register_method(
735 method => 'retrieve_all_user_purchase_order',
736 api_name => 'open-ils.acq.purchase_order.user.all.retrieve',
739 desc => 'Retrieves a purchase order',
741 {desc => 'Authentication token', type => 'string'},
742 {desc => 'purchase_order to retrieve', type => 'number'},
743 {desc => q/Options hash. flesh_lineitems: to get the lineitems and lineitem_attrs;
744 clear_marc: to clear the MARC data from the lineitem (for reduced bandwidth);
745 limit: number of items to return ,defaults to 50;
746 offset: offset in the list of items to return
747 order_by: sort the result, provide one or more colunm names, separated by commas,
748 optionally followed by ASC or DESC as a single string
749 li_limit : number of lineitems to return if fleshing line items;
750 li_offset : lineitem offset if fleshing line items
751 li_order_by : lineitem sort definition if fleshing line items
752 flesh_lineitem_detail_count : flesh lineitem_detail_count field
756 return => {desc => 'The purchase order, Event on failure'}
760 sub retrieve_all_user_purchase_order {
761 my($self, $conn, $auth, $options) = @_;
762 my $e = new_editor(authtoken=>$auth);
763 return $e->event unless $e->checkauth;
766 # grab purchase orders I have
767 my $perm_orgs = $U->user_has_work_perm_at($e, 'MANAGE_PROVIDER', {descendants =>1});
768 return OpenILS::Event->new('PERM_FAILURE', ilsperm => 'MANAGE_PROVIDER')
770 my $provider_ids = $e->search_acq_provider({owner => $perm_orgs}, {idlist=>1});
771 my $po_ids = $e->search_acq_purchase_order({provider => $provider_ids}, {idlist=>1});
773 # grab my purchase orders
774 push(@$po_ids, @{$e->search_acq_purchase_order({owner => $e->requestor->id}, {idlist=>1})});
776 return undef unless @$po_ids;
778 # now get the db to limit/sort for us
779 $po_ids = $e->search_acq_purchase_order(
781 limit => $$options{limit} || 50,
782 offset => $$options{offset} || 0,
783 order_by => {acqpo => $$options{order_by} || 'create_time'}
789 $conn->respond(retrieve_purchase_order_impl($e, $_, $options)) for @$po_ids;
794 __PACKAGE__->register_method(
795 method => 'search_purchase_order',
796 api_name => 'open-ils.acq.purchase_order.search',
799 desc => 'Search for a purchase order',
801 {desc => 'Authentication token', type => 'string'},
802 {desc => q/Search hash. Search fields include id, provider/, type => 'hash'}
804 return => {desc => 'A stream of POs'}
808 sub search_purchase_order {
809 my($self, $conn, $auth, $search, $options) = @_;
810 my $e = new_editor(authtoken=>$auth);
811 return $e->event unless $e->checkauth;
812 my $po_ids = $e->search_acq_purchase_order($search, {idlist=>1});
813 for my $po_id (@$po_ids) {
814 $conn->respond($e->retrieve_acq_purchase_order($po_id))
815 unless po_perm_failure($e, $po_id);
823 __PACKAGE__->register_method(
824 method => 'retrieve_purchase_order',
825 api_name => 'open-ils.acq.purchase_order.retrieve',
829 desc => 'Retrieves a purchase order',
831 {desc => 'Authentication token', type => 'string'},
832 {desc => 'purchase_order to retrieve', type => 'number'},
833 {desc => q/Options hash. flesh_lineitems, to get the lineitems and lineitem_attrs;
834 clear_marc, to clear the MARC data from the lineitem (for reduced bandwidth)
835 li_limit : number of lineitems to return if fleshing line items;
836 li_offset : lineitem offset if fleshing line items
837 li_order_by : lineitem sort definition if fleshing line items,
838 flesh_po_items : po_item objects
842 return => {desc => 'The purchase order, Event on failure'}
846 sub retrieve_purchase_order {
847 my($self, $conn, $auth, $po_id, $options) = @_;
848 my $e = new_editor(authtoken=>$auth);
849 return $e->event unless $e->checkauth;
851 $po_id = [ $po_id ] unless ref $po_id;
854 if ( po_perm_failure($e, $_) )
857 { $rv = retrieve_purchase_order_impl($e, $_, $options) }
866 # if the user does not have permission to perform actions on this PO, return the perm failure event
867 sub po_perm_failure {
868 my($e, $po_id, $fund_id) = @_;
869 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
870 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency, $po);
874 sub build_price_summary {
875 my ($e, $po_id) = @_;
877 # amounts for lineitems / lineitem_details
878 my $li_data = $e->json_query({
881 'estimated_unit_price',
882 {column => 'id', alias => 'li_id'}
885 # lineitem_detail.id is needed to ensure we have one
886 # "row" of data for every copy, regardless of whether
887 # a fund_debit exists for each copy.
888 {column => 'id', alias => 'lid_id'}
892 {column => 'amount', alias => 'debit_amount'}
903 fkey => 'fund_debit',
910 where => {'+jub' => {purchase_order => $po_id}}
913 # amounts for po_item's
914 my $item_data = $e->json_query({
916 acqpoi => ['estimated_cost'],
919 {column => 'amount', alias => 'debit_amount'}
926 fkey => 'fund_debit',
931 where => {'+acqpoi' => {purchase_order => $po_id}}
934 # debits for invoice items linked to "blanket" po_items are
935 # considered part of the PO. We are not duplicating debits
936 # here with po_item debits, because blanket po_item debits
937 # plus related invoice_item debits are cumulitive.
938 my $inv_data = $e->json_query({
942 {column => 'id', alias => 'item_id'}
947 {column => 'amount', alias => 'debit_amount'}
954 fkey => 'fund_debit',
961 '+acqii' => {purchase_order => $po_id},
962 '+aiit' => {blanket => 't'}
966 # sum amounts debited (for activated PO's) and amounts estimated
967 # (for pending PO's) for all lineitem_details and po_items.
969 my ($enc, $spent, $estimated) = (0, 0, 0);
971 for my $deb (@$li_data, @$item_data, @$inv_data) {
973 if (defined $deb->{debit_amount}) { # could be $0
974 # we have a debit, treat it as authoritative.
976 # estimated amount includes all amounts encumbered or spent
977 $estimated += $deb->{debit_amount};
979 if($U->is_true($deb->{encumbrance})) {
980 $enc += $deb->{debit_amount};
982 $spent += $deb->{debit_amount};
986 # PO is not activated, so sum estimated costs.
987 # There will be one $deb object for every lineitem_detail
988 # and po_item. Adding the estimated costs for all gives
989 # us the total esimated amount.
992 $deb->{estimated_unit_price} ||
993 $deb->{estimated_cost} ||
994 $deb->{amount_paid} || 0
999 return ($enc, $spent, $estimated);
1003 sub retrieve_purchase_order_impl {
1004 my($e, $po_id, $options) = @_;
1006 my $flesh = {"flesh" => 1, "flesh_fields" => {"acqpo" => []}};
1009 unless ($options->{"no_flesh_cancel_reason"}) {
1010 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "cancel_reason";
1012 if ($options->{"flesh_notes"}) {
1013 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
1015 if ($options->{"flesh_provider"}) {
1016 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider";
1019 push (@{$flesh->{flesh_fields}->{acqpo}}, 'po_items') if $options->{flesh_po_items};
1021 my $args = (@{$flesh->{"flesh_fields"}->{"acqpo"}}) ?
1022 [$po_id, $flesh] : $po_id;
1024 my $po = $e->retrieve_acq_purchase_order($args)
1025 or return $e->event;
1027 if($$options{flesh_lineitems}) {
1029 my $flesh_fields = { jub => ['attributes'] };
1030 $flesh_fields->{jub}->[1] = 'lineitem_details' if $$options{flesh_lineitem_details};
1031 $flesh_fields->{acqlid} = ['fund_debit'] if $$options{flesh_fund_debit};
1033 my $items = $e->search_acq_lineitem([
1034 {purchase_order => $po_id},
1037 flesh_fields => $flesh_fields,
1038 limit => $$options{li_limit} || 50,
1039 offset => $$options{li_offset} || 0,
1040 order_by => {jub => $$options{li_order_by} || 'create_time'}
1044 if($$options{clear_marc}) {
1045 $_->clear_marc for @$items;
1048 $po->lineitems($items);
1049 $po->lineitem_count(scalar(@$items));
1051 } elsif( $$options{flesh_lineitem_ids} ) {
1052 $po->lineitems($e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1}));
1054 } elsif( $$options{flesh_lineitem_count} ) {
1056 my $items = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist=>1});
1057 $po->lineitem_count(scalar(@$items));
1060 if($$options{flesh_price_summary}) {
1061 my ($enc, $spent, $estimated) = build_price_summary($e, $po_id);
1062 $po->amount_encumbered($enc);
1063 $po->amount_spent($spent);
1064 $po->amount_estimated($estimated);
1071 __PACKAGE__->register_method(
1072 method => 'format_po',
1073 api_name => 'open-ils.acq.purchase_order.format'
1077 my($self, $conn, $auth, $po_id, $format) = @_;
1078 my $e = new_editor(authtoken=>$auth);
1079 return $e->event unless $e->checkauth;
1081 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
1082 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1084 my $hook = "format.po.$format";
1085 return $U->fire_object_event(undef, $hook, $po, $po->ordering_agency);
1088 __PACKAGE__->register_method(
1089 method => 'format_lineitem',
1090 api_name => 'open-ils.acq.lineitem.format'
1093 sub format_lineitem {
1094 my($self, $conn, $auth, $li_id, $format, $user_data) = @_;
1095 my $e = new_editor(authtoken=>$auth);
1096 return $e->event unless $e->checkauth;
1098 my $li = $e->retrieve_acq_lineitem($li_id) or return $e->event;
1101 if (defined $li->purchase_order) {
1102 my $po = $e->retrieve_acq_purchase_order($li->purchase_order) or return $e->die_event;
1103 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1104 $context_org = $po->ordering_agency;
1106 my $pl = $e->retrieve_acq_picklist($li->picklist) or return $e->die_event;
1107 if($e->requestor->id != $pl->owner) {
1108 return $e->event unless
1109 $e->allowed('VIEW_PICKLIST', $pl->org_unit, $pl);
1111 $context_org = $pl->org_unit;
1114 my $hook = "format.acqli.$format";
1115 return $U->fire_object_event(undef, $hook, $li, $context_org, 'print-on-demand', $user_data);
1118 __PACKAGE__->register_method (
1119 method => 'po_events',
1120 api_name => 'open-ils.acq.purchase_order.events.owner',
1123 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1124 @param authtoken Login session key
1125 @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.
1126 @param options Object for tweaking the selection criteria and fleshing options.
1130 __PACKAGE__->register_method (
1131 method => 'po_events',
1132 api_name => 'open-ils.acq.purchase_order.events.ordering_agency',
1135 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1136 @param authtoken Login session key
1137 @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.
1138 @param options Object for tweaking the selection criteria and fleshing options.
1142 __PACKAGE__->register_method (
1143 method => 'po_events',
1144 api_name => 'open-ils.acq.purchase_order.events.id',
1147 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1148 @param authtoken Login session key
1149 @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.
1150 @param options Object for tweaking the selection criteria and fleshing options.
1155 my($self, $conn, $auth, $search_value, $options) = @_;
1156 my $e = new_editor(authtoken => $auth);
1157 return $e->event unless $e->checkauth;
1159 (my $search_field = $self->api_name) =~ s/.*\.([_a-z]+)$/$1/;
1160 my $obj_type = 'acqpo';
1162 if ($search_field eq 'ordering_agency') {
1163 $search_value = $U->get_org_descendants($search_value);
1167 "select"=>{"atev"=>["id"]},
1172 "select"=>{$obj_type=>["id"]},
1174 "where"=>{$search_field=>$search_value}
1179 "select"=>{atevdef=>["id"]},
1182 "hook"=>"format.po.jedi"
1188 "order_by"=>[{"class"=>"atev", "field"=>"run_time", "direction"=>"desc"}]
1191 if ($options && defined $options->{state}) {
1192 $query->{'where'}{'state'} = $options->{state}
1195 if ($options && defined $options->{start_time}) {
1196 $query->{'where'}{'start_time'} = $options->{start_time};
1199 if ($options && defined $options->{order_by}) {
1200 $query->{'order_by'} = $options->{order_by};
1202 my $po_events = $e->json_query($query);
1204 my $flesh_fields = { 'atev' => [ 'event_def' ] };
1205 my $flesh_depth = 1;
1207 for my $id (@$po_events) {
1208 my $event = $e->retrieve_action_trigger_event([
1210 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
1212 if (! $event) { next; }
1214 my $po = retrieve_purchase_order_impl(
1217 {flesh_lineitem_count=>1,flesh_price_summary=>1}
1220 if ($e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() )) {
1221 $event->target( $po );
1222 $conn->respond($event);
1229 __PACKAGE__->register_method (
1230 method => 'update_po_events',
1231 api_name => 'open-ils.acq.purchase_order.event.cancel.batch',
1234 __PACKAGE__->register_method (
1235 method => 'update_po_events',
1236 api_name => 'open-ils.acq.purchase_order.event.reset.batch',
1240 sub update_po_events {
1241 my($self, $conn, $auth, $event_ids) = @_;
1242 my $e = new_editor(xact => 1, authtoken => $auth);
1243 return $e->die_event unless $e->checkauth;
1246 for my $id (@$event_ids) {
1248 # do a little dance to determine what libraries we are ultimately affecting
1249 my $event = $e->retrieve_action_trigger_event([
1252 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
1254 ]) or return $e->die_event;
1256 my $po = retrieve_purchase_order_impl(
1262 return $e->die_event unless $e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() );
1264 if($self->api_name =~ /cancel/) {
1265 $event->state('invalid');
1266 } elsif($self->api_name =~ /reset/) {
1267 $event->clear_start_time;
1268 $event->clear_update_time;
1269 $event->state('pending');
1272 $e->update_action_trigger_event($event) or return $e->die_event;
1273 $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
1277 return {complete => 1};
1281 __PACKAGE__->register_method (
1282 method => 'process_fiscal_rollover',
1283 api_name => 'open-ils.acq.fiscal_rollover.combined',
1287 Performs a combined fiscal fund rollover process.
1289 Creates a new series of funds for the following year, copying the old years
1290 funds that are marked as propagable. They apply to the funds belonging to
1291 either an org unit or to an org unit and all of its dependent org units.
1292 The procedures may be run repeatedly; if any fund has already been propagated,
1293 both the old and the new funds will be left alone.
1295 Closes out any applicable funds (by org unit or by org unit and dependents)
1296 that are marked as propagable. If such a fund has not already been propagated
1297 to the new year, it will be propagated at closing time.
1299 If a fund is marked as subject to rollover, any unspent balance in the old year's
1300 fund (including money encumbered but not spent) is transferred to the new year's
1301 fund. Otherwise it is deallocated back to the funding source(s).
1303 In either case, any encumbrance debits are transferred to the new fund, along
1304 with the corresponding lineitem details. The old year's fund is marked as inactive
1305 so that new debits may not be charged to it.
1308 {desc => 'Authentication token', type => 'string'},
1309 {desc => 'Fund Year to roll over', type => 'integer'},
1310 {desc => 'Org unit ID', type => 'integer'},
1311 {desc => 'Include Descendant Orgs (boolean)', type => 'integer'},
1312 {desc => 'Option hash: limit, offset, encumb_only', type => 'object'},
1314 return => {desc => 'Returns a stream of all related funds for the next year including fund summary for each'}
1319 __PACKAGE__->register_method (
1320 method => 'process_fiscal_rollover',
1321 api_name => 'open-ils.acq.fiscal_rollover.combined.dry_run',
1325 @see open-ils.acq.fiscal_rollover.combined
1326 This is the dry-run version. The action is performed,
1327 new fund information is returned, then all changes are rolled back.
1333 __PACKAGE__->register_method (
1334 method => 'process_fiscal_rollover',
1335 api_name => 'open-ils.acq.fiscal_rollover.propagate',
1339 @see open-ils.acq.fiscal_rollover.combined
1340 This version performs fund propagation only. I.e, creation of
1341 the following year's funds. It does not rollover over balances, encumbrances,
1342 or mark the previous year's funds as complete.
1347 __PACKAGE__->register_method (
1348 method => 'process_fiscal_rollover',
1349 api_name => 'open-ils.acq.fiscal_rollover.propagate.dry_run',
1351 signature => { desc => q/
1352 @see open-ils.acq.fiscal_rollover.propagate
1353 This is the dry-run version. The action is performed,
1354 new fund information is returned, then all changes are rolled back.
1360 sub process_fiscal_rollover {
1361 my( $self, $conn, $auth, $year, $org_id, $descendants, $options ) = @_;
1363 my $e = new_editor(xact=>1, authtoken=>$auth);
1364 return $e->die_event unless $e->checkauth;
1365 return $e->die_event unless $e->allowed('ADMIN_FUND', $org_id);
1368 my $combined = ($self->api_name =~ /combined/);
1369 my $encumb_only = $U->is_true($options->{encumb_only}) ? 't' : 'f';
1371 my $org_ids = ($descendants) ?
1374 { $_->{id} } # fetch my descendants
1375 @{$e->json_query({from => ['actor.org_unit_descendants', $org_id]})}
1379 # Create next year's funds
1380 # Note, it's safe to run this more than once.
1381 # IOW, it will not create duplicate new funds.
1385 'acq.propagate_funds_by_org_tree' :
1386 'acq.propagate_funds_by_org_unit',
1387 $year, $e->requestor->id, $org_id
1393 # Roll the uncumbrances over to next year's funds
1394 # Mark the funds for $year as inactive
1399 'acq.rollover_funds_by_org_tree' :
1400 'acq.rollover_funds_by_org_unit',
1401 $year, $e->requestor->id, $org_id, $encumb_only
1406 # Fetch all funds for the specified org units for the subsequent year
1407 my $fund_ids = $e->search_acq_fund(
1408 [{ year => int($year) + 1,
1410 propagate => 't' }],
1414 foreach (@$fund_ids) {
1415 my $fund = $e->retrieve_acq_fund($_) or return $e->die_event;
1416 $fund->summary(retrieve_fund_summary_impl($e, $fund));
1419 if($combined and $U->is_true($fund->rollover)) {
1420 # see how much money was rolled over
1422 my $sum = $e->json_query({
1423 select => {acqftr => [{column => 'dest_amount', transform => 'sum'}]},
1425 where => {dest_fund => $fund->id, note => { like => 'Rollover%' } }
1428 $amount = $sum->{dest_amount} if $sum;
1431 $conn->respond({fund => $fund, rollover_amount => $amount});
1434 $self->api_name =~ /dry_run/ and $e->rollback or $e->commit;
1438 __PACKAGE__->register_method(
1439 method => 'org_fiscal_year',
1440 api_name => 'open-ils.acq.org_unit.current_fiscal_year',
1443 Returns the current fiscal year for the given org unit.
1444 If no fiscal year is configured, the current calendar
1448 {desc => 'Authentication token', type => 'string'},
1449 {desc => 'Org unit ID', type => 'number'}
1451 return => {desc => 'Year as a string (e.g. "2012")'}
1455 sub org_fiscal_year {
1456 my($self, $conn, $auth, $org_id) = @_;
1458 my $e = new_editor(authtoken => $auth);
1459 return $e->event unless $e->checkauth;
1461 my $year = $e->json_query({
1462 select => {acqfy => ['year']},
1463 from => {acqfy => {acqfc => {join => 'aou'}}},
1466 year_begin => {'<=' => 'now'},
1467 year_end => {'>=' => 'now'},
1469 '+aou' => {id => $org_id}
1473 return $year ? $year->{year} : DateTime->now->year;