1 package OpenILS::Application::Acq::Invoice;
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::Application::AppUtils;
10 my $U = 'OpenILS::Application::AppUtils';
13 # return nothing on success, event on failure
14 sub _prepare_fund_debit_for_inv_item {
15 my ($debit, $item, $e) = @_;
17 $debit->fund($item->fund);
18 $debit->amount($item->amount_paid);
19 $debit->origin_amount($item->amount_paid);
21 # future: cache funds locally
22 my $fund = $e->retrieve_acq_fund($item->fund) or return $e->die_event;
24 $debit->origin_currency_type($fund->currency_type);
25 $debit->encumbrance('f');
26 $debit->debit_type('direct_charge');
31 __PACKAGE__->register_method(
32 method => 'build_invoice_api',
33 api_name => 'open-ils.acq.invoice.update',
35 desc => q/Creates, updates, and deletes invoices, and related invoice entries, and invoice items/,
37 {desc => 'Authentication token', type => 'string'},
38 {desc => q/Invoice/, type => 'number'},
39 {desc => q/Entries. Array of 'acqie' objects/, type => 'array'},
40 {desc => q/Items. Array of 'acqii' objects/, type => 'array'},
41 {desc => q/Finalize PO's. Array of 'acqpo' ID's/, type => 'array'},
43 return => {desc => 'The invoice w/ entries and items attached', type => 'object', class => 'acqinv'}
48 sub build_invoice_impl {
49 my ($e, $invoice, $entries, $items, $do_commit, $finalize_pos) = @_;
53 if ($invoice->isnew) {
54 $invoice->recv_method('PPR') unless $invoice->recv_method;
55 $invoice->recv_date('now') unless $invoice->recv_date;
56 $e->create_acq_invoice($invoice) or return $e->die_event;
57 } elsif ($invoice->isdeleted) {
58 $e->delete_acq_invoice($invoice) or return $e->die_event;
60 $e->update_acq_invoice($invoice) or return $e->die_event;
66 for my $entry (@$entries) {
67 $entry->invoice($invoice->id);
70 $e->create_acq_invoice_entry($entry) or return $e->die_event;
71 return $evt if $evt = uncancel_copies_as_needed($e, $entry);
72 return $evt if $evt = update_entry_debits($e, $entry);
73 } elsif ($entry->isdeleted) {
74 # XXX Deleting entries does not recancel anything previously
76 return $evt if $evt = rollback_entry_debits($e, $entry);
77 $e->delete_acq_invoice_entry($entry) or return $e->die_event;
78 } elsif ($entry->ischanged) {
79 my $orig_entry = $e->retrieve_acq_invoice_entry($entry->id) or
82 if ($orig_entry->amount_paid != $entry->amount_paid or
83 $entry->phys_item_count != $orig_entry->phys_item_count) {
84 return $evt if $evt = rollback_entry_debits($e,$orig_entry);
86 # XXX Updates can only uncancel more LIDs when
87 # phys_item_count goes up, but cannot recancel them when
88 # phys_item_count goes down.
89 return $evt if $evt = uncancel_copies_as_needed($e, $entry);
91 return $evt if $evt = update_entry_debits($e, $entry);
94 $e->update_acq_invoice_entry($entry) or return $e->die_event;
100 for my $item (@$items) {
101 $item->invoice($invoice->id);
103 # future: cache item types
104 my $item_type = $e->retrieve_acq_invoice_item_type(
105 $item->inv_item_type) or return $e->die_event;
108 $e->create_acq_invoice_item($item) or return $e->die_event;
111 # This following complex conditional statement effecively means:
112 # 1) Items with item_types that are prorate are handled
114 # 2) Only items with a po_item, or which are linked to a fund
115 # already, or which belong to invoices which we're trying
116 # to *close* will actually go through this fund_debit
117 # creation process. In other cases, we'll consider it
118 # ok for an item to remain sans fund_debit for the time
121 if (not $U->is_true($item_type->prorate) and
122 ($item->po_item or $item->fund or
123 $U->is_true($invoice->complete))) {
126 if ($item->po_item) {
127 my $po_item = $e->retrieve_acq_po_item($item->po_item)
128 or return $e->die_event;
129 $debit = $e->retrieve_acq_fund_debit($po_item->fund_debit)
130 or return $e->die_event;
132 if ($U->is_true($item_type->blanket)) {
133 # Each payment toward a blanket charge results
134 # in a new debit to track the payment and a
135 # decrease in the original encumbrance by
136 # the amount paid on this invoice item
137 $debit->amount($debit->amount - $item->amount_paid);
138 $e->update_acq_fund_debit($debit) or return $e->die_event;
139 $debit = undef; # new debit created below
144 $debit = Fieldmapper::acq::fund_debit->new;
149 $evt = _prepare_fund_debit_for_inv_item($debit, $item, $e);
152 $e->create_acq_fund_debit($debit)
153 or return $e->die_event;
155 $e->update_acq_fund_debit($debit)
156 or return $e->die_event;
159 $item->fund_debit($debit->id);
160 $e->update_acq_invoice_item($item) or return $e->die_event;
162 } elsif ($item->isdeleted) {
163 $e->delete_acq_invoice_item($item) or return $e->die_event;
165 if ($item->po_item and
166 $e->retrieve_acq_po_item($item->po_item)->fund_debit == $item->fund_debit) {
167 # the debit is attached to the po_item. instead of
168 # deleting it, roll it back to being an encumbrance.
169 # Note: a prorated invoice_item that points to a
170 # po_item could point to a different fund_debit. We
171 # can't go back in time to collect all the prorated
172 # invoice_items (nor is the caller asking us too),
173 # so when that happens, just delete the extraneous
174 # debit (in the else block).
175 my $debit = $e->retrieve_acq_fund_debit($item->fund_debit);
176 $debit->encumbrance('t');
177 $e->update_acq_fund_debit($debit) or return $e->die_event;
179 } elsif ($item->fund_debit) {
181 my $inv_debit = $e->retrieve_acq_fund_debit($item->fund_debit);
183 if ($U->is_true($item_type->blanket)) {
184 # deleting a payment against a blanket charge means
185 # we have to re-encumber the paid amount by adding
186 # it back to the debit linked to the source po_item.
188 my $po_debit = $e->retrieve_acq_fund_debit($item->po_item->fund_debit);
189 $po_debit->amount($po_debit->amount + $inv_debit->amount);
191 $e->update_acq_fund_debit($po_debit)
192 or return $e->die_event;
195 $e->delete_acq_fund_debit($inv_debit) or return $e->die_event;
198 } elsif ($item->ischanged) {
201 if (!$item->fund_debit) {
202 # No fund_debit yet? Make one now.
203 $debit = Fieldmapper::acq::fund_debit->new;
207 $evt = _prepare_fund_debit_for_inv_item($debit, $item, $e);
209 $debit = $e->retrieve_acq_fund_debit($item->fund_debit) or
210 return $e->die_event;
213 if ($U->is_true($item_type->blanket)) {
214 # modifying a payment against a blanket charge means
215 # modifying the amount encumbered on the source debit
216 # by the same (but opposite) amount.
218 my $po_debit = $e->retrieve_acq_fund_debit(
219 $item->po_item->fund_debit);
221 my $delta = $debit->amount - $item->amount_paid;
222 $po_debit->amount($po_debit->amount + $delta);
223 $e->update_acq_fund_debit($po_debit) or return $e->die_event;
227 $debit->amount($item->amount_paid);
228 $debit->fund($item->fund);
231 # Making a new debit, so make it and link our item to it.
232 $e->create_acq_fund_debit($debit) or return $e->die_event;
233 $item->fund_debit($e->data->id);
235 $e->update_acq_fund_debit($debit) or return $e->die_event;
238 $e->update_acq_invoice_item($item) or return $e->die_event;
243 for my $po_id (@$finalize_pos) {
244 my $po = $e->retrieve_acq_purchase_order($po_id)
245 or return $e->die_event;
247 my $evt = finalize_blanket_po($e, $po);
251 $invoice = fetch_invoice_impl($e, $invoice->id);
253 $e->commit or return $e->die_event;
259 sub build_invoice_api {
260 my($self, $conn, $auth, $invoice, $entries, $items, $finalize_pos) = @_;
262 my $e = new_editor(xact => 1, authtoken=>$auth);
263 return $e->die_event unless $e->checkauth;
265 if (not ref $invoice) {
266 # caller only provided the ID
267 $invoice = $e->retrieve_acq_invoice($invoice) or return $e->die_event;
270 if (not $invoice->receiver and $invoice->isnew) {
271 $invoice->receiver($e->requestor->ws_ou);
274 return $e->die_event unless
275 $e->allowed('CREATE_INVOICE', $invoice->receiver);
277 return build_invoice_impl($e, $invoice, $entries, $items, 1, $finalize_pos);
281 sub rollback_entry_debits {
283 my $debits = find_entry_debits($e, $entry, 'f', entry_amount_per_item($entry));
284 my $lineitem = $e->retrieve_acq_lineitem($entry->lineitem) or return $e->die_event;
286 for my $debit (@$debits) {
287 # revert to the original estimated amount re-encumber
288 $debit->encumbrance('t');
289 $debit->amount($lineitem->estimated_unit_price());
291 # debit is no longer "invoiced"; detach it from the entry;
292 $debit->clear_invoice_entry;
294 $e->update_acq_fund_debit($debit) or return $e->die_event;
295 update_copy_cost($e, $debit) or return $e->die_event; # clear the cost
301 sub update_entry_debits {
304 my $debits = find_entry_debits($e, $entry, 't');
305 return undef unless @$debits;
307 if($entry->phys_item_count > @$debits) {
309 # We can't invoice for more items than we have debits for
310 return OpenILS::Event->new(
311 'ACQ_INVOICE_ENTRY_COUNT_EXCEEDS_DEBITS',
312 payload => {entry => $entry->id});
315 for my $debit (@$debits) {
316 my $amount = entry_amount_per_item($entry);
317 $debit->amount($amount);
318 $debit->encumbrance('f');
320 # debit always reports the invoice_entry responsible
321 # for its most recent modification.
322 $debit->invoice_entry($entry->id);
324 $e->update_acq_fund_debit($debit) or return $e->die_event;
326 # TODO: this does not reflect ancillary charges, like taxes, etc.
327 # We may need a way to indicate whether the amount attached to an
328 # invoice_item should be prorated and included in the copy cost.
329 # Note that acq.invoice_item_type.prorate does not necessarily
330 # mean a charge should be included in the copy price, only that
331 # it should spread accross funds.
332 update_copy_cost($e, $debit, $amount) or return $e->die_event;
338 # This was originally done only for EDI invoices, but needs added to the
339 # manual invoice-entering process for consistency's sake.
340 sub uncancel_copies_as_needed {
341 my ($e, $entry) = @_;
343 return unless $entry->lineitem and $entry->phys_item_count;
345 my $li = $e->retrieve_acq_lineitem($entry->lineitem) or
346 return $e->die_event;
348 # if an invoiced lineitem is marked as cancelled
349 # (e.g. back-order), invoicing the lineitem implies
350 # we need to un-cancel it
352 # collect the LIDs, starting with those that are
353 # not cancelled, followed by those that have keep-debits cancel_reasons,
354 # followed by non-keep-debit cancel reasons.
356 my $lid_ids = $e->json_query({
357 select => {acqlid => ['id']},
360 acqcr => {type => 'left'},
361 acqfdeb => {type => 'left'}
365 '+acqlid' => {lineitem => $li->id},
366 '+acqfdeb' => {encumbrance => 't'} # not-yet invoiced copies
370 field => 'keep_debits',
373 limit => $entry->phys_item_count # crucial
376 for my $lid_id (map {$_->{id}} @$lid_ids) {
377 my $lid = $e->retrieve_acq_lineitem_detail($lid_id);
378 next unless $lid->cancel_reason;
381 "un-cancelling invoice lineitem " . $li->id .
382 " lineitem_detail " . $lid_id
384 $lid->clear_cancel_reason;
385 return $e->die_event unless $e->update_acq_lineitem_detail($lid);
388 $li->clear_cancel_reason;
389 $li->state("on-order") if $li->state eq "cancelled"; # sic
390 $li->edit_time("now");
392 unless ($e->update_acq_lineitem($li)) {
393 my $evt = $e->die_event;
394 $logger->error("couldn't clear li cancel reason: ". $evt->{textcode});
402 # update the linked copy to reflect the amount paid for the item
403 # returns true on success, false on error
404 sub update_copy_cost {
405 my ($e, $debit, $amount) = @_;
407 my $lid = $e->search_acq_lineitem_detail([
408 {fund_debit => $debit->id},
409 {flesh => 1, flesh_fields => {acqlid => ['eg_copy_id']}}
412 if($lid and my $copy = $lid->eg_copy_id) {
413 defined $amount and $copy->cost($amount) or $copy->clear_cost;
415 # XXX It would be nice to have a way to record that a copy was
416 # updated by a non-user mechanism, like EDI, but we don't have
417 # a clear way to do that here.
419 $copy->editor($e->requestor->id);
420 $copy->edit_date('now');
423 $e->update_asset_copy($copy) or return 0;
430 sub entry_amount_per_item {
432 return $entry->amount_paid if $U->is_true($entry->billed_per_item);
433 return 0 if $entry->phys_item_count == 0;
434 return $entry->amount_paid / $entry->phys_item_count;
437 sub easy_money { # TODO XXX replace with something from a library
440 my $rounded = int($val * 100) / 100.0;
441 if ($rounded == $val) {
442 return sprintf("%.02f", $val);
444 return sprintf("%g", $val);
448 # 0 on failure (caller should call $e->die_event), array on success
449 sub amounts_spent_per_fund {
450 my ($e, $inv_id) = @_;
452 my $entries = $e->search_acq_invoice_entry({"invoice" => $inv_id}) or
455 my $items = $e->search_acq_invoice_item({"invoice" => $inv_id}) or
459 foreach my $entry (@$entries) {
460 my $debits = find_entry_debits($e, $entry, "f") or return 0;
462 $totals_by_fund{$_->fund} ||= 0.0;
463 $totals_by_fund{$_->fund} += $_->amount;
467 foreach my $item (@$items) {
468 next unless $item->fund and $item->amount_paid;
469 $totals_by_fund{$item->fund} ||= 0.0;
470 $totals_by_fund{$item->fund} += $item->amount_paid;
474 foreach my $fund_id (keys %totals_by_fund) {
475 my $fund = $e->retrieve_acq_fund($fund_id) or return 0;
477 "fund" => $fund->to_bare_hash,
478 "total" => easy_money($totals_by_fund{$fund_id})
485 # find fund debits related to an invoice entry.
486 sub find_entry_debits {
487 my($e, $entry, $encumbrance, $amount, $fallback) = @_;
490 select => {acqfdeb => ['id']},
491 # sort received items to the front
492 order_by => {'acqlid' => ['recv_time']}
495 if ($encumbrance eq 'f' and !$fallback) { # previously invoiced
497 # Debits which have been invoiced (encumbrance = f) will have a
498 # link to the last entry which affected them
500 $query->{from} = {acqfdeb => 'acqlid'};
501 $query->{where} = {'+acqfdeb' => {invoice_entry => $entry->id}};
505 # For un-invoiced (or $fallback) debits, search for those
506 # that are linked to the entry via the lineitem.
515 filter => {id => $entry->id}
524 $query->{limit} = $entry->phys_item_count;
525 $query->{where} = {'+acqfdeb' => {encumbrance => $encumbrance}};
528 $query->{where}->{'+acqfdeb'}->{amount} = $amount if $amount;
530 my $debits = $e->json_query($query);
531 my $debit_ids = [map { $_->{id} } @$debits];
533 if (!@$debit_ids) { # no debits found
535 # if a lookup for previously invoiced debits (encumbrance=f)
536 # returns zero results, it may be becuase the debits were
537 # created before the presence of the acq.fund_debit.invoice_entry
538 # column. Attempt to use the old-style lookup for these debits
539 # using the "$fallback" flag.
540 if (!$fallback and $encumbrance eq 'f') {
542 "invoice: using debit fallback lookup for entry ".$entry->id);
543 return find_entry_debits($e, $entry, $encumbrance, $amount, 1);
549 return $e->search_acq_fund_debit({id => $debit_ids});
553 __PACKAGE__->register_method(
554 method => 'build_invoice_api',
555 api_name => 'open-ils.acq.invoice.retrieve',
558 desc => q/Creates a new stub invoice/,
560 {desc => 'Authentication token', type => 'string'},
561 {desc => q/Invoice Id/, type => 'number'},
563 return => {desc => 'The new invoice w/ entries and items attached', type => 'object', class => 'acqinv'}
568 sub fetch_invoice_api {
569 my($self, $conn, $auth, $invoice_id, $options) = @_;
571 my $e = new_editor(authtoken=>$auth);
572 return $e->event unless $e->checkauth;
574 my $invoice = fetch_invoice_impl($e, $invoice_id, $options) or
576 return $e->event unless $e->allowed(['VIEW_INVOICE', 'CREATE_INVOICE'], $invoice->receiver);
581 sub fetch_invoice_impl {
582 my ($e, $invoice_id, $options) = @_;
586 my $args = $options->{"no_flesh_misc"} ? $invoice_id : [
591 "acqinv" => ["entries", "items"],
592 "acqii" => ["fund_debit", "purchase_order", "po_item"]
597 return $e->retrieve_acq_invoice($args);
600 __PACKAGE__->register_method(
601 method => 'prorate_invoice',
602 api_name => 'open-ils.acq.invoice.apply_prorate',
605 For all invoice items that have the prorate flag set to true, this will create the necessary
606 additional invoice_item's to prorate the cost across all affected funds by percent spent for each fund.
609 {desc => 'Authentication token', type => 'string'},
610 {desc => q/Invoice Id/, type => 'number'},
612 return => {desc => 'The updated invoice w/ entries and items attached', type => 'object', class => 'acqinv'}
617 sub prorate_invoice {
618 my($self, $conn, $auth, $invoice_id) = @_;
620 my $e = new_editor(xact => 1, authtoken=>$auth);
621 return $e->die_event unless $e->checkauth;
623 my $invoice = fetch_invoice_impl($e, $invoice_id) or return $e->die_event;
624 return $e->die_event unless $e->allowed('CREATE_INVOICE', $invoice->receiver);
627 push(@lid_debits, @{find_entry_debits($e, $_, 'f', entry_amount_per_item($_))}) for @{$invoice->entries};
629 my $inv_items = $e->search_acq_invoice_item([
630 {"invoice" => $invoice_id, "fund_debit" => {"!=" => undef}},
631 {"flesh" => 1, "flesh_fields" => {"acqii" => ["fund_debit"]}}
632 ]) or return $e->die_event;
634 my @item_debits = map { $_->fund_debit } @$inv_items;
637 my $total_entry_paid = 0;
638 for my $debit (@lid_debits, @item_debits) {
639 $fund_totals{$debit->fund} = 0 unless $fund_totals{$debit->fund};
640 $fund_totals{$debit->fund} += $debit->amount;
641 $total_entry_paid += $debit->amount;
644 $logger->info("invoice: prorating against invoice amount $total_entry_paid");
646 for my $item (@{$invoice->items}) {
648 next if $item->fund_debit; # item has already been processed
650 # future: cache item types locally
651 my $item_type = $e->retrieve_acq_invoice_item_type($item->inv_item_type) or return $e->die_event;
652 next unless $U->is_true($item_type->prorate);
654 # Prorate charges across applicable funds
655 my $full_item_paid = $item->amount_paid; # total amount paid for this item before splitting
656 my $full_item_cost = $item->cost_billed; # total amount invoiced for this item before splitting
660 my $total_debited = 0;
661 my $total_costed = 0;
663 for my $fund_id (keys %fund_totals) {
665 my $spent_for_fund = $fund_totals{$fund_id};
666 next unless $spent_for_fund > 0;
668 my $prorated_amount = ($spent_for_fund / $total_entry_paid) * $full_item_paid;
669 my $prorated_cost = ($spent_for_fund / $total_entry_paid) * $full_item_cost;
670 $logger->info("invoice: attaching prorated amount $prorated_amount to fund $fund_id for invoice $invoice_id");
673 if($first_round and $item->po_item) {
674 # if this item is the result of a PO item, repurpose the original debit
675 # for the first chunk of the prorated amount
676 $debit = $e->retrieve_acq_fund_debit($item->po_item->fund_debit);
678 $debit = Fieldmapper::acq::fund_debit->new;
682 $debit->fund($fund_id);
683 $debit->amount($prorated_amount);
684 $debit->origin_amount($prorated_amount);
685 $debit->origin_currency_type($e->retrieve_acq_fund($fund_id)->currency_type); # future: cache funds locally
686 $debit->encumbrance('f');
687 $debit->debit_type('prorated_charge');
690 $e->create_acq_fund_debit($debit) or return $e->die_event;
692 $e->update_acq_fund_debit($debit) or return $e->die_event;
695 $total_debited += $prorated_amount;
696 $total_costed += $prorated_cost;
697 $largest_debit = $debit if !$largest_debit or $prorated_amount > $largest_debit->amount;
701 # re-purpose the original invoice_item for the first prorated amount
702 $item->fund($fund_id);
703 $item->fund_debit($debit->id);
704 $item->amount_paid($prorated_amount);
705 $item->cost_billed($prorated_cost);
706 $e->update_acq_invoice_item($item) or return $e->die_event;
707 $largest_item = $item if !$largest_item or $prorated_amount > $largest_item->amount_paid;
711 # for subsequent prorated amounts, create a new invoice_item
712 my $new_item = $item->clone;
714 $new_item->fund($fund_id);
715 $new_item->fund_debit($debit->id);
716 $new_item->amount_paid($prorated_amount);
717 $new_item->cost_billed($prorated_cost);
718 $e->create_acq_invoice_item($new_item) or return $e->die_event;
719 $largest_item = $new_item if !$largest_item or $prorated_amount > $largest_item->amount_paid;
725 # make sure the percentages didn't leave a small sliver of money over/under-debited
726 # if so, tweak the largest debit to smooth out the difference
727 if($total_debited != $full_item_paid or $total_costed != $full_item_cost) {
729 my $paid_diff = $full_item_paid - $total_debited;
730 my $cost_diff = $full_item_cost - $total_debited;
731 $logger->info("invoice: repairing prorate descrepency of paid:$paid_diff and cost:$cost_diff");
732 my $new_paid = $largest_item->amount_paid + $paid_diff;
733 my $new_cost = $largest_item->cost_billed + $cost_diff;
735 $largest_debit = $e->retrieve_acq_fund_debit($largest_debit->id); # get latest copy
736 $largest_debit->amount($new_paid);
737 $e->update_acq_fund_debit($largest_debit) or return $e->die_event;
739 $largest_item = $e->retrieve_acq_invoice_item($largest_item->id); # get latest copy
740 $largest_item->amount_paid($new_paid);
741 $largest_item->cost_billed($new_cost);
743 $e->update_acq_invoice_item($largest_item) or return $e->die_event;
747 $invoice = fetch_invoice_impl($e, $invoice_id);
754 __PACKAGE__->register_method(
755 method => "print_html_invoice",
756 api_name => "open-ils.acq.invoice.print.html",
759 desc => "Retrieve printable HTML vouchers for each given invoice",
761 {desc => "Authentication token", type => "string"},
762 {desc => "Invoice ID or a list of them", type => "mixed"},
765 desc => q{One A/T event containing a printable HTML voucher for
767 type => "object", class => "atev"}
772 sub print_html_invoice {
773 my ($self, $conn, $auth, $id_list) = @_;
775 my $e = new_editor("authtoken" => $auth);
776 return $e->die_event unless $e->checkauth;
778 $id_list = [$id_list] unless ref $id_list;
780 my $invoices = $e->search_acq_invoice({"id" => $id_list}) or
781 return $e->die_event;
783 foreach my $invoice (@$invoices) {
784 return $e->die_event unless
785 $e->allowed("VIEW_INVOICE", $invoice->receiver);
787 my $amounts = amounts_spent_per_fund($e, $invoice->id) or
788 return $e->die_event;
791 $U->fire_object_event(
792 undef, "format.acqinv.html", $invoice, $invoice->receiver,
793 "print-on-demand", $amounts
802 __PACKAGE__->register_method(
803 method => 'finalize_blanket_po_api',
804 api_name => 'open-ils.acq.purchase_order.blanket.finalize',
807 1. Set encumbered amount to zero for all blanket po_item's
808 2. If the PO does not have any outstanding lineitems, mark
809 the PO as 'received'.
812 {desc => 'Authentication token', type => 'string'},
813 {desc => q/PO ID/, type => 'number'}
815 return => {desc => '1 on success, event on error'}
819 sub finalize_blanket_po_api {
820 my ($self, $client, $auth, $po_id) = @_;
822 my $e = new_editor(xact => 1, authtoken=>$auth);
823 return $e->die_event unless $e->checkauth;
825 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
827 return $e->die_event unless
828 $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
830 my $evt = finalize_blanket_po($e, $po);
838 # 1. set any remaining blanket encumbrances to $0.
839 # 2. mark the PO as received if there are no pending lineitems.
840 sub finalize_blanket_po {
845 # blanket po_items on this PO
846 my $blanket_items = $e->json_query({
847 select => {acqpoi => ['id']},
848 from => {acqpoi => {aiit => {}}},
850 '+aiit' => {blanket => 't'},
851 '+acqpoi' => {purchase_order => $po_id}
855 for my $item_id (map { $_->{id} } @$blanket_items) {
857 my $item = $e->retrieve_acq_po_item([
860 flesh_fields => {acqpoi => ['fund_debit']}
864 my $debit = $item->fund_debit or next;
866 next unless $U->is_true($debit->encumbrance);
869 $debit->encumbrance('f');
870 $e->update_acq_fund_debit($debit) or return $e->die_event;
873 # Number of pending lineitems on this PO.
874 # If there are any, we don't mark 'received'
875 my $li_count = $e->json_query({
876 select => {jub => [{column => 'id', transform => 'count'}]},
880 purchase_order => $po_id,
886 if ($li_count->{count} > 0) {
887 $logger->info("skipping 'received' state change for po $po_id ".
888 "during finalization, because PO has pending lineitems");
892 $po->state('received');
893 $po->edit_time('now');
894 $po->editor($e->requestor->id);
896 $e->update_acq_purchase_order($po) or return $e->die_event;