]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm
LP#1440114 Blanket order PO "finalize"
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Acq / Invoice.pm
1 package OpenILS::Application::Acq::Invoice;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
4
5 use OpenSRF::Utils::Logger qw(:logger);
6 use OpenILS::Utils::Fieldmapper;
7 use OpenILS::Utils::CStoreEditor q/:funcs/;
8 use OpenILS::Application::AppUtils;
9 use OpenILS::Event;
10 my $U = 'OpenILS::Application::AppUtils';
11
12
13 # return nothing on success, event on failure
14 sub _prepare_fund_debit_for_inv_item {
15     my ($debit, $item, $e) = @_;
16
17     $debit->fund($item->fund);
18     $debit->amount($item->amount_paid);
19     $debit->origin_amount($item->amount_paid);
20
21     # future: cache funds locally
22     my $fund = $e->retrieve_acq_fund($item->fund) or return $e->die_event;
23
24     $debit->origin_currency_type($fund->currency_type);
25     $debit->encumbrance('f');
26     $debit->debit_type('direct_charge');
27
28     return;
29 }
30
31 __PACKAGE__->register_method(
32     method => 'build_invoice_api',
33     api_name    => 'open-ils.acq.invoice.update',
34     signature => {
35         desc => q/Creates, updates, and deletes invoices, and related invoice entries, and invoice items/,
36         params => [
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'},
42         ],
43         return => {desc => 'The invoice w/ entries and items attached', type => 'object', class => 'acqinv'}
44     }
45 );
46
47
48 sub build_invoice_impl {
49     my ($e, $invoice, $entries, $items, $do_commit, $finalize_pos) = @_;
50
51     $finalize_pos ||= [];
52
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;
59     } else {
60         $e->update_acq_invoice($invoice) or return $e->die_event;
61     }
62
63     my $evt;
64
65     if ($entries) {
66         for my $entry (@$entries) {
67             $entry->invoice($invoice->id);
68
69             if ($entry->isnew) {
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
75                 # uncanceled.
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
80                     return $e->die_event;
81
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);
85
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);
90
91                     return $evt if $evt = update_entry_debits($e, $entry);
92                 }
93
94                 $e->update_acq_invoice_entry($entry) or return $e->die_event;
95             }
96         }
97     }
98
99     if ($items) {
100         for my $item (@$items) {
101             $item->invoice($invoice->id);
102                 
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;
106
107             if ($item->isnew) {
108                 $e->create_acq_invoice_item($item) or return $e->die_event;
109
110
111                 # This following complex conditional statement effecively means:
112                 #   1) Items with item_types that are prorate are handled
113                 #       differently.
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
119                 #       being.
120
121                 if (not $U->is_true($item_type->prorate) and
122                     ($item->po_item or $item->fund or
123                         $U->is_true($invoice->complete))) {
124
125                     my $debit;
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;
131
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
140                         }
141                     }
142
143                     if (!$debit) {
144                         $debit = Fieldmapper::acq::fund_debit->new;
145                         $debit->isnew(1);
146                     }
147
148                     return $evt if
149                         $evt = _prepare_fund_debit_for_inv_item($debit, $item, $e);
150
151                     if ($debit->isnew) {
152                         $e->create_acq_fund_debit($debit)
153                             or return $e->die_event;
154                     } else {
155                         $e->update_acq_fund_debit($debit)
156                             or return $e->die_event;
157                     }
158
159                     $item->fund_debit($debit->id);
160                     $e->update_acq_invoice_item($item) or return $e->die_event;
161                 }
162             } elsif ($item->isdeleted) {
163                 $e->delete_acq_invoice_item($item) or return $e->die_event;
164
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;
178
179                 } elsif ($item->fund_debit) {
180
181                     my $inv_debit = $e->retrieve_acq_fund_debit($item->fund_debit);
182
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.
187
188                         my $po_debit = $e->retrieve_acq_fund_debit($item->po_item->fund_debit);
189                         $po_debit->amount($po_debit->amount + $inv_debit->amount);
190
191                         $e->update_acq_fund_debit($po_debit) 
192                             or return $e->die_event;
193                     }
194
195                     $e->delete_acq_fund_debit($inv_debit) or return $e->die_event;
196                 }
197
198             } elsif ($item->ischanged) {
199                 my $debit;
200
201                 if (!$item->fund_debit) {
202                     # No fund_debit yet? Make one now.
203                     $debit = Fieldmapper::acq::fund_debit->new;
204                     $debit->isnew(1);
205
206                     return $evt if
207                         $evt = _prepare_fund_debit_for_inv_item($debit, $item, $e);
208                 } else {
209                     $debit = $e->retrieve_acq_fund_debit($item->fund_debit) or
210                         return $e->die_event;
211                 }
212
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.
217
218                     my $po_debit = $e->retrieve_acq_fund_debit(
219                         $item->po_item->fund_debit);
220
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;
224                 }
225
226
227                 $debit->amount($item->amount_paid);
228                 $debit->fund($item->fund);
229
230                 if ($debit->isnew) {
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);
234                 } else {
235                     $e->update_acq_fund_debit($debit) or return $e->die_event;
236                 }
237
238                 $e->update_acq_invoice_item($item) or return $e->die_event;
239             }
240         }
241     }
242
243     for my $po_id (@$finalize_pos) {
244         my $po = $e->retrieve_acq_purchase_order($po_id) 
245             or return $e->die_event;
246         
247         my $evt = finalize_blanket_po($e, $po);
248         return $evt if $evt;
249     }
250
251     $invoice = fetch_invoice_impl($e, $invoice->id);
252     if ($do_commit) {
253         $e->commit or return $e->die_event;
254     }
255
256     return $invoice;
257 }
258
259 sub build_invoice_api {
260     my($self, $conn, $auth, $invoice, $entries, $items, $finalize_pos) = @_;
261
262     my $e = new_editor(xact => 1, authtoken=>$auth);
263     return $e->die_event unless $e->checkauth;
264
265     if (not ref $invoice) {
266         # caller only provided the ID
267         $invoice = $e->retrieve_acq_invoice($invoice) or return $e->die_event;
268     }
269
270     if (not $invoice->receiver and $invoice->isnew) {
271         $invoice->receiver($e->requestor->ws_ou);
272     }
273
274     return $e->die_event unless
275         $e->allowed('CREATE_INVOICE', $invoice->receiver);
276
277     return build_invoice_impl($e, $invoice, $entries, $items, 1, $finalize_pos);
278 }
279
280
281 sub rollback_entry_debits {
282     my($e, $entry) = @_;
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;
285
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());
290         $e->update_acq_fund_debit($debit) or return $e->die_event;
291         update_copy_cost($e, $debit) or return $e->die_event; # clear the cost
292     }
293
294     return undef;
295 }
296
297 sub update_entry_debits {
298     my($e, $entry) = @_;
299
300     my $debits = find_entry_debits($e, $entry, 't');
301     return undef unless @$debits;
302
303     if($entry->phys_item_count > @$debits) {
304         $e->rollback;
305         # We can't invoice for more items than we have debits for
306         return OpenILS::Event->new(
307             'ACQ_INVOICE_ENTRY_COUNT_EXCEEDS_DEBITS', 
308             payload => {entry => $entry->id});
309     }
310
311     for my $debit (@$debits) {
312         my $amount = entry_amount_per_item($entry);
313         $debit->amount($amount);
314         $debit->encumbrance('f');
315         $e->update_acq_fund_debit($debit) or return $e->die_event;
316
317         # TODO: this does not reflect ancillary charges, like taxes, etc.
318         # We may need a way to indicate whether the amount attached to an 
319         # invoice_item should be prorated and included in the copy cost.
320         # Note that acq.invoice_item_type.prorate does not necessarily 
321         # mean a charge should be included in the copy price, only that 
322         # it should spread accross funds.
323         update_copy_cost($e, $debit, $amount) or return $e->die_event;
324     }
325
326     return undef;
327 }
328
329 # This was originally done only for EDI invoices, but needs added to the
330 # manual invoice-entering process for consistency's sake.
331 sub uncancel_copies_as_needed {
332     my ($e, $entry) = @_;
333
334     return unless $entry->lineitem and $entry->phys_item_count;
335
336     my $li = $e->retrieve_acq_lineitem($entry->lineitem) or
337         return $e->die_event;
338
339     # if an invoiced lineitem is marked as cancelled
340     # (e.g. back-order), invoicing the lineitem implies
341     # we need to un-cancel it
342
343     # collect the LIDs, starting with those that are
344     # not cancelled, followed by those that have keep-debits cancel_reasons,
345     # followed by non-keep-debit cancel reasons.
346
347     my $lid_ids = $e->json_query({
348         select => {acqlid => ['id']},
349         from => {
350             acqlid => {
351                 acqcr => {type => 'left'},
352                 acqfdeb => {type => 'left'}
353             }
354         },
355         where => {
356             '+acqlid' => {lineitem => $li->id},
357             '+acqfdeb' => {encumbrance => 't'}  # not-yet invoiced copies
358         },
359         order_by => [{
360             class => 'acqcr',
361             field => 'keep_debits',
362             direction => 'desc'
363         }],
364         limit => $entry->phys_item_count    # crucial
365     });
366
367     for my $lid_id (map {$_->{id}} @$lid_ids) {
368         my $lid = $e->retrieve_acq_lineitem_detail($lid_id);
369         next unless $lid->cancel_reason;
370
371         $logger->info(
372             "un-cancelling invoice lineitem " . $li->id .
373             " lineitem_detail " . $lid_id
374         );
375         $lid->clear_cancel_reason;
376         return $e->die_event unless $e->update_acq_lineitem_detail($lid);
377     }
378
379     $li->clear_cancel_reason;
380     $li->state("on-order") if $li->state eq "cancelled";    # sic
381     $li->edit_time("now");
382
383     unless ($e->update_acq_lineitem($li)) {
384         my $evt = $e->die_event;
385         $logger->error("couldn't clear li cancel reason: ". $evt->{textcode});
386         return $evt;
387     }
388
389     return;
390 }
391
392
393 # update the linked copy to reflect the amount paid for the item
394 # returns true on success, false on error
395 sub update_copy_cost {
396     my ($e, $debit, $amount) = @_;
397
398     my $lid = $e->search_acq_lineitem_detail([
399         {fund_debit => $debit->id},
400         {flesh => 1, flesh_fields => {acqlid => ['eg_copy_id']}}
401     ])->[0];
402
403     if($lid and my $copy = $lid->eg_copy_id) {
404         defined $amount and $copy->cost($amount) or $copy->clear_cost;
405
406         # XXX It would be nice to have a way to record that a copy was
407         # updated by a non-user mechanism, like EDI, but we don't have
408         # a clear way to do that here.
409         if ($e->requestor) {
410             $copy->editor($e->requestor->id);
411             $copy->edit_date('now');
412         }
413
414         $e->update_asset_copy($copy) or return 0;
415     }
416
417     return 1;
418 }
419
420
421 sub entry_amount_per_item {
422     my $entry = shift;
423     return $entry->amount_paid if $U->is_true($entry->billed_per_item);
424     return 0 if $entry->phys_item_count == 0;
425     return $entry->amount_paid / $entry->phys_item_count;
426 }
427
428 sub easy_money { # TODO XXX replace with something from a library
429     my ($val) = @_;
430
431     my $rounded = int($val * 100) / 100.0;
432     if ($rounded == $val) {
433         return sprintf("%.02f", $val);
434     } else {
435         return sprintf("%g", $val);
436     }
437 }
438
439 # 0 on failure (caller should call $e->die_event), array on success
440 sub amounts_spent_per_fund {
441     my ($e, $inv_id) = @_;
442
443     my $entries = $e->search_acq_invoice_entry({"invoice" => $inv_id}) or
444         return 0;
445
446     my $items = $e->search_acq_invoice_item({"invoice" => $inv_id}) or
447         return 0;
448
449     my %totals_by_fund;
450     foreach my $entry (@$entries) {
451         my $debits = find_entry_debits($e, $entry, "f") or return 0;
452         foreach (@$debits) {
453             $totals_by_fund{$_->fund} ||= 0.0;
454             $totals_by_fund{$_->fund} += $_->amount;
455         }
456     }
457
458     foreach my $item (@$items) {
459         next unless $item->fund and $item->amount_paid;
460         $totals_by_fund{$item->fund} ||= 0.0;
461         $totals_by_fund{$item->fund} += $item->amount_paid;
462     }
463
464     my @totals;
465     foreach my $fund_id (keys %totals_by_fund) {
466         my $fund = $e->retrieve_acq_fund($fund_id) or return 0;
467         push @totals, {
468             "fund" => $fund->to_bare_hash,
469             "total" => easy_money($totals_by_fund{$fund_id})
470         };
471     }
472
473     return \@totals;
474 }
475
476 # there is no direct link between invoice_entry and fund debits.
477 # when we need to retrieve the related debits, we have to do some searching
478 sub find_entry_debits {
479     my($e, $entry, $encumbrance, $amount) = @_;
480
481     my $query = {
482         select => {acqfdeb => ['id']},
483         from => {
484             acqfdeb => {
485                 acqlid => {
486                     join => {
487                         jub =>  {
488                             join => {
489                                 acqie => {
490                                     filter => {id => $entry->id}
491                                 }
492                             }
493                         }
494                     }
495                 }
496             }
497         },
498         where => {'+acqfdeb' => {encumbrance => $encumbrance}},
499         order_by => {'acqlid' => ['recv_time']}, # un-received items will sort to the end
500         limit => $entry->phys_item_count
501     };
502
503     $query->{where}->{'+acqfdeb'}->{amount} = $amount if $amount;
504
505     my $debits = $e->json_query($query);
506     my $debit_ids = [map { $_->{id} } @$debits];
507     return (@$debit_ids) ? $e->search_acq_fund_debit({id => $debit_ids}) : [];
508 }
509
510
511 __PACKAGE__->register_method(
512     method => 'build_invoice_api',
513     api_name    => 'open-ils.acq.invoice.retrieve',
514     authoritative => 1,
515     signature => {
516         desc => q/Creates a new stub invoice/,
517         params => [
518             {desc => 'Authentication token', type => 'string'},
519             {desc => q/Invoice Id/, type => 'number'},
520         ],
521         return => {desc => 'The new invoice w/ entries and items attached', type => 'object', class => 'acqinv'}
522     }
523 );
524
525
526 sub fetch_invoice_api {
527     my($self, $conn, $auth, $invoice_id, $options) = @_;
528
529     my $e = new_editor(authtoken=>$auth);
530     return $e->event unless $e->checkauth;
531
532     my $invoice = fetch_invoice_impl($e, $invoice_id, $options) or
533         return $e->event;
534     return $e->event unless $e->allowed(['VIEW_INVOICE', 'CREATE_INVOICE'], $invoice->receiver);
535
536     return $invoice;
537 }
538
539 sub fetch_invoice_impl {
540     my ($e, $invoice_id, $options) = @_;
541
542     $options ||= {};
543
544     my $args = $options->{"no_flesh_misc"} ? $invoice_id : [
545         $invoice_id,
546         {
547             "flesh" => 6,
548             "flesh_fields" => {
549                 "acqinv" => ["entries", "items"],
550                 "acqii" => ["fund_debit", "purchase_order", "po_item"]
551             }
552         }
553     ];
554
555     return $e->retrieve_acq_invoice($args);
556 }
557
558 __PACKAGE__->register_method(
559     method => 'prorate_invoice',
560     api_name    => 'open-ils.acq.invoice.apply_prorate',
561     signature => {
562         desc => q/
563             For all invoice items that have the prorate flag set to true, this will create the necessary 
564             additional invoice_item's to prorate the cost across all affected funds by percent spent for each fund.
565         /,
566         params => [
567             {desc => 'Authentication token', type => 'string'},
568             {desc => q/Invoice Id/, type => 'number'},
569         ],
570         return => {desc => 'The updated invoice w/ entries and items attached', type => 'object', class => 'acqinv'}
571     }
572 );
573
574
575 sub prorate_invoice {
576     my($self, $conn, $auth, $invoice_id) = @_;
577
578     my $e = new_editor(xact => 1, authtoken=>$auth);
579     return $e->die_event unless $e->checkauth;
580
581     my $invoice = fetch_invoice_impl($e, $invoice_id) or return $e->die_event;
582     return $e->die_event unless $e->allowed('CREATE_INVOICE', $invoice->receiver);
583
584     my @lid_debits;
585     push(@lid_debits, @{find_entry_debits($e, $_, 'f', entry_amount_per_item($_))}) for @{$invoice->entries};
586
587     my $inv_items = $e->search_acq_invoice_item([
588         {"invoice" => $invoice_id, "fund_debit" => {"!=" => undef}},
589         {"flesh" => 1, "flesh_fields" => {"acqii" => ["fund_debit"]}}
590     ]) or return $e->die_event;
591
592     my @item_debits = map { $_->fund_debit } @$inv_items;
593
594     my %fund_totals;
595     my $total_entry_paid = 0;
596     for my $debit (@lid_debits, @item_debits) {
597         $fund_totals{$debit->fund} = 0 unless $fund_totals{$debit->fund};
598         $fund_totals{$debit->fund} += $debit->amount;
599         $total_entry_paid += $debit->amount;
600     }
601
602     $logger->info("invoice: prorating against invoice amount $total_entry_paid");
603
604     for my $item (@{$invoice->items}) {
605
606         next if $item->fund_debit; # item has already been processed
607
608         # future: cache item types locally
609         my $item_type = $e->retrieve_acq_invoice_item_type($item->inv_item_type) or return $e->die_event;
610         next unless $U->is_true($item_type->prorate);
611
612         # Prorate charges across applicable funds
613         my $full_item_paid = $item->amount_paid; # total amount paid for this item before splitting
614         my $full_item_cost = $item->cost_billed; # total amount invoiced for this item before splitting
615         my $first_round = 1;
616         my $largest_debit;
617         my $largest_item;
618         my $total_debited = 0;
619         my $total_costed = 0;
620
621         for my $fund_id (keys %fund_totals) {
622
623             my $spent_for_fund = $fund_totals{$fund_id};
624             next unless $spent_for_fund > 0;
625
626             my $prorated_amount = ($spent_for_fund / $total_entry_paid) * $full_item_paid;
627             my $prorated_cost = ($spent_for_fund / $total_entry_paid) * $full_item_cost;
628             $logger->info("invoice: attaching prorated amount $prorated_amount to fund $fund_id for invoice $invoice_id");
629
630             my $debit;
631             if($first_round and $item->po_item) {
632                 # if this item is the result of a PO item, repurpose the original debit
633                 # for the first chunk of the prorated amount
634                 $debit = $e->retrieve_acq_fund_debit($item->po_item->fund_debit);
635             } else {
636                 $debit = Fieldmapper::acq::fund_debit->new;
637                 $debit->isnew(1);
638             }
639
640             $debit->fund($fund_id);
641             $debit->amount($prorated_amount);
642             $debit->origin_amount($prorated_amount);
643             $debit->origin_currency_type($e->retrieve_acq_fund($fund_id)->currency_type); # future: cache funds locally
644             $debit->encumbrance('f');
645             $debit->debit_type('prorated_charge');
646
647             if($debit->isnew) {
648                 $e->create_acq_fund_debit($debit) or return $e->die_event;
649             } else {
650                 $e->update_acq_fund_debit($debit) or return $e->die_event;
651             }
652
653             $total_debited += $prorated_amount;
654             $total_costed += $prorated_cost;
655             $largest_debit = $debit if !$largest_debit or $prorated_amount > $largest_debit->amount;
656
657             if($first_round) {
658
659                 # re-purpose the original invoice_item for the first prorated amount
660                 $item->fund($fund_id);
661                 $item->fund_debit($debit->id);
662                 $item->amount_paid($prorated_amount);
663                 $item->cost_billed($prorated_cost);
664                 $e->update_acq_invoice_item($item) or return $e->die_event;
665                 $largest_item = $item if !$largest_item or $prorated_amount > $largest_item->amount_paid;
666
667             } else {
668
669                 # for subsequent prorated amounts, create a new invoice_item
670                 my $new_item = $item->clone;
671                 $new_item->clear_id;
672                 $new_item->fund($fund_id);
673                 $new_item->fund_debit($debit->id);
674                 $new_item->amount_paid($prorated_amount);
675                 $new_item->cost_billed($prorated_cost);
676                 $e->create_acq_invoice_item($new_item) or return $e->die_event;
677                 $largest_item = $new_item if !$largest_item or $prorated_amount > $largest_item->amount_paid;
678             }
679
680             $first_round = 0;
681         }
682
683         # make sure the percentages didn't leave a small sliver of money over/under-debited
684         # if so, tweak the largest debit to smooth out the difference
685         if($total_debited != $full_item_paid or $total_costed != $full_item_cost) {
686             
687             my $paid_diff = $full_item_paid - $total_debited;
688             my $cost_diff = $full_item_cost - $total_debited;
689             $logger->info("invoice: repairing prorate descrepency of paid:$paid_diff and cost:$cost_diff");
690             my $new_paid = $largest_item->amount_paid + $paid_diff;
691             my $new_cost = $largest_item->cost_billed + $cost_diff;
692
693             $largest_debit = $e->retrieve_acq_fund_debit($largest_debit->id); # get latest copy
694             $largest_debit->amount($new_paid);
695             $e->update_acq_fund_debit($largest_debit) or return $e->die_event;
696
697             $largest_item = $e->retrieve_acq_invoice_item($largest_item->id); # get latest copy
698             $largest_item->amount_paid($new_paid);
699             $largest_item->cost_billed($new_cost);
700
701             $e->update_acq_invoice_item($largest_item) or return $e->die_event;
702         }
703     }
704
705     $invoice = fetch_invoice_impl($e, $invoice_id);
706     $e->commit;
707
708     return $invoice;
709 }
710
711
712 __PACKAGE__->register_method(
713     method      => "print_html_invoice",
714     api_name    => "open-ils.acq.invoice.print.html",
715     stream      => 1,
716     signature   => {
717         desc    => "Retrieve printable HTML vouchers for each given invoice",
718         params => [
719             {desc => "Authentication token", type => "string"},
720             {desc => "Invoice ID or a list of them", type => "mixed"},
721         ],
722         return => {
723             desc => q{One A/T event containing a printable HTML voucher for
724                 each given invoice},
725             type => "object", class => "atev"}
726     }
727 );
728
729
730 sub print_html_invoice {
731     my ($self, $conn, $auth, $id_list) = @_;
732
733     my $e = new_editor("authtoken" => $auth);
734     return $e->die_event unless $e->checkauth;
735
736     $id_list = [$id_list] unless ref $id_list;
737
738     my $invoices = $e->search_acq_invoice({"id" => $id_list}) or
739         return $e->die_event;
740
741     foreach my $invoice (@$invoices) {
742         return $e->die_event unless
743             $e->allowed("VIEW_INVOICE", $invoice->receiver);
744
745         my $amounts = amounts_spent_per_fund($e, $invoice->id) or
746             return $e->die_event;
747
748         $conn->respond(
749             $U->fire_object_event(
750                 undef, "format.acqinv.html", $invoice, $invoice->receiver,
751                 "print-on-demand", $amounts
752             )
753         );
754     }
755
756     $e->disconnect;
757     undef;
758 }
759
760 __PACKAGE__->register_method(
761     method => 'finalize_blanket_po_api',
762     api_name    => 'open-ils.acq.purchase_order.blanket.finalize',
763     signature => {
764         desc => q/
765             1. Set encumbered amount to zero for all blanket po_item's
766             2. If the PO does not have any outstanding lineitems, mark
767                the PO as 'received'.
768         /,
769         params => [
770             {desc => 'Authentication token', type => 'string'},
771             {desc => q/PO ID/, type => 'number'}
772         ],
773         return => {desc => '1 on success, event on error'}
774     }
775 );
776
777 sub finalize_blanket_po_api {
778     my ($self, $client, $auth, $po_id) = @_;
779
780     my $e = new_editor(xact => 1, authtoken=>$auth);
781     return $e->die_event unless $e->checkauth;
782
783     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
784
785     return $e->die_event unless
786         $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
787
788     my $evt = finalize_blanket_po($e, $po);
789     return $evt if $evt;
790
791     $e->commit;
792     return 1;
793 }
794
795
796 # 1. set any remaining blanket encumbrances to $0.
797 # 2. mark the PO as received if there are no pending lineitems.
798 sub finalize_blanket_po {
799     my ($e, $po) = @_;
800
801     my $po_id = $po->id;
802
803     # blanket po_items on this PO
804     my $blanket_items = $e->json_query({
805         select => {acqpoi => ['id']},
806         from => {acqpoi => {aiit => {}}},
807         where => {
808             '+aiit' => {blanket => 't'},
809             '+acqpoi' => {purchase_order => $po_id}
810         }
811     });
812
813     for my $item_id (map { $_->{id} } @$blanket_items) {
814
815         my $item = $e->retrieve_acq_po_item([
816             $item_id, {
817                 flesh => 1,
818                 flesh_fields => {acqpoi => ['fund_debit']}
819             }
820         ]); 
821
822         my $debit = $item->fund_debit or next;
823
824         next unless $U->is_true($debit->encumbrance);
825
826         $debit->amount(0);
827         $debit->encumbrance('f');
828         $e->update_acq_fund_debit($debit) or return $e->die_event;
829     }
830
831     # Number of pending lineitems on this PO. 
832     # If there are any, we don't mark 'received'
833     my $li_count = $e->json_query({
834         select => {jub => [{column => 'id', transform => 'count'}]},
835         from => 'jub',
836         where => {
837             '+jub' => {
838                 purchase_order => $po_id,
839                 state => 'on-order'
840             }
841         }
842     })->[0];
843     
844     if ($li_count->{count} > 0) {
845         $logger->info("skipping 'received' state change for po $po_id ".
846             "during finalization, because PO has pending lineitems");
847         return undef;
848     }
849
850     $po->state('received');
851     $po->edit_time('now');
852     $po->editor($e->requestor->id);
853
854     $e->update_acq_purchase_order($po) or return $e->die_event;
855
856     return undef;
857 }
858
859 1;