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',
825 desc => 'Retrieves a purchase order',
827 {desc => 'Authentication token', type => 'string'},
828 {desc => 'purchase_order to retrieve', type => 'number'},
829 {desc => q/Options hash. flesh_lineitems, to get the lineitems and lineitem_attrs;
830 clear_marc, to clear the MARC data from the lineitem (for reduced bandwidth)
831 li_limit : number of lineitems to return if fleshing line items;
832 li_offset : lineitem offset if fleshing line items
833 li_order_by : lineitem sort definition if fleshing line items,
834 flesh_po_items : po_item objects
838 return => {desc => 'The purchase order, Event on failure'}
842 sub retrieve_purchase_order {
843 my($self, $conn, $auth, $po_id, $options) = @_;
844 my $e = new_editor(authtoken=>$auth);
845 return $e->event unless $e->checkauth;
847 $po_id = [ $po_id ] unless ref $po_id;
850 if ( po_perm_failure($e, $_) )
853 { $rv = retrieve_purchase_order_impl($e, $_, $options) }
862 # if the user does not have permission to perform actions on this PO, return the perm failure event
863 sub po_perm_failure {
864 my($e, $po_id, $fund_id) = @_;
865 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
866 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency, $po);
870 sub build_price_summary {
871 my ($e, $po_id) = @_;
873 # TODO: Add summary value for estimated amount (pre-encumber)
875 # fetch the fund debits for this purchase order
876 my $debits = $e->json_query({
877 "select" => {"acqfdeb" => [qw/encumbrance amount/]},
881 "fkey" => "lineitem",
885 "fkey" => "purchase_order", "field" => "id"
889 "acqfdeb" => {"fkey" => "fund_debit", "field" => "id"}
892 "where" => {"+acqpo" => {"id" => $po_id}}
895 # add any debits for non-bib po_items
898 "select" => {"acqfdeb" => [qw/encumbrance amount/]},
899 "from" => {acqpoi => 'acqfdeb'},
900 "where" => {"+acqpoi" => {"purchase_order" => $po_id}}
904 my ($enc, $spent) = (0, 0);
905 for my $deb (@$debits) {
906 if($U->is_true($deb->{encumbrance})) {
907 $enc += $deb->{amount};
909 $spent += $deb->{amount};
916 sub retrieve_purchase_order_impl {
917 my($e, $po_id, $options) = @_;
919 my $flesh = {"flesh" => 1, "flesh_fields" => {"acqpo" => []}};
922 unless ($options->{"no_flesh_cancel_reason"}) {
923 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "cancel_reason";
925 if ($options->{"flesh_notes"}) {
926 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
928 if ($options->{"flesh_provider"}) {
929 push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider";
932 push (@{$flesh->{flesh_fields}->{acqpo}}, 'po_items') if $options->{flesh_po_items};
934 my $args = (@{$flesh->{"flesh_fields"}->{"acqpo"}}) ?
935 [$po_id, $flesh] : $po_id;
937 my $po = $e->retrieve_acq_purchase_order($args)
940 if($$options{flesh_lineitems}) {
942 my $flesh_fields = { jub => ['attributes'] };
943 $flesh_fields->{jub}->[1] = 'lineitem_details' if $$options{flesh_lineitem_details};
944 $flesh_fields->{acqlid} = ['fund_debit'] if $$options{flesh_fund_debit};
946 my $items = $e->search_acq_lineitem([
947 {purchase_order => $po_id},
950 flesh_fields => $flesh_fields,
951 limit => $$options{li_limit} || 50,
952 offset => $$options{li_offset} || 0,
953 order_by => {jub => $$options{li_order_by} || 'create_time'}
957 if($$options{clear_marc}) {
958 $_->clear_marc for @$items;
961 $po->lineitems($items);
962 $po->lineitem_count(scalar(@$items));
964 } elsif( $$options{flesh_lineitem_ids} ) {
965 $po->lineitems($e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1}));
967 } elsif( $$options{flesh_lineitem_count} ) {
969 my $items = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist=>1});
970 $po->lineitem_count(scalar(@$items));
973 if($$options{flesh_price_summary}) {
974 my ($enc, $spent) = build_price_summary($e, $po_id);
975 $po->amount_encumbered($enc);
976 $po->amount_spent($spent);
983 __PACKAGE__->register_method(
984 method => 'format_po',
985 api_name => 'open-ils.acq.purchase_order.format'
989 my($self, $conn, $auth, $po_id, $format) = @_;
990 my $e = new_editor(authtoken=>$auth);
991 return $e->event unless $e->checkauth;
993 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
994 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
996 my $hook = "format.po.$format";
997 return $U->fire_object_event(undef, $hook, $po, $po->ordering_agency);
1000 __PACKAGE__->register_method(
1001 method => 'format_lineitem',
1002 api_name => 'open-ils.acq.lineitem.format'
1005 sub format_lineitem {
1006 my($self, $conn, $auth, $li_id, $format, $user_data) = @_;
1007 my $e = new_editor(authtoken=>$auth);
1008 return $e->event unless $e->checkauth;
1010 my $li = $e->retrieve_acq_lineitem($li_id) or return $e->event;
1013 if (defined $li->purchase_order) {
1014 my $po = $e->retrieve_acq_purchase_order($li->purchase_order) or return $e->die_event;
1015 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
1016 $context_org = $po->ordering_agency;
1018 my $pl = $e->retrieve_acq_picklist($li->picklist) or return $e->die_event;
1019 if($e->requestor->id != $pl->owner) {
1020 return $e->event unless
1021 $e->allowed('VIEW_PICKLIST', $pl->org_unit, $pl);
1023 $context_org = $pl->org_unit;
1026 my $hook = "format.acqli.$format";
1027 return $U->fire_object_event(undef, $hook, $li, $context_org, 'print-on-demand', $user_data);
1030 __PACKAGE__->register_method (
1031 method => 'po_events',
1032 api_name => 'open-ils.acq.purchase_order.events.owner',
1035 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1036 @param authtoken Login session key
1037 @param owner Id or array of id's for the purchase order Owner field. Filters the events to just those pertaining to PO's meeting this criteria.
1038 @param options Object for tweaking the selection criteria and fleshing options.
1042 __PACKAGE__->register_method (
1043 method => 'po_events',
1044 api_name => 'open-ils.acq.purchase_order.events.ordering_agency',
1047 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1048 @param authtoken Login session key
1049 @param owner Id or array of id's for the purchase order Ordering Agency field. Filters the events to just those pertaining to PO's meeting this criteria.
1050 @param options Object for tweaking the selection criteria and fleshing options.
1054 __PACKAGE__->register_method (
1055 method => 'po_events',
1056 api_name => 'open-ils.acq.purchase_order.events.id',
1059 Retrieve EDI-related purchase order events (format.po.jedi), by default those which are pending.
1060 @param authtoken Login session key
1061 @param owner Id or array of id's for the purchase order Id field. Filters the events to just those pertaining to PO's meeting this criteria.
1062 @param options Object for tweaking the selection criteria and fleshing options.
1067 my($self, $conn, $auth, $search_value, $options) = @_;
1068 my $e = new_editor(authtoken => $auth);
1069 return $e->event unless $e->checkauth;
1071 (my $search_field = $self->api_name) =~ s/.*\.([_a-z]+)$/$1/;
1072 my $obj_type = 'acqpo';
1074 if ($search_field eq 'ordering_agency') {
1075 $search_value = $U->get_org_descendants($search_value);
1079 "select"=>{"atev"=>["id"]},
1084 "select"=>{$obj_type=>["id"]},
1086 "where"=>{$search_field=>$search_value}
1091 "select"=>{atevdef=>["id"]},
1094 "hook"=>"format.po.jedi"
1100 "order_by"=>[{"class"=>"atev", "field"=>"run_time", "direction"=>"desc"}]
1103 if ($options && defined $options->{state}) {
1104 $query->{'where'}{'state'} = $options->{state}
1107 if ($options && defined $options->{start_time}) {
1108 $query->{'where'}{'start_time'} = $options->{start_time};
1111 if ($options && defined $options->{order_by}) {
1112 $query->{'order_by'} = $options->{order_by};
1114 my $po_events = $e->json_query($query);
1116 my $flesh_fields = { 'atev' => [ 'event_def' ] };
1117 my $flesh_depth = 1;
1119 for my $id (@$po_events) {
1120 my $event = $e->retrieve_action_trigger_event([
1122 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
1124 if (! $event) { next; }
1126 my $po = retrieve_purchase_order_impl(
1129 {flesh_lineitem_count=>1,flesh_price_summary=>1}
1132 if ($e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() )) {
1133 $event->target( $po );
1134 $conn->respond($event);
1141 __PACKAGE__->register_method (
1142 method => 'update_po_events',
1143 api_name => 'open-ils.acq.purchase_order.event.cancel.batch',
1146 __PACKAGE__->register_method (
1147 method => 'update_po_events',
1148 api_name => 'open-ils.acq.purchase_order.event.reset.batch',
1152 sub update_po_events {
1153 my($self, $conn, $auth, $event_ids) = @_;
1154 my $e = new_editor(xact => 1, authtoken => $auth);
1155 return $e->die_event unless $e->checkauth;
1158 for my $id (@$event_ids) {
1160 # do a little dance to determine what libraries we are ultimately affecting
1161 my $event = $e->retrieve_action_trigger_event([
1164 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
1166 ]) or return $e->die_event;
1168 my $po = retrieve_purchase_order_impl(
1174 return $e->die_event unless $e->allowed( ['CREATE_PURCHASE_ORDER','VIEW_PURCHASE_ORDER'], $po->ordering_agency() );
1176 if($self->api_name =~ /cancel/) {
1177 $event->state('invalid');
1178 } elsif($self->api_name =~ /reset/) {
1179 $event->clear_start_time;
1180 $event->clear_update_time;
1181 $event->state('pending');
1184 $e->update_action_trigger_event($event) or return $e->die_event;
1185 $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
1189 return {complete => 1};
1193 __PACKAGE__->register_method (
1194 method => 'process_fiscal_rollover',
1195 api_name => 'open-ils.acq.fiscal_rollover.combined',
1199 Performs a combined fiscal fund rollover process.
1201 Creates a new series of funds for the following year, copying the old years
1202 funds that are marked as propagable. They apply to the funds belonging to
1203 either an org unit or to an org unit and all of its dependent org units.
1204 The procedures may be run repeatedly; if any fund has already been propagated,
1205 both the old and the new funds will be left alone.
1207 Closes out any applicable funds (by org unit or by org unit and dependents)
1208 that are marked as propagable. If such a fund has not already been propagated
1209 to the new year, it will be propagated at closing time.
1211 If a fund is marked as subject to rollover, any unspent balance in the old year's
1212 fund (including money encumbered but not spent) is transferred to the new year's
1213 fund. Otherwise it is deallocated back to the funding source(s).
1215 In either case, any encumbrance debits are transferred to the new fund, along
1216 with the corresponding lineitem details. The old year's fund is marked as inactive
1217 so that new debits may not be charged to it.
1220 {desc => 'Authentication token', type => 'string'},
1221 {desc => 'Fund Year to roll over', type => 'integer'},
1222 {desc => 'Org unit ID', type => 'integer'},
1223 {desc => 'Include Descendant Orgs (boolean)', type => 'integer'},
1225 return => {desc => 'Returns a stream of all related funds for the next year including fund summary for each'}
1230 __PACKAGE__->register_method (
1231 method => 'process_fiscal_rollover',
1232 api_name => 'open-ils.acq.fiscal_rollover.combined.dry_run',
1236 @see open-ils.acq.fiscal_rollover.combined
1237 This is the dry-run version. The action is performed,
1238 new fund information is returned, then all changes are rolled back.
1244 __PACKAGE__->register_method (
1245 method => 'process_fiscal_rollover',
1246 api_name => 'open-ils.acq.fiscal_rollover.propagate',
1250 @see open-ils.acq.fiscal_rollover.combined
1251 This version performs fund propagation only. I.e, creation of
1252 the following year's funds. It does not rollover over balances, encumbrances,
1253 or mark the previous year's funds as complete.
1258 __PACKAGE__->register_method (
1259 method => 'process_fiscal_rollover',
1260 api_name => 'open-ils.acq.fiscal_rollover.propagate.dry_run',
1262 signature => { desc => q/
1263 @see open-ils.acq.fiscal_rollover.propagate
1264 This is the dry-run version. The action is performed,
1265 new fund information is returned, then all changes are rolled back.
1271 sub process_fiscal_rollover {
1272 my( $self, $conn, $auth, $year, $org_id, $descendants, $options ) = @_;
1274 my $e = new_editor(xact=>1, authtoken=>$auth);
1275 return $e->die_event unless $e->checkauth;
1276 return $e->die_event unless $e->allowed('ADMIN_FUND', $org_id);
1279 my $combined = ($self->api_name =~ /combined/);
1281 my $org_ids = ($descendants) ?
1284 { $_->{id} } # fetch my descendants
1285 @{$e->json_query({from => ['actor.org_unit_descendants', $org_id]})}
1289 # Create next year's funds
1290 # Note, it's safe to run this more than once.
1291 # IOW, it will not create duplicate new funds.
1295 'acq.propagate_funds_by_org_tree' :
1296 'acq.propagate_funds_by_org_unit',
1297 $year, $e->requestor->id, $org_id
1303 # Roll the uncumbrances over to next year's funds
1304 # Mark the funds for $year as inactive
1309 'acq.rollover_funds_by_org_tree' :
1310 'acq.rollover_funds_by_org_unit',
1311 $year, $e->requestor->id, $org_id
1316 # Fetch all funds for the specified org units for the subsequent year
1317 my $fund_ids = $e->search_acq_fund([
1319 year => int($year) + 1,
1323 limit => $$options{limit} || 20,
1324 offset => $$options{offset} || 0,
1330 foreach (@$fund_ids) {
1331 my $fund = $e->retrieve_acq_fund($_) or return $e->die_event;
1332 $fund->summary(retrieve_fund_summary_impl($e, $fund));
1335 if($combined and $U->is_true($fund->rollover)) {
1336 # see how much money was rolled over
1338 my $sum = $e->json_query({
1339 select => {acqftr => [{column => 'dest_amount', transform => 'sum'}]},
1341 where => {dest_fund => $fund->id, note => 'Rollover'}
1344 $amount = $sum->{dest_amount} if $sum;
1347 $conn->respond({fund => $fund, rollover_amount => $amount});
1350 $self->api_name =~ /dry_run/ and $e->rollback or $e->commit;