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'}]
333 return [map { $_->{year} } @$data];
336 my $funds = $e->search_acq_fund($query);
338 for my $fund (@$funds) {
339 $fund->summary(retrieve_fund_summary_impl($e, $fund))
340 if $$options{flesh_summary};
341 $conn->respond($fund);
347 __PACKAGE__->register_method(
348 method => 'retrieve_fund_summary',
349 api_name => 'open-ils.acq.fund.summary.retrieve',
352 desc => 'Returns a summary of credits/debits/encumbrances for a fund',
354 {desc => 'Authentication token', type => 'string'},
355 {desc => 'fund id', type => 'number' }
357 return => {desc => 'A hash of summary information, Event on failure'}
361 sub retrieve_fund_summary {
362 my($self, $conn, $auth, $fund_id) = @_;
363 my $e = new_editor(authtoken=>$auth);
364 return $e->event unless $e->checkauth;
365 my $fund = $e->retrieve_acq_fund($fund_id) or return $e->event;
366 return $e->event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
367 return retrieve_fund_summary_impl($e, $fund);
371 sub retrieve_fund_summary_impl {
374 my $at = $e->search_acq_fund_allocation_total({fund => $fund->id})->[0];
375 my $dt = $e->search_acq_fund_debit_total({fund => $fund->id})->[0];
376 my $et = $e->search_acq_fund_encumbrance_total({fund => $fund->id})->[0];
377 my $st = $e->search_acq_fund_spent_total({fund => $fund->id})->[0];
378 my $cb = $e->search_acq_fund_combined_balance({fund => $fund->id})->[0];
379 my $sb = $e->search_acq_fund_spent_balance({fund => $fund->id})->[0];
382 allocation_total => ($at) ? $at->amount : 0,
383 debit_total => ($dt) ? $dt->amount : 0,
384 encumbrance_total => ($et) ? $et->amount : 0,
385 spent_total => ($st) ? $st->amount : 0,
386 combined_balance => ($cb) ? $cb->amount : 0,
387 spent_balance => ($sb) ? $sb->amount : 0,
391 __PACKAGE__->register_method(
392 method => 'transfer_money_between_funds',
393 api_name => 'open-ils.acq.funds.transfer_money',
395 desc => 'Method for transfering money between funds',
397 {desc => 'Authentication token', type => 'string'},
398 {desc => 'Originating fund ID', type => 'number'},
399 {desc => 'Amount of money to transfer away from the originating fund, in the same currency as said fund', type => 'number'},
400 {desc => 'Destination fund ID', type => 'number'},
401 {desc => 'Amount of money to transfer to the destination fund, in the same currency as said fund. If null, uses the same amount specified with the Originating Fund, and attempts a currency conversion if appropriate.', type => 'number'},
402 {desc => 'Transfer Note', type => 'string'}
404 return => {desc => '1 on success, Event on failure'}
408 sub transfer_money_between_funds {
409 my($self, $conn, $auth, $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $note) = @_;
410 my $e = new_editor(xact=>1, authtoken=>$auth);
411 return $e->die_event unless $e->checkauth;
412 my $ofund = $e->retrieve_acq_fund($ofund_id) or return $e->event;
413 return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $ofund->org, $ofund);
414 my $dfund = $e->retrieve_acq_fund($dfund_id) or return $e->event;
415 return $e->die_event unless $e->allowed(['ADMIN_FUND','MANAGE_FUND'], $dfund->org, $dfund);
417 if (!defined $dfund_amount) {
419 if ($ofund->currency_type ne $dfund->currency_type) {
421 $dfund_amount = $e->json_query({
423 'acq.exchange_ratio',
424 $ofund->currency_type,
425 $dfund->currency_type,
428 })->[0]->{'acq.exchange_ratio'};
432 $dfund_amount = $ofund_amount;
436 return $e->die_event unless $e->allowed("ACQ_XFER_MANUAL_DFUND_AMOUNT");
442 $ofund_id, $ofund_amount, $dfund_id, $dfund_amount, $e->requestor->id, $note
453 # ---------------------------------------------------------------
455 # ---------------------------------------------------------------
457 __PACKAGE__->register_method(
458 method => 'create_fund_alloc',
459 api_name => 'open-ils.acq.fund_allocation.create',
461 desc => 'Creates a new fund_allocation',
463 {desc => 'Authentication token', type => 'string'},
464 {desc => 'fund allocation object to create', type => 'object'}
466 return => {desc => 'The ID of the new fund_allocation'}
470 sub create_fund_alloc {
471 my($self, $conn, $auth, $fund_alloc) = @_;
472 my $e = new_editor(xact=>1, authtoken=>$auth);
473 return $e->die_event unless $e->checkauth;
475 # this action is equivalent to both debiting a funding source and crediting a fund
477 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
478 or return $e->die_event;
479 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner);
481 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
482 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
484 $fund_alloc->allocator($e->requestor->id);
485 $e->create_acq_fund_allocation($fund_alloc) or return $e->die_event;
487 return $fund_alloc->id;
491 __PACKAGE__->register_method(
492 method => 'delete_fund_alloc',
493 api_name => 'open-ils.acq.fund_allocation.delete',
495 desc => 'Deletes a fund_allocation',
497 {desc => 'Authentication token', type => 'string'},
498 {desc => 'fund Alocation ID', type => 'number'}
500 return => {desc => '1 on success, Event on failure'}
504 sub delete_fund_alloc {
505 my($self, $conn, $auth, $fund_alloc_id) = @_;
506 my $e = new_editor(xact=>1, authtoken=>$auth);
507 return $e->die_event unless $e->checkauth;
509 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->die_event;
511 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
512 or return $e->die_event;
513 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
515 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
516 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
518 $e->delete_acq_fund_allocation($fund_alloc) or return $e->die_event;
523 __PACKAGE__->register_method(
524 method => 'retrieve_fund_alloc',
525 api_name => 'open-ils.acq.fund_allocation.retrieve',
528 desc => 'Retrieves a new fund_allocation',
530 {desc => 'Authentication token', type => 'string'},
531 {desc => 'fund Allocation ID', type => 'number'}
533 return => {desc => 'The fund allocation object on success, Event on failure'}
537 sub retrieve_fund_alloc {
538 my($self, $conn, $auth, $fund_alloc_id) = @_;
539 my $e = new_editor(authtoken=>$auth);
540 return $e->event unless $e->checkauth;
541 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
543 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
544 or return $e->die_event;
545 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
547 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
548 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
554 __PACKAGE__->register_method(
555 method => 'retrieve_funding_source_allocations',
556 api_name => 'open-ils.acq.funding_source.allocations.retrieve',
559 desc => 'Retrieves a new fund_allocation',
561 {desc => 'Authentication token', type => 'string'},
562 {desc => 'fund Allocation ID', type => 'number'}
564 return => {desc => 'The fund allocation object on success, Event on failure'}
568 sub retrieve_funding_source_allocations {
569 my($self, $conn, $auth, $fund_alloc_id) = @_;
570 my $e = new_editor(authtoken=>$auth);
571 return $e->event unless $e->checkauth;
572 my $fund_alloc = $e->retrieve_acq_fund_allocation($fund_alloc_id) or return $e->event;
574 my $source = $e->retrieve_acq_funding_source($fund_alloc->funding_source)
575 or return $e->die_event;
576 return $e->die_event unless $e->allowed('MANAGE_FUNDING_SOURCE', $source->owner, $source);
578 my $fund = $e->retrieve_acq_fund($fund_alloc->fund) or return $e->die_event;
579 return $e->die_event unless $e->allowed('MANAGE_FUND', $fund->org, $fund);
584 # ----------------------------------------------------------------------------
586 # ----------------------------------------------------------------------------
588 __PACKAGE__->register_method(
589 method => 'retrieve_all_currency_type',
590 api_name => 'open-ils.acq.currency_type.all.retrieve',
593 desc => 'Retrieves all currency_type objects',
595 {desc => 'Authentication token', type => 'string'},
597 return => {desc => 'List of currency_type objects', type => 'list'}
601 sub retrieve_all_currency_type {
602 my($self, $conn, $auth, $fund_alloc_id) = @_;
603 my $e = new_editor(authtoken=>$auth);
604 return $e->event unless $e->checkauth;
605 return $e->event unless $e->allowed('GENERAL_ACQ');
606 $conn->respond($_) for @{$e->retrieve_all_acq_currency_type()};
609 __PACKAGE__->register_method(
610 method => 'create_lineitem_assets',
611 api_name => 'open-ils.acq.lineitem.assets.create',
613 desc => q/Creates the bibliographic data, volume, and copies associated with a lineitem./,
615 {desc => 'Authentication token', type => 'string'},
616 {desc => 'The lineitem id', type => 'number'},
617 {desc => q/Options hash./}
619 return => {desc => 'ID of newly created bib record, Event on error'}
623 sub create_lineitem_assets {
624 my($self, $conn, $auth, $li_id, $options) = @_;
625 my $e = new_editor(authtoken=>$auth, xact=>1);
626 return $e->die_event unless $e->checkauth;
627 my ($count, $resp) = create_lineitem_assets_impl($e, $li_id, $options);
628 return $resp if $resp;
633 sub create_lineitem_assets_impl {
634 my($e, $li_id, $options) = @_;
638 my $li = $e->retrieve_acq_lineitem([
641 flesh_fields => {jub => ['purchase_order', 'attributes']}
643 ]) or return (undef, $e->die_event);
645 # -----------------------------------------------------------------
646 # first, create the bib record if necessary
647 # -----------------------------------------------------------------
648 unless($li->eg_bib_id) {
650 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
651 $e, $li->marc); #$rec->bib_source
653 if($U->event_code($record)) {
655 return (undef, $record);
658 $li->editor($e->requestor->id);
659 $li->edit_time('now');
660 $li->eg_bib_id($record->id);
661 $e->update_acq_lineitem($li) or return (undef, $e->die_event);
664 my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
666 # -----------------------------------------------------------------
667 # for each lineitem_detail, create the volume if necessary, create
668 # a copy, and link them all together.
669 # -----------------------------------------------------------------
671 for my $li_detail_id (@{$li_details}) {
673 my $li_detail = $e->retrieve_acq_lineitem_detail($li_detail_id)
674 or return (undef, $e->die_event);
676 # Create the volume object if necessary
677 my $volume = $volcache{$li_detail->cn_label};
678 unless($volume and $volume->owning_lib == $li_detail->owning_lib) {
680 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
681 $e, $li_detail->cn_label, $li->eg_bib_id, $li_detail->owning_lib);
682 return (undef, $evt) if $evt;
683 $volcache{$volume->id} = $volume;
686 my $copy = Fieldmapper::asset::copy->new;
688 $copy->loan_duration(2);
689 $copy->fine_level(2);
690 $copy->status(OILS_COPY_STATUS_ON_ORDER);
691 $copy->barcode($li_detail->barcode);
692 $copy->location($li_detail->location);
693 $copy->call_number($volume->id);
694 $copy->circ_lib($volume->owning_lib);
695 $copy->circ_modifier($$options{circ_modifier} || 'book');
697 $evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $volume, $copy);
698 return (undef, $evt) if $evt;
700 $li_detail->eg_copy_id($copy->id);
701 $e->update_acq_lineitem_detail($li_detail) or return (undef, $e->die_event);
704 return (scalar @{$li_details});
710 sub create_purchase_order_impl {
711 my($e, $p_order) = @_;
713 $p_order->creator($e->requestor->id);
714 $p_order->editor($e->requestor->id);
715 $p_order->owner($e->requestor->id);
716 $p_order->edit_time('now');
718 return $e->die_event unless
719 $e->allowed('CREATE_PURCHASE_ORDER', $p_order->ordering_agency);
721 my $provider = $e->retrieve_acq_provider($p_order->provider)
722 or return $e->die_event;
723 return $e->die_event unless
724 $e->allowed('MANAGE_PROVIDER', $provider->owner, $provider);
726 $e->create_acq_purchase_order($p_order) or return $e->die_event;
731 __PACKAGE__->register_method(
732 method => 'retrieve_all_user_purchase_order',
733 api_name => 'open-ils.acq.purchase_order.user.all.retrieve',
736 desc => 'Retrieves a purchase order',
738 {desc => 'Authentication token', type => 'string'},
739 {desc => 'purchase_order to retrieve', type => 'number'},
740 {desc => q/Options hash. flesh_lineitems: to get the lineitems and lineitem_attrs;
741 clear_marc: to clear the MARC data from the lineitem (for reduced bandwidth);
742 limit: number of items to return ,defaults to 50;
743 offset: offset in the list of items to return
744 order_by: sort the result, provide one or more colunm names, separated by commas,
745 optionally followed by ASC or DESC as a single string
746 li_limit : number of lineitems to return if fleshing line items;
747 li_offset : lineitem offset if fleshing line items
748 li_order_by : lineitem sort definition if fleshing line items
749 flesh_lineitem_detail_count : flesh lineitem_detail_count field
753 return => {desc => 'The purchase order, Event on failure'}
757 sub retrieve_all_user_purchase_order {
758 my($self, $conn, $auth, $options) = @_;
759 my $e = new_editor(authtoken=>$auth);
760 return $e->event unless $e->checkauth;
763 # grab purchase orders I have
764 my $perm_orgs = $U->user_has_work_perm_at($e, 'MANAGE_PROVIDER', {descendants =>1});
765 return OpenILS::Event->new('PERM_FAILURE', ilsperm => 'MANAGE_PROVIDER')
767 my $provider_ids = $e->search_acq_provider({owner => $perm_orgs}, {idlist=>1});
768 my $po_ids = $e->search_acq_purchase_order({provider => $provider_ids}, {idlist=>1});
770 # grab my purchase orders
771 push(@$po_ids, @{$e->search_acq_purchase_order({owner => $e->requestor->id}, {idlist=>1})});
773 return undef unless @$po_ids;
775 # now get the db to limit/sort for us
776 $po_ids = $e->search_acq_purchase_order(
778 limit => $$options{limit} || 50,
779 offset => $$options{offset} || 0,
780 order_by => {acqpo => $$options{order_by} || 'create_time'}
786 $conn->respond(retrieve_purchase_order_impl($e, $_, $options)) for @$po_ids;
791 __PACKAGE__->register_method(
792 method => 'search_purchase_order',
793 api_name => 'open-ils.acq.purchase_order.search',
796 desc => 'Search for a purchase order',
798 {desc => 'Authentication token', type => 'string'},
799 {desc => q/Search hash. Search fields include id, provider/, type => 'hash'}
801 return => {desc => 'A stream of POs'}
805 sub search_purchase_order {
806 my($self, $conn, $auth, $search, $options) = @_;
807 my $e = new_editor(authtoken=>$auth);
808 return $e->event unless $e->checkauth;
809 my $po_ids = $e->search_acq_purchase_order($search, {idlist=>1});
810 for my $po_id (@$po_ids) {
811 $conn->respond($e->retrieve_acq_purchase_order($po_id))
812 unless po_perm_failure($e, $po_id);
820 __PACKAGE__->register_method(
821 method => 'retrieve_purchase_order',
822 api_name => 'open-ils.acq.purchase_order.retrieve',
826 desc => 'Retrieves a purchase order',
828 {desc => 'Authentication token', type => 'string'},
829 {desc => 'purchase_order to retrieve', type => 'number'},
830 {desc => q/Options hash. flesh_lineitems, to get the lineitems and lineitem_attrs;
831 clear_marc, to clear the MARC data from the lineitem (for reduced bandwidth)
832 li_limit : number of lineitems to return if fleshing line items;
833 li_offset : lineitem offset if fleshing line items
834 li_order_by : lineitem sort definition if fleshing line items,
835 flesh_po_items : po_item objects
839 return => {desc => 'The purchase order, Event on failure'}
843 sub retrieve_purchase_order {
844 my($self, $conn, $auth, $po_id, $options) = @_;
845 my $e = new_editor(authtoken=>$auth);
846 return $e->event unless $e->checkauth;
848 $po_id = [ $po_id ] unless ref $po_id;
851 if ( po_perm_failure($e, $_) )
854 { $rv = retrieve_purchase_order_impl($e, $_, $options) }
863 # if the user does not have permission to perform actions on this PO, return the perm failure event
864 sub po_perm_failure {
865 my($e, $po_id, $fund_id) = @_;
866 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
867 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency, $po);
871 sub build_price_summary {
872 my ($e, $po_id) = @_;
874 # amounts for lineitems / lineitem_details
875 my $li_data = $e->json_query({
878 'estimated_unit_price',
879 {column => 'id', alias => 'li_id'}
882 # lineitem_detail.id is needed to ensure we have one
883 # "row" of data for every copy, regardless of whether
884 # a fund_debit exists for each copy.
885 {column => 'id', alias => 'lid_id'}
889 {column => 'amount', alias => 'debit_amount'}
900 fkey => 'fund_debit',
907 where => {'+jub' => {purchase_order => $po_id}}
910 # amounts for po_item's
911 my $item_data = $e->json_query({
913 acqpoi => ['estimated_cost'],
916 {column => 'amount', alias => 'debit_amount'}
923 fkey => 'fund_debit',
928 where => {'+acqpoi' => {purchase_order => $po_id}}
931 # sum amounts debited (for activated PO's) and amounts estimated
932 # (for pending PO's) for all lineitem_details and po_items.
934 my ($enc, $spent, $estimated) = (0, 0, 0);
936 for my $deb (@$li_data, @$item_data) {
938 if (defined $deb->{debit_amount}) { # could be $0
939 # we have a debit, treat it as authoritative.
941 # estimated amount includes all amounts encumbered or spent
942 $estimated += $deb->{debit_amount};
944 if($U->is_true($deb->{encumbrance})) {
945 $enc += $deb->{debit_amount};
947 $spent += $deb->{debit_amount};
951 # PO is not activated, so sum estimated costs.
952 # There will be one $deb object for every lineitem_detail
953 # and po_item. Adding the estimated costs for all gives
954 # us the total esimated amount.
957 $deb->{estimated_unit_price} || $deb->{estimated_cost} || 0
962 return ($enc, $spent, $estimated);
966 sub retrieve_purchase_order_impl {
967 my($e, $po_id, $options) = @_;
969 my $flesh = {"flesh" => 1, "flesh_fields" => {"acqpo" => []}};
972 unless ($options->{"no_flesh_cancel_reason"}) {
973 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "cancel_reason";
975 if ($options->{"flesh_notes"}) {
976 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
978 if ($options->{"flesh_provider"}) {
979 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider";
982 push (@{$flesh->{flesh_fields}->{acqpo}}, 'po_items') if $options->{flesh_po_items};
984 my $args = (@{$flesh->{"flesh_fields"}->{"acqpo"}}) ?
985 [$po_id, $flesh] : $po_id;
987 my $po = $e->retrieve_acq_purchase_order($args)
990 if($$options{flesh_lineitems}) {
992 my $flesh_fields = { jub => ['attributes'] };
993 $flesh_fields->{jub}->[1] = 'lineitem_details' if $$options{flesh_lineitem_details};
994 $flesh_fields->{acqlid} = ['fund_debit'] if $$options{flesh_fund_debit};
996 my $items = $e->search_acq_lineitem([
997 {purchase_order => $po_id},
1000 flesh_fields => $flesh_fields,
1001 limit => $$options{li_limit} || 50,
1002 offset => $$options{li_offset} || 0,
1003 order_by => {jub => $$options{li_order_by} || 'create_time'}
1007 if($$options{clear_marc}) {
1008 $_->clear_marc for @$items;
1011 $po->lineitems($items);
1012 $po->lineitem_count(scalar(@$items));
1014 } elsif( $$options{flesh_lineitem_ids} ) {
1015 $po->lineitems($e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1}));
1017 } elsif( $$options{flesh_lineitem_count} ) {
1019 my $items = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist=>1});
1020 $po->lineitem_count(scalar(@$items));
1023 if($$options{flesh_price_summary}) {
1024 my ($enc, $spent, $estimated) = build_price_summary($e, $po_id);
1025 $po->amount_encumbered($enc);
1026 $po->amount_spent($spent);
1027 $po->amount_estimated($estimated);
1034 __PACKAGE__->register_method(
1035 method => 'format_po',
1036 api_name => 'open-ils.acq.purchase_order.format'
1040 my($self, $conn, $auth, $po_id, $format) = @_;
1041 my $e = new_editor(authtoken=>$auth);
1042 return $e->event unless $e->checkauth;
1044 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
1045 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1047 my $hook = "format.po.$format";
1048 return $U->fire_object_event(undef, $hook, $po, $po->ordering_agency);
1051 __PACKAGE__->register_method(
1052 method => 'format_lineitem',
1053 api_name => 'open-ils.acq.lineitem.format'
1056 sub format_lineitem {
1057 my($self, $conn, $auth, $li_id, $format, $user_data) = @_;
1058 my $e = new_editor(authtoken=>$auth);
1059 return $e->event unless $e->checkauth;
1061 my $li = $e->retrieve_acq_lineitem($li_id) or return $e->event;
1064 if (defined $li->purchase_order) {
1065 my $po = $e->retrieve_acq_purchase_order($li->purchase_order) or return $e->die_event;
1066 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1067 $context_org = $po->ordering_agency;
1069 my $pl = $e->retrieve_acq_picklist($li->picklist) or return $e->die_event;
1070 if($e->requestor->id != $pl->owner) {
1071 return $e->event unless
1072 $e->allowed('VIEW_PICKLIST', $pl->org_unit, $pl);
1074 $context_org = $pl->org_unit;
1077 my $hook = "format.acqli.$format";
1078 return $U->fire_object_event(undef, $hook, $li, $context_org, 'print-on-demand', $user_data);
1081 __PACKAGE__->register_method (
1082 method => 'po_events',
1083 api_name => 'open-ils.acq.purchase_order.events.owner',
1086 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1087 @param authtoken Login session key
1088 @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.
1089 @param options Object for tweaking the selection criteria and fleshing options.
1093 __PACKAGE__->register_method (
1094 method => 'po_events',
1095 api_name => 'open-ils.acq.purchase_order.events.ordering_agency',
1098 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1099 @param authtoken Login session key
1100 @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.
1101 @param options Object for tweaking the selection criteria and fleshing options.
1105 __PACKAGE__->register_method (
1106 method => 'po_events',
1107 api_name => 'open-ils.acq.purchase_order.events.id',
1110 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1111 @param authtoken Login session key
1112 @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.
1113 @param options Object for tweaking the selection criteria and fleshing options.
1118 my($self, $conn, $auth, $search_value, $options) = @_;
1119 my $e = new_editor(authtoken => $auth);
1120 return $e->event unless $e->checkauth;
1122 (my $search_field = $self->api_name) =~ s/.*\.([_a-z]+)$/$1/;
1123 my $obj_type = 'acqpo';
1125 if ($search_field eq 'ordering_agency') {
1126 $search_value = $U->get_org_descendants($search_value);
1130 "select"=>{"atev"=>["id"]},
1135 "select"=>{$obj_type=>["id"]},
1137 "where"=>{$search_field=>$search_value}
1142 "select"=>{atevdef=>["id"]},
1145 "hook"=>"format.po.jedi"
1151 "order_by"=>[{"class"=>"atev", "field"=>"run_time", "direction"=>"desc"}]
1154 if ($options && defined $options->{state}) {
1155 $query->{'where'}{'state'} = $options->{state}
1158 if ($options && defined $options->{start_time}) {
1159 $query->{'where'}{'start_time'} = $options->{start_time};
1162 if ($options && defined $options->{order_by}) {
1163 $query->{'order_by'} = $options->{order_by};
1165 my $po_events = $e->json_query($query);
1167 my $flesh_fields = { 'atev' => [ 'event_def' ] };
1168 my $flesh_depth = 1;
1170 for my $id (@$po_events) {
1171 my $event = $e->retrieve_action_trigger_event([
1173 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
1175 if (! $event) { next; }
1177 my $po = retrieve_purchase_order_impl(
1180 {flesh_lineitem_count=>1,flesh_price_summary=>1}
1183 if ($e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() )) {
1184 $event->target( $po );
1185 $conn->respond($event);
1192 __PACKAGE__->register_method (
1193 method => 'update_po_events',
1194 api_name => 'open-ils.acq.purchase_order.event.cancel.batch',
1197 __PACKAGE__->register_method (
1198 method => 'update_po_events',
1199 api_name => 'open-ils.acq.purchase_order.event.reset.batch',
1203 sub update_po_events {
1204 my($self, $conn, $auth, $event_ids) = @_;
1205 my $e = new_editor(xact => 1, authtoken => $auth);
1206 return $e->die_event unless $e->checkauth;
1209 for my $id (@$event_ids) {
1211 # do a little dance to determine what libraries we are ultimately affecting
1212 my $event = $e->retrieve_action_trigger_event([
1215 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
1217 ]) or return $e->die_event;
1219 my $po = retrieve_purchase_order_impl(
1225 return $e->die_event unless $e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() );
1227 if($self->api_name =~ /cancel/) {
1228 $event->state('invalid');
1229 } elsif($self->api_name =~ /reset/) {
1230 $event->clear_start_time;
1231 $event->clear_update_time;
1232 $event->state('pending');
1235 $e->update_action_trigger_event($event) or return $e->die_event;
1236 $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
1240 return {complete => 1};
1244 __PACKAGE__->register_method (
1245 method => 'process_fiscal_rollover',
1246 api_name => 'open-ils.acq.fiscal_rollover.combined',
1250 Performs a combined fiscal fund rollover process.
1252 Creates a new series of funds for the following year, copying the old years
1253 funds that are marked as propagable. They apply to the funds belonging to
1254 either an org unit or to an org unit and all of its dependent org units.
1255 The procedures may be run repeatedly; if any fund has already been propagated,
1256 both the old and the new funds will be left alone.
1258 Closes out any applicable funds (by org unit or by org unit and dependents)
1259 that are marked as propagable. If such a fund has not already been propagated
1260 to the new year, it will be propagated at closing time.
1262 If a fund is marked as subject to rollover, any unspent balance in the old year's
1263 fund (including money encumbered but not spent) is transferred to the new year's
1264 fund. Otherwise it is deallocated back to the funding source(s).
1266 In either case, any encumbrance debits are transferred to the new fund, along
1267 with the corresponding lineitem details. The old year's fund is marked as inactive
1268 so that new debits may not be charged to it.
1271 {desc => 'Authentication token', type => 'string'},
1272 {desc => 'Fund Year to roll over', type => 'integer'},
1273 {desc => 'Org unit ID', type => 'integer'},
1274 {desc => 'Include Descendant Orgs (boolean)', type => 'integer'},
1275 {desc => 'Option hash: limit, offset, encumb_only', type => 'object'},
1277 return => {desc => 'Returns a stream of all related funds for the next year including fund summary for each'}
1282 __PACKAGE__->register_method (
1283 method => 'process_fiscal_rollover',
1284 api_name => 'open-ils.acq.fiscal_rollover.combined.dry_run',
1288 @see open-ils.acq.fiscal_rollover.combined
1289 This is the dry-run version. The action is performed,
1290 new fund information is returned, then all changes are rolled back.
1296 __PACKAGE__->register_method (
1297 method => 'process_fiscal_rollover',
1298 api_name => 'open-ils.acq.fiscal_rollover.propagate',
1302 @see open-ils.acq.fiscal_rollover.combined
1303 This version performs fund propagation only. I.e, creation of
1304 the following year's funds. It does not rollover over balances, encumbrances,
1305 or mark the previous year's funds as complete.
1310 __PACKAGE__->register_method (
1311 method => 'process_fiscal_rollover',
1312 api_name => 'open-ils.acq.fiscal_rollover.propagate.dry_run',
1314 signature => { desc => q/
1315 @see open-ils.acq.fiscal_rollover.propagate
1316 This is the dry-run version. The action is performed,
1317 new fund information is returned, then all changes are rolled back.
1323 sub process_fiscal_rollover {
1324 my( $self, $conn, $auth, $year, $org_id, $descendants, $options ) = @_;
1326 my $e = new_editor(xact=>1, authtoken=>$auth);
1327 return $e->die_event unless $e->checkauth;
1328 return $e->die_event unless $e->allowed('ADMIN_FUND', $org_id);
1331 my $combined = ($self->api_name =~ /combined/);
1332 my $encumb_only = $U->is_true($options->{encumb_only}) ? 't' : 'f';
1334 my $org_ids = ($descendants) ?
1337 { $_->{id} } # fetch my descendants
1338 @{$e->json_query({from => ['actor.org_unit_descendants', $org_id]})}
1342 # Create next year's funds
1343 # Note, it's safe to run this more than once.
1344 # IOW, it will not create duplicate new funds.
1348 'acq.propagate_funds_by_org_tree' :
1349 'acq.propagate_funds_by_org_unit',
1350 $year, $e->requestor->id, $org_id
1356 # Roll the uncumbrances over to next year's funds
1357 # Mark the funds for $year as inactive
1362 'acq.rollover_funds_by_org_tree' :
1363 'acq.rollover_funds_by_org_unit',
1364 $year, $e->requestor->id, $org_id, $encumb_only
1369 # Fetch all funds for the specified org units for the subsequent year
1370 my $fund_ids = $e->search_acq_fund(
1371 [{ year => int($year) + 1,
1373 propagate => 't' }],
1377 foreach (@$fund_ids) {
1378 my $fund = $e->retrieve_acq_fund($_) or return $e->die_event;
1379 $fund->summary(retrieve_fund_summary_impl($e, $fund));
1382 if($combined and $U->is_true($fund->rollover)) {
1383 # see how much money was rolled over
1385 my $sum = $e->json_query({
1386 select => {acqftr => [{column => 'dest_amount', transform => 'sum'}]},
1388 where => {dest_fund => $fund->id, note => { like => 'Rollover%' } }
1391 $amount = $sum->{dest_amount} if $sum;
1394 $conn->respond({fund => $fund, rollover_amount => $amount});
1397 $self->api_name =~ /dry_run/ and $e->rollback or $e->commit;
1401 __PACKAGE__->register_method(
1402 method => 'org_fiscal_year',
1403 api_name => 'open-ils.acq.org_unit.current_fiscal_year',
1406 Returns the current fiscal year for the given org unit.
1407 If no fiscal year is configured, the current calendar
1411 {desc => 'Authentication token', type => 'string'},
1412 {desc => 'Org unit ID', type => 'number'}
1414 return => {desc => 'Year as a string (e.g. "2012")'}
1418 sub org_fiscal_year {
1419 my($self, $conn, $auth, $org_id) = @_;
1421 my $e = new_editor(authtoken => $auth);
1422 return $e->event unless $e->checkauth;
1424 my $year = $e->json_query({
1425 select => {acqfy => ['year']},
1426 from => {acqfy => {acqfc => {join => 'aou'}}},
1429 year_begin => {'<=' => 'now'},
1430 year_end => {'>=' => 'now'},
1432 '+aou' => {id => $org_id}
1436 return $year ? $year->{year} : DateTime->now->year;