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 if ($options->{"flesh_tags"}) {
258 push @{$flesh->{"flesh_fields"}->{"acqf"}}, "tags";
259 $flesh->{"flesh_fields"}->{"acqftm"} = ["tag"];
261 push(@{$flesh->{flesh_fields}->{acqf}}, 'debits') if $$options{flesh_debits};
262 push(@{$flesh->{flesh_fields}->{acqf}}, 'allocations') if $$options{flesh_allocations};
263 push(@{$flesh->{flesh_fields}->{acqfa}}, 'funding_source') if $$options{flesh_allocation_sources};
265 my $fund = $e->retrieve_acq_fund([$fund_id, $flesh]) or return $e->event;
266 return $e->event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND', 'VIEW_FUND'], $fund->org, $fund);
267 $fund->summary(retrieve_fund_summary_impl($e, $fund))
268 if $$options{flesh_summary};
272 __PACKAGE__->register_method(
273 method => 'retrieve_org_funds',
274 api_name => 'open-ils.acq.fund.org.retrieve',
277 desc => 'Retrieves all the funds associated with an org unit',
279 {desc => 'Authentication token', type => 'string'},
280 {desc => 'List of org Unit IDs. If no IDs are provided, this method returns the
281 full set of funding sources this user has permission to view', type => 'number'},
282 {desc => q/Options hash.
283 "limit_perm" -- this permission is used find the work-org tree from which
284 the list of orgs is generated if no org ids are provided. The default is ADMIN_FUND.
285 "flesh_summary" -- if true, the summary field on each fund is fleshed
286 The default is ADMIN_FUND/, type => 'string'},
288 return => {desc => 'The fund objects on success, Event on failure'}
292 __PACKAGE__->register_method(
293 method => 'retrieve_org_funds',
294 api_name => 'open-ils.acq.fund.org.years.retrieve');
297 sub retrieve_org_funds {
298 my($self, $conn, $auth, $filter, $options) = @_;
299 my $e = new_editor(authtoken=>$auth);
300 return $e->event unless $e->checkauth;
304 my $limit_perm = ($$options{limit_perm}) ? $$options{limit_perm} : 'ADMIN_FUND';
305 return OpenILS::Event->new('BAD_PARAMS')
306 unless $limit_perm =~ /(ADMIN|MANAGE|VIEW)_FUND/;
308 $filter->{org} = $filter->{org} ||
309 $U->user_has_work_perm_at($e, $limit_perm, {descendants =>1});
310 return undef unless @{$filter->{org}};
315 limit => $$options{limit} || 50,
316 offset => $$options{offset} || 0,
317 order_by => $$options{order_by} || {acqf => 'name'}
321 if($self->api_name =~ /years/) {
322 # return the distinct set of fund years covered by the selected funds
323 my $data = $e->json_query({
325 acqf => [{column => 'year', transform => 'distinct'}]
331 return [map { $_->{year} } @$data];
334 my $funds = $e->search_acq_fund($query);
336 for my $fund (@$funds) {
337 $fund->summary(retrieve_fund_summary_impl($e, $fund))
338 if $$options{flesh_summary};
339 $conn->respond($fund);
345 __PACKAGE__->register_method(
346 method => 'retrieve_fund_summary',
347 api_name => 'open-ils.acq.fund.summary.retrieve',
349 desc => 'Returns a summary of credits/debits/encumbrances for a fund',
351 {desc => 'Authentication token', type => 'string'},
352 {desc => 'fund id', type => 'number' }
354 return => {desc => 'A hash of summary information, Event on failure'}
358 sub retrieve_fund_summary {
359 my($self, $conn, $auth, $fund_id) = @_;
360 my $e = new_editor(authtoken=>$auth);
361 return $e->event unless $e->checkauth;
362 my $fund = $e->retrieve_acq_fund($fund_id) or return $e->event;
363 return $e->event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
364 return retrieve_fund_summary_impl($e, $fund);
368 sub retrieve_fund_summary_impl {
371 my $at = $e->search_acq_fund_allocation_total({fund => $fund->id})->[0];
372 my $dt = $e->search_acq_fund_debit_total({fund => $fund->id})->[0];
373 my $et = $e->search_acq_fund_encumbrance_total({fund => $fund->id})->[0];
374 my $st = $e->search_acq_fund_spent_total({fund => $fund->id})->[0];
375 my $cb = $e->search_acq_fund_combined_balance({fund => $fund->id})->[0];
376 my $sb = $e->search_acq_fund_spent_balance({fund => $fund->id})->[0];
379 allocation_total => ($at) ? $at->amount : 0,
380 debit_total => ($dt) ? $dt->amount : 0,
381 encumbrance_total => ($et) ? $et->amount : 0,
382 spent_total => ($st) ? $st->amount : 0,
383 combined_balance => ($cb) ? $cb->amount : 0,
384 spent_balance => ($sb) ? $sb->amount : 0,
388 __PACKAGE__->register_method(
389 method => 'transfer_money_between_funds',
390 api_name => 'open-ils.acq.funds.transfer_money',
392 desc => 'Method for transfering money between funds',
394 {desc => 'Authentication token', type => 'string'},
395 {desc => 'Originating fund ID', type => 'number'},
396 {desc => 'Amount of money to transfer away from the originating fund, in the same currency as said fund', type => 'number'},
397 {desc => 'Destination fund ID', type => 'number'},
398 {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'},
399 {desc => 'Transfer Note', type => 'string'}
401 return => {desc => '1 on success, Event on failure'}
405 sub transfer_money_between_funds {
406 my($self, $conn, $auth, $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $note) = @_;
407 my $e = new_editor(xact=>1, authtoken=>$auth);
408 return $e->die_event unless $e->checkauth;
409 my $ofund = $e->retrieve_acq_fund($ofund_id) or return $e->event;
410 return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $ofund->org, $ofund);
411 my $dfund = $e->retrieve_acq_fund($dfund_id) or return $e->event;
412 return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $dfund->org, $dfund);
414 if (!defined $dfund_amount) {
416 if ($ofund->currency_type ne $dfund->currency_type) {
417 my $exchange_rate = $e->json_query({
418 "select"=>{"acqexr"=>["ratio"]},
421 "from_currency"=>$ofund->currency_type,
422 "to_currency"=>$dfund->currency_type
425 if (scalar(@$exchange_rate)<1) {
426 $logger->error('Unable to find exchange rate for ' . $ofund->currency_type . ' to ' . $dfund->currency_type);
427 return $e->die_event;
429 $ratio = @{$exchange_rate}[0]->{ratio};
431 $dfund_amount = $ofund_amount * $ratio;
433 return $e->die_event unless $e->allowed("ACQ_XFER_MANUAL_DFUND_AMOUNT");
439 $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $e->requestor->id, $note
450 # ---------------------------------------------------------------
452 # ---------------------------------------------------------------
454 __PACKAGE__->register_method(
455 method => 'create_fund_alloc',
456 api_name => 'open-ils.acq.fund_allocation.create',
458 desc => 'Creates a new fund_allocation',
460 {desc => 'Authentication token', type => 'string'},
461 {desc => 'fund allocation object to create', type => 'object'}
463 return => {desc => 'The ID of the new fund_allocation'}
467 sub create_fund_alloc {
468 my($self, $conn, $auth, $fund_alloc) = @_;
469 my $e = new_editor(xact=>1, authtoken=>$auth);
470 return $e->die_event unless $e->checkauth;
472 # this action is equivalent to both debiting a funding source and crediting a fund
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);
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);
481 $fund_alloc->allocator($e->requestor->id);
482 $e->create_acq_fund_allocation($fund_alloc) or return $e->die_event;
484 return $fund_alloc->id;
488 __PACKAGE__->register_method(
489 method => 'delete_fund_alloc',
490 api_name => 'open-ils.acq.fund_allocation.delete',
492 desc => 'Deletes a fund_allocation',
494 {desc => 'Authentication token', type => 'string'},
495 {desc => 'fund Alocation ID', type => 'number'}
497 return => {desc => '1 on success, Event on failure'}
501 sub delete_fund_alloc {
502 my($self, $conn, $auth, $fund_alloc_id) = @_;
503 my $e = new_editor(xact=>1, authtoken=>$auth);
504 return $e->die_event unless $e->checkauth;
506 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->die_event;
508 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
509 or return $e->die_event;
510 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
512 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
513 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
515 $e->delete_acq_fund_allocation($fund_alloc) or return $e->die_event;
520 __PACKAGE__->register_method(
521 method => 'retrieve_fund_alloc',
522 api_name => 'open-ils.acq.fund_allocation.retrieve',
524 desc => 'Retrieves a new fund_allocation',
526 {desc => 'Authentication token', type => 'string'},
527 {desc => 'fund Allocation ID', type => 'number'}
529 return => {desc => 'The fund allocation object on success, Event on failure'}
533 sub retrieve_fund_alloc {
534 my($self, $conn, $auth, $fund_alloc_id) = @_;
535 my $e = new_editor(authtoken=>$auth);
536 return $e->event unless $e->checkauth;
537 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
539 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
540 or return $e->die_event;
541 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
543 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
544 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
550 __PACKAGE__->register_method(
551 method => 'retrieve_funding_source_allocations',
552 api_name => 'open-ils.acq.funding_source.allocations.retrieve',
554 desc => 'Retrieves a new fund_allocation',
556 {desc => 'Authentication token', type => 'string'},
557 {desc => 'fund Allocation ID', type => 'number'}
559 return => {desc => 'The fund allocation object on success, Event on failure'}
563 sub retrieve_funding_source_allocations {
564 my($self, $conn, $auth, $fund_alloc_id) = @_;
565 my $e = new_editor(authtoken=>$auth);
566 return $e->event unless $e->checkauth;
567 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
569 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
570 or return $e->die_event;
571 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
573 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
574 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
579 # ----------------------------------------------------------------------------
581 # ----------------------------------------------------------------------------
583 __PACKAGE__->register_method(
584 method => 'retrieve_all_currency_type',
585 api_name => 'open-ils.acq.currency_type.all.retrieve',
588 desc => 'Retrieves all currency_type objects',
590 {desc => 'Authentication token', type => 'string'},
592 return => {desc => 'List of currency_type objects', type => 'list'}
596 sub retrieve_all_currency_type {
597 my($self, $conn, $auth, $fund_alloc_id) = @_;
598 my $e = new_editor(authtoken=>$auth);
599 return $e->event unless $e->checkauth;
600 return $e->event unless $e->allowed('GENERAL_ACQ');
601 $conn->respond($_) for @{$e->retrieve_all_acq_currency_type()};
604 __PACKAGE__->register_method(
605 method => 'create_lineitem_assets',
606 api_name => 'open-ils.acq.lineitem.assets.create',
608 desc => q/Creates the bibliographic data, volume, and copies associated with a lineitem./,
610 {desc => 'Authentication token', type => 'string'},
611 {desc => 'The lineitem id', type => 'number'},
612 {desc => q/Options hash./}
614 return => {desc => 'ID of newly created bib record, Event on error'}
618 sub create_lineitem_assets {
619 my($self, $conn, $auth, $li_id, $options) = @_;
620 my $e = new_editor(authtoken=>$auth, xact=>1);
621 return $e->die_event unless $e->checkauth;
622 my ($count, $resp) = create_lineitem_assets_impl($e, $li_id, $options);
623 return $resp if $resp;
628 sub create_lineitem_assets_impl {
629 my($e, $li_id, $options) = @_;
633 my $li = $e->retrieve_acq_lineitem([
636 flesh_fields => {jub => ['purchase_order', 'attributes']}
638 ]) or return (undef, $e->die_event);
640 # -----------------------------------------------------------------
641 # first, create the bib record if necessary
642 # -----------------------------------------------------------------
643 unless($li->eg_bib_id) {
645 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
646 $e, $li->marc); #$rec->bib_source
648 if($U->event_code($record)) {
650 return (undef, $record);
653 $li->editor($e->requestor->id);
654 $li->edit_time('now');
655 $li->eg_bib_id($record->id);
656 $e->update_acq_lineitem($li) or return (undef, $e->die_event);
659 my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
661 # -----------------------------------------------------------------
662 # for each lineitem_detail, create the volume if necessary, create
663 # a copy, and link them all together.
664 # -----------------------------------------------------------------
666 for my $li_detail_id (@{$li_details}) {
668 my $li_detail = $e->retrieve_acq_lineitem_detail($li_detail_id)
669 or return (undef, $e->die_event);
671 # Create the volume object if necessary
672 my $volume = $volcache{$li_detail->cn_label};
673 unless($volume and $volume->owning_lib == $li_detail->owning_lib) {
675 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
676 $e, $li_detail->cn_label, $li->eg_bib_id, $li_detail->owning_lib);
677 return (undef, $evt) if $evt;
678 $volcache{$volume->id} = $volume;
681 my $copy = Fieldmapper::asset::copy->new;
683 $copy->loan_duration(2);
684 $copy->fine_level(2);
685 $copy->status(OILS_COPY_STATUS_ON_ORDER);
686 $copy->barcode($li_detail->barcode);
687 $copy->location($li_detail->location);
688 $copy->call_number($volume->id);
689 $copy->circ_lib($volume->owning_lib);
690 $copy->circ_modifier($$options{circ_modifier} || 'book');
692 $evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $volume, $copy);
693 return (undef, $evt) if $evt;
695 $li_detail->eg_copy_id($copy->id);
696 $e->update_acq_lineitem_detail($li_detail) or return (undef, $e->die_event);
699 return (scalar @{$li_details});
705 sub create_purchase_order_impl {
706 my($e, $p_order) = @_;
708 $p_order->creator($e->requestor->id);
709 $p_order->editor($e->requestor->id);
710 $p_order->owner($e->requestor->id);
711 $p_order->edit_time('now');
713 return $e->die_event unless
714 $e->allowed('CREATE_PURCHASE_ORDER', $p_order->ordering_agency);
716 my $provider = $e->retrieve_acq_provider($p_order->provider)
717 or return $e->die_event;
718 return $e->die_event unless
719 $e->allowed('MANAGE_PROVIDER', $provider->owner, $provider);
721 $e->create_acq_purchase_order($p_order) or return $e->die_event;
726 __PACKAGE__->register_method(
727 method => 'retrieve_all_user_purchase_order',
728 api_name => 'open-ils.acq.purchase_order.user.all.retrieve',
731 desc => 'Retrieves a purchase order',
733 {desc => 'Authentication token', type => 'string'},
734 {desc => 'purchase_order to retrieve', type => 'number'},
735 {desc => q/Options hash. flesh_lineitems: to get the lineitems and lineitem_attrs;
736 clear_marc: to clear the MARC data from the lineitem (for reduced bandwidth);
737 limit: number of items to return ,defaults to 50;
738 offset: offset in the list of items to return
739 order_by: sort the result, provide one or more colunm names, separated by commas,
740 optionally followed by ASC or DESC as a single string
741 li_limit : number of lineitems to return if fleshing line items;
742 li_offset : lineitem offset if fleshing line items
743 li_order_by : lineitem sort definition if fleshing line items
744 flesh_lineitem_detail_count : flesh lineitem_detail_count field
748 return => {desc => 'The purchase order, Event on failure'}
752 sub retrieve_all_user_purchase_order {
753 my($self, $conn, $auth, $options) = @_;
754 my $e = new_editor(authtoken=>$auth);
755 return $e->event unless $e->checkauth;
758 # grab purchase orders I have
759 my $perm_orgs = $U->user_has_work_perm_at($e, 'MANAGE_PROVIDER', {descendants =>1});
760 return OpenILS::Event->new('PERM_FAILURE', ilsperm => 'MANAGE_PROVIDER')
762 my $provider_ids = $e->search_acq_provider({owner => $perm_orgs}, {idlist=>1});
763 my $po_ids = $e->search_acq_purchase_order({provider => $provider_ids}, {idlist=>1});
765 # grab my purchase orders
766 push(@$po_ids, @{$e->search_acq_purchase_order({owner => $e->requestor->id}, {idlist=>1})});
768 return undef unless @$po_ids;
770 # now get the db to limit/sort for us
771 $po_ids = $e->search_acq_purchase_order(
773 limit => $$options{limit} || 50,
774 offset => $$options{offset} || 0,
775 order_by => {acqpo => $$options{order_by} || 'create_time'}
781 $conn->respond(retrieve_purchase_order_impl($e, $_, $options)) for @$po_ids;
786 __PACKAGE__->register_method(
787 method => 'search_purchase_order',
788 api_name => 'open-ils.acq.purchase_order.search',
791 desc => 'Search for a purchase order',
793 {desc => 'Authentication token', type => 'string'},
794 {desc => q/Search hash. Search fields include id, provider/, type => 'hash'}
796 return => {desc => 'A stream of POs'}
800 sub search_purchase_order {
801 my($self, $conn, $auth, $search, $options) = @_;
802 my $e = new_editor(authtoken=>$auth);
803 return $e->event unless $e->checkauth;
804 my $po_ids = $e->search_acq_purchase_order($search, {idlist=>1});
805 for my $po_id (@$po_ids) {
806 $conn->respond($e->retrieve_acq_purchase_order($po_id))
807 unless po_perm_failure($e, $po_id);
815 __PACKAGE__->register_method(
816 method => 'retrieve_purchase_order',
817 api_name => 'open-ils.acq.purchase_order.retrieve',
820 desc => 'Retrieves a purchase order',
822 {desc => 'Authentication token', type => 'string'},
823 {desc => 'purchase_order to retrieve', type => 'number'},
824 {desc => q/Options hash. flesh_lineitems, to get the lineitems and lineitem_attrs;
825 clear_marc, to clear the MARC data from the lineitem (for reduced bandwidth)
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
832 return => {desc => 'The purchase order, Event on failure'}
836 sub retrieve_purchase_order {
837 my($self, $conn, $auth, $po_id, $options) = @_;
838 my $e = new_editor(authtoken=>$auth);
839 return $e->event unless $e->checkauth;
841 $po_id = [ $po_id ] unless ref $po_id;
844 if ( po_perm_failure($e, $_) )
847 { $rv = retrieve_purchase_order_impl($e, $_, $options) }
854 # if the user does not have permission to perform actions on this PO, return the perm failure event
855 sub po_perm_failure {
856 my($e, $po_id, $fund_id) = @_;
857 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
858 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency, $po);
862 sub build_price_summary {
863 my ($e, $po_id) = @_;
865 # TODO: Add summary value for estimated amount (pre-encumber)
867 # fetch the fund debits for this purchase order
868 my $debits = $e->json_query({
869 "select" => {"acqfdeb" => [qw/encumbrance amount/]},
873 "fkey" => "lineitem",
877 "fkey" => "purchase_order", "field" => "id"
881 "acqfdeb" => {"fkey" => "fund_debit", "field" => "id"}
884 "where" => {"+acqpo" => {"id" => $po_id}}
887 my ($enc, $spent) = (0, 0);
888 for my $deb (@$debits) {
889 if($U->is_true($deb->{encumbrance})) {
890 $enc += $deb->{amount};
892 $spent += $deb->{amount};
899 sub retrieve_purchase_order_impl {
900 my($e, $po_id, $options) = @_;
902 # let's just always flesh this if it's there. what the hey.
904 "flesh" => 1, "flesh_fields" => {"acqpo" => ["cancel_reason"]}
908 if ($options->{"flesh_notes"}) {
909 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
911 if ($options->{"flesh_provider"}) {
912 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider";
914 my $po = $e->retrieve_acq_purchase_order([$po_id, $flesh])
917 if($$options{flesh_lineitems}) {
919 my $items = $e->search_acq_lineitem([
920 {purchase_order => $po_id},
924 jub => ['attributes']
926 limit => $$options{li_limit} || 50,
927 offset => $$options{li_offset} || 0,
928 order_by => {jub => $$options{li_order_by} || 'create_time'}
932 if($$options{clear_marc}) {
933 $_->clear_marc for @$items;
936 $po->lineitems($items);
937 $po->lineitem_count(scalar(@$items));
939 } elsif( $$options{flesh_lineitem_count} ) {
941 my $items = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist=>1});
942 $po->lineitem_count(scalar(@$items));
945 if($$options{flesh_price_summary}) {
946 my ($enc, $spent) = build_price_summary($e, $po_id);
947 $po->amount_encumbered($enc);
948 $po->amount_spent($spent);
955 __PACKAGE__->register_method(
956 method => 'format_po',
957 api_name => 'open-ils.acq.purchase_order.format'
961 my($self, $conn, $auth, $po_id, $format) = @_;
962 my $e = new_editor(authtoken=>$auth);
963 return $e->event unless $e->checkauth;
965 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
966 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
968 my $hook = "format.po.$format";
969 return $U->fire_object_event(undef, $hook, $po, $po->ordering_agency);
972 __PACKAGE__->register_method(
973 method => 'format_lineitem',
974 api_name => 'open-ils.acq.lineitem.format'
977 sub format_lineitem {
978 my($self, $conn, $auth, $li_id, $format, $user_data) = @_;
979 my $e = new_editor(authtoken=>$auth);
980 return $e->event unless $e->checkauth;
982 my $li = $e->retrieve_acq_lineitem($li_id) or return $e->event;
985 if (defined $li->purchase_order) {
986 my $po = $e->retrieve_acq_purchase_order($li->purchase_order) or return $e->die_event;
987 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
988 $context_org = $po->ordering_agency;
990 my $pl = $e->retrieve_acq_picklist($li->picklist) or return $e->die_event;
991 if($e->requestor->id != $pl->owner) {
992 return $e->event unless
993 $e->allowed('VIEW_PICKLIST', $pl->org_unit, $pl);
995 $context_org = $pl->org_unit;
998 my $hook = "format.acqli.$format";
999 return $U->fire_object_event(undef, $hook, $li, $context_org, 'print-on-demand', $user_data);
1002 __PACKAGE__->register_method (
1003 method => 'po_events',
1004 api_name => 'open-ils.acq.purchase_order.events.owner',
1007 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1008 @param authtoken Login session key
1009 @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.
1010 @param options Object for tweaking the selection criteria and fleshing options.
1014 __PACKAGE__->register_method (
1015 method => 'po_events',
1016 api_name => 'open-ils.acq.purchase_order.events.ordering_agency',
1019 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1020 @param authtoken Login session key
1021 @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.
1022 @param options Object for tweaking the selection criteria and fleshing options.
1026 __PACKAGE__->register_method (
1027 method => 'po_events',
1028 api_name => 'open-ils.acq.purchase_order.events.id',
1031 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1032 @param authtoken Login session key
1033 @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.
1034 @param options Object for tweaking the selection criteria and fleshing options.
1039 my($self, $conn, $auth, $search_value, $options) = @_;
1040 my $e = new_editor(authtoken => $auth);
1041 return $e->event unless $e->checkauth;
1043 (my $search_field = $self->api_name) =~ s/.*\.([_a-z]+)$/$1/;
1044 my $obj_type = 'acqpo';
1046 if ($search_field eq 'ordering_agency') {
1047 $search_value = $U->get_org_descendants($search_value);
1051 "select"=>{"atev"=>["id"]},
1056 "select"=>{$obj_type=>["id"]},
1058 "where"=>{$search_field=>$search_value}
1063 "select"=>{atevdef=>["id"]},
1066 "hook"=>"format.po.jedi"
1072 "order_by"=>[{"class"=>"atev", "field"=>"run_time", "direction"=>"desc"}]
1075 if ($options && defined $options->{state}) {
1076 $query->{'where'}{'state'} = $options->{state}
1079 if ($options && defined $options->{start_time}) {
1080 $query->{'where'}{'start_time'} = $options->{start_time};
1083 if ($options && defined $options->{order_by}) {
1084 $query->{'order_by'} = $options->{order_by};
1086 my $po_events = $e->json_query($query);
1088 my $flesh_fields = { 'atev' => [ 'event_def' ] };
1089 my $flesh_depth = 1;
1091 for my $id (@$po_events) {
1092 my $event = $e->retrieve_action_trigger_event([
1094 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
1096 if (! $event) { next; }
1098 my $po = retrieve_purchase_order_impl(
1101 {flesh_lineitem_count=>1,flesh_price_summary=>1}
1104 if ($e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() )) {
1105 $event->target( $po );
1106 $conn->respond($event);
1113 __PACKAGE__->register_method (
1114 method => 'update_po_events',
1115 api_name => 'open-ils.acq.purchase_order.event.cancel.batch',
1118 __PACKAGE__->register_method (
1119 method => 'update_po_events',
1120 api_name => 'open-ils.acq.purchase_order.event.reset.batch',
1124 sub update_po_events {
1125 my($self, $conn, $auth, $event_ids) = @_;
1126 my $e = new_editor(xact => 1, authtoken => $auth);
1127 return $e->die_event unless $e->checkauth;
1130 for my $id (@$event_ids) {
1132 # do a little dance to determine what libraries we are ultimately affecting
1133 my $event = $e->retrieve_action_trigger_event([
1136 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
1138 ]) or return $e->die_event;
1140 my $po = retrieve_purchase_order_impl(
1146 return $e->die_event unless $e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() );
1148 if($self->api_name =~ /cancel/) {
1149 $event->state('invalid');
1150 } elsif($self->api_name =~ /reset/) {
1151 $event->clear_start_time;
1152 $event->clear_update_time;
1153 $event->state('pending');
1156 $e->update_action_trigger_event($event) or return $e->die_event;
1157 $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
1161 return {complete => 1};
1165 __PACKAGE__->register_method (
1166 method => 'process_fiscal_rollover',
1167 api_name => 'open-ils.acq.fiscal_rollover.combined',
1171 Performs a combined fiscal fund rollover process.
1173 Creates a new series of funds for the following year, copying the old years
1174 funds that are marked as propagable. They apply to the funds belonging to
1175 either an org unit or to an org unit and all of its dependent org units.
1176 The procedures may be run repeatedly; if any fund has already been propagated,
1177 both the old and the new funds will be left alone.
1179 Closes out any applicable funds (by org unit or by org unit and dependents)
1180 that are marked as propagable. If such a fund has not already been propagated
1181 to the new year, it will be propagated at closing time.
1183 If a fund is marked as subject to rollover, any unspent balance in the old year's
1184 fund (including money encumbered but not spent) is transferred to the new year's
1185 fund. Otherwise it is deallocated back to the funding source(s).
1187 In either case, any encumbrance debits are transferred to the new fund, along
1188 with the corresponding lineitem details. The old year's fund is marked as inactive
1189 so that new debits may not be charged to it.
1192 {desc => 'Authentication token', type => 'string'},
1193 {desc => 'Fund Year to roll over', type => 'integer'},
1194 {desc => 'Org unit ID', type => 'integer'},
1195 {desc => 'Include Descendant Orgs (boolean)', type => 'integer'},
1197 return => {desc => 'Returns a stream of all related funds for the next year including fund summary for each'}
1202 __PACKAGE__->register_method (
1203 method => 'process_fiscal_rollover',
1204 api_name => 'open-ils.acq.fiscal_rollover.combined.dry_run',
1208 @see open-ils.acq.fiscal_rollover.combined
1209 This is the dry-run version. The action is performed,
1210 new fund information is returned, then all changes are rolled back.
1216 __PACKAGE__->register_method (
1217 method => 'process_fiscal_rollover',
1218 api_name => 'open-ils.acq.fiscal_rollover.propagate',
1222 @see open-ils.acq.fiscal_rollover.combined
1223 This version performs fund propagation only. I.e, creation of
1224 the following year's funds. It does not rollover over balances, encumbrances,
1225 or mark the previous year's funds as complete.
1230 __PACKAGE__->register_method (
1231 method => 'process_fiscal_rollover',
1232 api_name => 'open-ils.acq.fiscal_rollover.propagate.dry_run',
1234 signature => { desc => q/
1235 @see open-ils.acq.fiscal_rollover.propagate
1236 This is the dry-run version. The action is performed,
1237 new fund information is returned, then all changes are rolled back.
1243 sub process_fiscal_rollover {
1244 my( $self, $conn, $auth, $year, $org_id, $descendants, $options ) = @_;
1246 my $e = new_editor(xact=>1, authtoken=>$auth);
1247 return $e->die_event unless $e->checkauth;
1248 return $e->die_event unless $e->allowed('ADMIN_FUND', $org_id);
1251 my $combined = ($self->api_name =~ /combined/);
1253 my $org_ids = ($descendants) ?
1256 { $_->{id} } # fetch my descendants
1257 @{$e->json_query({from => ['actor.org_unit_descendants', $org_id]})}
1261 # Create next year's funds
1262 # Note, it's safe to run this more than once.
1263 # IOW, it will not create duplicate new funds.
1267 'acq.propagate_funds_by_org_tree' :
1268 'acq.propagate_funds_by_org_unit',
1269 $year, $e->requestor->id, $org_id
1275 # Roll the uncumbrances over to next year's funds
1276 # Mark the funds for $year as inactive
1281 'acq.rollover_funds_by_org_tree' :
1282 'acq.rollover_funds_by_org_unit',
1283 $year, $e->requestor->id, $org_id
1288 # Fetch all funds for the specified org units for the subsequent year
1289 my $fund_ids = $e->search_acq_fund([
1291 year => int($year) + 1,
1295 limit => $$options{limit} || 20,
1296 offset => $$options{offset} || 0,
1302 foreach (@$fund_ids) {
1303 my $fund = $e->retrieve_acq_fund($_) or return $e->die_event;
1304 $fund->summary(retrieve_fund_summary_impl($e, $fund));
1307 if($combined and $U->is_true($fund->rollover)) {
1308 # see how much money was rolled over
1310 my $sum = $e->json_query({
1311 select => {acqftr => [{column => 'dest_amount', transform => 'sum'}]},
1313 where => {dest_fund => $fund->id, note => 'Rollover'}
1316 $amount = $sum->{dest_amount} if $sum;
1319 $conn->respond({fund => $fund, rollover_amount => $amount});
1322 $self->api_name =~ /dry_run/ and $e->rollback or $e->commit;