1 package OpenILS::Application::Acq::EDI;
2 use base qw/OpenILS::Application/;
4 use strict; use warnings;
8 use OpenSRF::AppSession;
9 use OpenSRF::EX qw/:try/;
10 use OpenSRF::Utils::Logger qw(:logger);
11 use OpenSRF::Utils::JSON;
13 use OpenILS::Application::Acq::Lineitem;
14 use OpenILS::Utils::RemoteAccount;
15 use OpenILS::Utils::CStoreEditor q/new_editor/;
16 use OpenILS::Utils::Fieldmapper;
17 use OpenILS::Application::Acq::EDI::Translator;
18 use OpenILS::Application::AppUtils;
19 my $U = 'OpenILS::Application::AppUtils';
21 use OpenILS::Utils::EDIReader;
27 my($class, %args) = @_;
28 my $self = bless(\%args, $class);
33 # our $reasons = {}; # cache for acq.cancel_reason rows ?
38 return $translator ||= OpenILS::Application::Acq::EDI::Translator->new(@_);
42 host => 'remote_host',
43 username => 'remote_user',
44 password => 'remote_password',
45 account => 'remote_account',
46 # in_dir => 'remote_path', # field_map overrides path with in_dir
47 path => 'remote_path',
51 ## Just for debugging stuff:
53 my ($self, $conn) = @_;
54 my $e = new_editor(xact=>1);
55 my $incoming = Fieldmapper::acq::edi_message->new;
56 $incoming->edi("This is content");
57 $incoming->account(1);
58 $incoming->remote_file('in/some_file.edi');
59 $e->create_acq_edi_message($incoming);;
62 # __PACKAGE__->register_method( method => 'add_a_msg', api_name => 'open-ils.acq.edi.add_a_msg'); # debugging
64 __PACKAGE__->register_method(
66 api_name => 'open-ils.acq.edi.retrieve',
69 desc => 'Fetch incoming message(s) from EDI accounts. ' .
70 'Optional arguments to restrict to one vendor and/or a max number of messages. ' .
71 'Note that messages are not parsed or processed here, just fetched and translated.',
73 {desc => 'Authentication token', type => 'string'},
74 {desc => 'Vendor ID (undef for "all")', type => 'number'},
75 {desc => 'Date Inactive Since', type => 'string'},
76 {desc => 'Max Messages Retrieved', type => 'number'}
79 desc => 'List of new message IDs (empty if none)',
86 my ($self, $set, $max, $e, $test) = @_; # $e is a working editor
89 $set ||= __PACKAGE__->retrieve_vendors($e);
93 foreach my $account (@$set) {
96 $logger->info("EDI check for vendor " . ++$vcount . " of " . scalar(@$set) . ": " . $account->host);
97 unless ($server = __PACKAGE__->remote_account($account)) { # assignment, not comparison
98 $logger->err(sprintf "Failed remote account mapping for %s (%s)", $account->host, $account->id);
101 # my $rf_starter = './'; # default to current dir
102 if ($account->in_dir) {
103 if ($account->in_dir =~ /\*+.*\//) {
104 $logger->err("EDI in_dir has a slash after an asterisk in value: '" . $account->in_dir . "'. Skipping account with indeterminate target dir!");
107 # $rf_starter = $account->in_dir;
108 # $rf_starter =~ s/((\/)?[^\/]*)\*+[^\/]*$//; # kill up to the first (possible) slash before the asterisk: keep the preceeding static dir
109 # $rf_starter .= '/' if $rf_starter or $2; # recap the dir, or replace leading "/" if there was one (but don't add if empty)
111 my @files = ($server->ls({remote_file => ($account->in_dir || './')}));
112 my @ok_files = grep {$_ !~ /\/\.?\.$/ and $_ ne '0'} @files;
113 $logger->info(sprintf "%s of %s files at %s/%s", scalar(@ok_files), scalar(@files), $account->host, $account->in_dir);
114 # $server->remote_path(undef);
115 foreach my $remote_file (@ok_files) {
116 # my $remote_file = $rf_starter . $_;
117 my $description = sprintf "%s/%s", $account->host, $remote_file;
119 # deduplicate vs. acct/filenames already in DB
120 my $hits = $e->search_acq_edi_message([
122 account => $account->id,
123 remote_file => $remote_file,
124 status => {'in' => [qw/ processed /]}, # if it never got processed, go ahead and get the new one (try again)
125 # create_time => 'NOW() - 60 DAYS', # if we wanted to allow filenames to be reused after a certain time
126 # ideally we would also use the date from FTP, but that info isn't available via RemoteAccount
128 # { flesh => 1, flesh_fields => {...}, }
130 if (scalar(@$hits)) {
131 $logger->debug("EDI: $remote_file already retrieved. Skipping");
132 warn "EDI: $remote_file already retrieved. Skipping";
137 $max and $count > $max and last;
138 $logger->info(sprintf "%s of %s targets: %s", $count, scalar(@ok_files), $description);
139 print sprintf "%s of %s targets: %s\n", $count, scalar(@ok_files), $description;
141 push @return, "test_$count";
145 my $io = IO::Scalar->new(\$content);
146 unless ( $server->get({remote_file => $remote_file, local_file => $io}) ) {
147 $logger->error("(S)FTP get($description) failed");
150 my $incoming = __PACKAGE__->process_retrieval($content, $remote_file, $server, $account->id);
151 # $server->delete(remote_file => $_); # delete remote copies of saved message
152 push @return, @$incoming;
158 # my $msg_ids = OpenILS::Application::Acq::EDI->process_retrieval(
159 # $file_content, $remote_filename, $server, $account_id, $editor);
161 sub process_retrieval {
162 my ($class, $content, $filename, $server, $account_or_id) = @_;
166 my $account = __PACKAGE__->record_activity($account_or_id, $e);
168 # a single EDI blob can contain multiple messages
169 # create one edi_message per included message
171 my $messages = OpenILS::Utils::EDIReader->new->read($content);
174 for my $msg_hash (@$messages) {
176 my $incoming = Fieldmapper::acq::edi_message->new;
178 $incoming->remote_file($filename);
179 $incoming->account($account->id);
180 $incoming->edi($content);
181 $incoming->message_type($msg_hash->{message_type});
182 $incoming->jedi(OpenSRF::Utils::JSON->perl2JSON($msg_hash)); # jedi-2.0
183 $incoming->status('translated');
184 $incoming->translate_time('NOW');
186 if ($msg_hash->{purchase_order}) {
187 $logger->info("EDI: processing message for PO " . $msg_hash->{purchase_order});
188 $incoming->purchase_order($msg_hash->{purchase_order});
189 unless ($e->retrieve_acq_purchase_order($incoming->purchase_order)) {
190 $logger->warn("EDI: received order response for nonexistent PO. Skipping...");
196 unless($e->create_acq_edi_message($incoming)) {
197 $logger->error("EDI: unable to create edi_message " . $e->die_event);
200 # refresh to pickup create_date, etc.
201 $incoming = $e->retrieve_acq_edi_message($incoming->id);
204 # since there's a fair chance of unhandled problems
205 # cropping up, particularly with new vendors, wrap w/ eval.
206 eval { $class->process_parsed_msg($account, $incoming, $msg_hash) };
209 $incoming = $e->retrieve_acq_edi_message($incoming->id);
211 $incoming->status('proc_error');
212 $incoming->error($@);
214 $incoming->status('processed');
216 $e->update_acq_edi_message($incoming);
219 push(@return, $incoming->id);
226 # $account is a Fieldmapper object for acq.edi_account row
227 # $messageset is an arrayref with acq.edi_message.id values
228 # $e is optional editor object
230 my ($class, $account, $message_ids, $e) = @_; # $e is a working editor
232 ($account and scalar @$message_ids) or return;
236 my @messageset = map {$e->retrieve_acq_edi_message($_)} @$message_ids;
238 my $m_count = scalar(@messageset);
239 (scalar(@$message_ids) == $m_count) or
240 $logger->warn(scalar(@$message_ids) - $m_count . " bad IDs passed to send_core (ignored)");
242 my $log_str = sprintf "EDI send to edi_account %s (%s)", $account->id, $account->host;
243 $logger->info("$log_str: $m_count message(s)");
248 unless ($server = __PACKAGE__->remote_account($account, 1)) { # assignment, not comparison
249 $logger->error("Failed remote account connection for $log_str");
252 foreach (@messageset) {
253 $_ or next; # we already warned about bum ids
256 $error = "Server error: Failed remote account connection for $log_str"; # already told $logger, this is to update object below
257 } elsif (! $_->edi) {
258 $logger->error("Message (id " . $_->id. ") for $log_str has no EDI content");
259 $error = "EDI empty!";
260 } elsif ($res = $server->put({remote_path => $account->path, content => $_->edi, single_ext => 1})) {
261 # This is the successful case!
262 $_->remote_file($res);
263 $_->status('complete');
264 $_->process_time('NOW'); # For outbound files, sending is the end of processing on the EG side.
265 $logger->info("Sent message (id " . $_->id. ") via $log_str");
267 $logger->error("(S)FTP put to $log_str FAILED: " . ($server->error || 'UNKOWNN'));
268 $error = "put FAILED: " . ($server->error || 'UNKOWNN');
272 $_->error_time('NOW');
274 $logger->info("Calling update_acq_edi_message");
276 unless ($e->update_acq_edi_message($_)) {
277 $logger->error("EDI send_core update_acq_edi_message failed for message object: " . Dumper($_));
278 OpenILS::Application::Acq::EDI::Translator->debug_file(Dumper($_ ), '/tmp/update_acq_edi_message.FAIL');
279 OpenILS::Application::Acq::EDI::Translator->debug_file(Dumper($_->to_bare_hash), '/tmp/update_acq_edi_message.FAIL.to_bare_hash');
281 # There's always an update, even if we failed.
283 __PACKAGE__->record_activity($account, $e); # There's always an update, even if we failed.
288 # attempt_translation does not touch the DB, just the object.
289 sub attempt_translation {
290 my ($class, $edi_message, $to_edi) = @_;
291 my $tran = translator();
292 my $ret = $to_edi ? $tran->json2edi($edi_message->jedi) : $tran->edi2json($edi_message->edi);
293 # $logger->error("json: " . Dumper($json)); # debugging
295 if (not $ret or (! ref($ret)) or $ret->is_fault) { # RPC::XML::fault on failure
296 $edi_message->status('trans_error');
297 $edi_message->error_time('NOW');
298 my $pre = "EDI Translator " . ($to_edi ? 'json2edi' : 'edi2json') . " failed";
299 my $message = ref($ret) ?
300 ("$pre, Error " . $ret->code . ": " . __PACKAGE__->nice_string($ret->string)) :
301 ("$pre: " . __PACKAGE__->nice_string($ret) ) ;
302 $edi_message->error($message);
303 $logger->error($message);
307 $edi_message->status('translated');
308 $edi_message->translate_time('NOW');
311 $edi_message->edi($ret->value); # translator returns an object
313 $edi_message->jedi($ret->value); # translator returns an object
318 sub retrieve_vendors {
319 my ($self, $e, $vendor_id, $last_activity) = @_; # $e is a working editor
323 my $criteria = {'+acqpro' => {active => 't'}};
324 $criteria->{'+acqpro'}->{id} = $vendor_id if $vendor_id;
325 return $e->search_acq_edi_account([
330 acqedi => ['provider']
334 # {"id":{"!=":null},"+acqpro":{"active":"t"}}, {"join":"acqpro", "flesh_fields":{"acqedi":["provider"]},"flesh":1}
337 # This is the SRF-exposed call, so it does checkauth
340 my ($self, $conn, $auth, $vendor_id, $last_activity, $max) = @_;
342 my $e = new_editor(authtoken=>$auth);
343 unless ($e and $e->checkauth()) {
344 $logger->warn("checkauth failed for authtoken '$auth'");
347 # return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency); # add permission here ?
349 my $set = __PACKAGE__->retrieve_vendors($e, $vendor_id, $last_activity) or return $e->die_event;
350 return __PACKAGE__->retrieve_core($e, $set, $max);
354 # field_map takes the hashref of vendor data with fields from acq.edi_account and
355 # maps them to the argument style needed for RemoteAccount. It also extrapolates
356 # data from the remote_host string for type and port, when available.
360 my $vendor = shift or return;
361 my $no_override = @_ ? shift : 0;
363 $verbose and $logger->warn("vendor: " . Dumper($vendor));
364 foreach (keys %map) {
365 $args{$map{$_}} = $vendor->$_ if defined $vendor->$_;
367 unless ($no_override) {
368 $args{remote_path} = $vendor->in_dir; # override "path" with "in_dir"
370 my $host = $args{remote_host} || '';
371 ($host =~ s/^(S?FTP)://i and $args{type} = uc($1)) or
372 ($host =~ s/^(SSH|SCP)://i and $args{type} = 'SCP' ) ;
373 $host =~ s/:(\d+)$// and $args{port} = $1;
374 ($args{remote_host} = $host) =~ s#/+##;
375 $verbose and $logger->warn("field_map: " . Dumper(\%args));
380 # The point of remote_account is to get the RemoteAccount object with args from the DB
383 my ($self, $vendor, $outbound, $e) = @_;
385 unless (ref($vendor)) { # It's not a hashref/object.
386 $vendor or return; # If in fact it's nothing: abort!
387 # else it's a vendor_id string, so get the full vendor data
389 my $set_of_one = $self->retrieve_vendors($e, $vendor) or return;
390 $vendor = shift @$set_of_one;
393 return OpenILS::Utils::RemoteAccount->new(
394 $self->field_map($vendor, $outbound)
398 # takes account ID or account Fieldmapper object
400 sub record_activity {
401 my ($class, $account_or_id, $e) = @_;
402 $account_or_id or return;
404 my $account = ref($account_or_id) ? $account_or_id : $e->retrieve_acq_edi_account($account_or_id);
405 $logger->info("EDI record_activity calling update_acq_edi_account");
406 $account->last_activity('NOW') or return;
408 $e->update_acq_edi_account($account) or $logger->warn("EDI: in record_activity, update_acq_edi_account FAILED");
415 my $string = shift or return '';
417 my $head = @_ ? shift : 100;
418 my $tail = @_ ? shift : 25;
419 (length($string) < $head + $tail) and return $string;
420 my $h = substr($string,0,$head);
421 my $t = substr($string, -1*$tail);
425 # return substr($string,0,$head) . "... " . substr($string, -1*$tail);
428 # parts of this process can fail without the entire
429 # thing failing. If a catastrophic error occurs,
430 # it will occur via die.
431 sub process_parsed_msg {
432 my ($class, $account, $incoming, $msg_hash) = @_;
434 if ($incoming->message_type eq 'INVOIC') {
435 return $class->create_acq_invoice_from_edi(
436 $msg_hash, $account->provider, $incoming);
440 for my $li_hash (@{$msg_hash->{lineitems}}) {
441 my $e = new_editor(xact => 1);
443 my $li_id = $li_hash->{id};
444 my $li = $e->retrieve_acq_lineitem($li_id);
447 $logger->error("EDI: request for invalid lineitem ID '$li_id'");
452 if ($li_hash->{expected_date}) {
453 my ($y, $m, $d) = $li_hash->{expected_date} =~ /^(\d{4})(\d{2})(\d{2})/g;
455 $recv_time .= "-$m" if $m;
456 $recv_time .= "-$d" if $d;
457 $li->expected_recv_time($recv_time);
460 $li->estimated_unit_price($li_hash->{unit_price});
462 if (not $incoming->purchase_order) {
463 # PO should come from the EDI message, but if not...
465 # fetch the latest copy
466 $incoming = $e->retrieve_acq_edi_message($incoming->id);
467 $incoming->purchase_order($li->purchase_order);
469 unless($e->update_acq_edi_message($incoming)) {
470 $logger->error("EDI: unable to update edi_message " . $e->die_event);
475 my $lids = $e->json_query({
476 select => {acqlid => ['id']},
478 where => {lineitem => $li->id}
481 my @lids = map { $_->{id} } @$lids;
482 my $lid_count = scalar(@lids);
483 my $lids_covered = 0;
484 my $lids_cancelled = 0;
488 for my $qty (@{$li_hash->{quantities}}) {
490 my $qty_count = $qty->{quantity};
491 my $qty_code = $qty->{code};
493 next unless defined $qty_count;
496 $logger->warn("EDI: Response for LI $li_id specifies quantity ".
497 "$qty_count with no 6063 code! Contact vendor to resolve.");
501 $logger->info("EDI: LI $li_id processing quantity count=$qty_count / code=$qty_code");
503 if ($qty_code eq '21') { # "ordered quantity"
504 $order_qty = $qty_count;
505 $logger->info("EDI: LI $li_id -- vendor confirms $qty_count ordered");
506 $logger->warn("EDI: LI $li_id -- order count $qty_count ".
507 "does not match LID count $lid_count") unless $qty_count == $lid_count;
511 $lids_covered += $qty_count;
513 if ($qty_code eq '12') {
514 $dispatch_qty = $qty_count;
515 $logger->info("EDI: LI $li_id -- vendor dispatched $qty_count");
518 } elsif ($qty_code eq '57') {
519 $logger->info("EDI: LI $li_id -- $qty_count in transit");
522 # 84: urgent delivery
523 # 118: quantity manifested
526 # -------------------------------------------------------------------------
527 # All of the remaining quantity types require that we apply a cancel_reason
528 # DB populated w/ 6063 keys in 1200's
530 my $eg_reason = $e->retrieve_acq_cancel_reason(1200 + $qty_code);
533 $logger->warn("EDI: Unhandled quantity qty_code '$qty_code' ".
534 "for li $li_id. $qty_count items unprocessed");
538 my ($cancel_count, $fatal) =
539 $class->cancel_lids($e, $eg_reason, $qty_count, $lid_count, \@lids);
543 $lids_cancelled += $cancel_count;
545 # if ALL the items have the same cancel_reason, the LI gets it too
546 $li->cancel_reason($eg_reason->id) if $qty_count == $lid_count;
548 $li->edit_time('now');
549 unless ($e->update_acq_lineitem($li)) {
550 $logger->error("EDI: update_acq_lineitem failed " . $e->die_event);
555 # in case the provider neglected to echo back the order count
556 $order_qty = $lid_count unless defined $order_qty;
558 # it may be necessary to change the logic here to look for lineitem
559 # order status / availability status instead of dispatch_qty and
560 # assume that dispatch_qty simply equals the number of unaccounted-for copies
561 if (defined $dispatch_qty) {
562 # provider is telling us how may copies were delivered
564 # number of copies neither cancelled or delivered
565 my $remaining_lids = $order_qty - ($dispatch_qty + $lids_cancelled);
567 if ($remaining_lids > 0) {
569 # the vendor did not ship all items and failed to provide cancellation
570 # quantities for some or all of the items to be cancelled. When this
571 # happens, we cancel the remaining un-delivered copies using the
572 # lineitem order status to determine the cancel reason.
577 if ($stat = $li_hash->{order_status}) {
578 $logger->info("EDI: lineitem has order status $stat");
580 if ($stat eq '200') {
581 $reason_id = 1007; # not accepted
583 } elsif ($stat eq '400') {
584 $reason_id = 1283; # back-order
587 } elsif ($stat = $li_hash->{avail_status}) {
588 $logger->info("EDI: lineitem has availability status $stat");
592 # TODO: needs cancellation?
597 my $reason = $e->retrieve_acq_cancel_reason($reason_id);
599 my ($cancel_count, $fatal) =
600 $class->cancel_lids($e, $reason, $remaining_lids, $lid_count, \@lids);
603 $lids_cancelled += $cancel_count;
605 # All LIDs cancelled with same reason, apply
606 # the same cancel reason to the lineitem
607 $li->cancel_reason($reason->id) if $remaining_lids == $order_qty;
609 $li->edit_time('now');
610 unless ($e->update_acq_lineitem($li)) {
611 $logger->error("EDI: update_acq_lineitem failed " . $e->die_event);
616 $logger->warn("EDI: vendor says we ordered $order_qty and cancelled ".
617 "$lids_cancelled, but only shipped $dispatch_qty");
622 # LI and LIDs updated, let's wrap this one up.
623 # this is a no-op if the xact has already been rolled back
626 $logger->info("EDI: LI $li_id -- $order_qty LIDs ordered; ".
627 "$lids_cancelled LIDs cancelled");
632 my ($class, $e, $reason, $count, $lid_count, $lid_ids) = @_;
634 my $cancel_count = 0;
636 foreach (1 .. $count) {
638 my $lid_id = shift @$lid_ids;
641 $logger->warn("EDI: Used up all $lid_count LIDs. ".
642 "Ignoring extra status '" . $reason->label . "'");
646 my $lid = $e->retrieve_acq_lineitem_detail($lid_id);
647 $lid->cancel_reason($reason->id);
649 # item is cancelled. Remove the fund debit.
650 unless ($U->is_true($reason->keep_debits)) {
652 if (my $debit_id = $lid->fund_debit) {
654 $lid->clear_fund_debit;
655 my $debit = $e->retrieve_acq_fund_debit($debit_id);
657 if ($U->is_true($debit->encumbrance)) {
658 $logger->info("EDI: deleting debit $debit_id for cancelled LID $lid_id");
660 unless ($e->delete_acq_fund_debit($debit)) {
661 $logger->error("EDI: unable to update fund_debit " . $e->die_event);
665 # do not delete a paid-for debit
666 $logger->warn("EDI: cannot delete invoiced debit $debit_id");
671 $e->update_acq_lineitem_detail($lid);
675 return ($cancel_count);
679 # create_acq_invoice_from_edi() does what it sounds like it does for INVOIC
680 # messages. For similar operation on ORDRSP messages, see the guts of
682 # Return boolean success indicator.
683 sub create_acq_invoice_from_edi {
684 my ($class, $invoice, $provider, $message) = @_;
685 # $invoice is O::U::EDIReader hash
686 # $provider is only a pkey
687 # $message is Fieldmapper::acq::edi_message
689 my $e = new_editor();
691 my $log_prefix = "create_acq_invoice_from_edi(..., <acq.edi_message #" .
692 $message->id . ">): ";
694 my $eg_inv = Fieldmapper::acq::invoice->new;
696 $eg_inv->provider($provider);
697 $eg_inv->shipper($provider); # XXX Do we really have a meaningful way to
698 # distinguish provider and shipper?
699 $eg_inv->recv_method("EDI");
702 # some vendors encode the account number as the SAN.
703 # starting with the san value, then the account value,
704 # treat each as a san, then an acct number until the first success
705 for my $buyer ( ($invoice->{buyer_san}, $invoice->{buyer_acct}) ) {
708 # some vendors encode the SAN as "$SAN $vendcode"
711 my $addr = $e->search_actor_org_address(
712 {valid => "t", san => $buyer})->[0];
716 $eg_inv->receiver($addr->org_unit);
721 my $acct = $e->search_acq_edi_account({vendacct => $buyer})->[0];
724 $eg_inv->receiver($acct->owner);
730 if (!$eg_inv->receiver) {
731 $logger->error($log_prefix .
732 sprintf("unable to determine buyer (org unit) in invoice; ".
733 "buyer_san=%s; buyer_acct=%s",
734 ($invoice->{buyer_san} || ''),
735 ($invoice->{buyer_acct} || '')
741 $eg_inv->inv_ident($invoice->{invoice_ident});
743 if (!$eg_inv->inv_ident) {
745 $log_prefix . "no invoice ID # in INVOIC message; " . shift
752 $message->purchase_order($invoice->{purchase_order});
754 for my $lineitem (@{$invoice->{lineitems}}) {
755 my $li_id = $lineitem->{id};
758 $logger->warn($log_prefix . "no lineitem ID");
762 my $li = $e->retrieve_acq_lineitem($li_id);
765 $logger->warn($log_prefix .
766 "no LI found with ID: $li_id : " . $e->event);
770 my ($quant) = grep {$_->{code} eq '47'} @{$lineitem->{quantities}};
771 my $quantity = ($quant) ? $quant->{quantity} : 0;
774 $logger->warn($log_prefix .
775 "no invoice quantity specified for LI $li_id");
779 # NOTE: if needed, we also have $lineitem->{net_unit_price}
780 # and $lineitem->{gross_unit_price}
781 my $lineitem_price = $lineitem->{amount_billed};
783 # if the top-level PO value is unset, get it from the first LI
784 $message->purchase_order($li->purchase_order)
785 unless $message->purchase_order;
787 my $eg_inv_entry = Fieldmapper::acq::invoice_entry->new;
788 $eg_inv_entry->inv_item_count($quantity);
790 # XXX Validate by making sure the LI is on-order and belongs to
791 # the right provider and ordering agency and all that.
792 $eg_inv_entry->lineitem($li_id);
794 # XXX Do we actually need to link to PO directly here?
795 $eg_inv_entry->purchase_order($li->purchase_order);
797 # This is the total price for all units billed, not per-unit.
798 $eg_inv_entry->cost_billed($lineitem_price);
800 push @eg_inv_entries, $eg_inv_entry;
805 my %charge_type_map = (
806 'TX' => ['TAX', 'Tax from electronic invoice'],
807 'CA' => ['PRO', 'Cataloging services'],
808 'DL' => ['SHP', 'Delivery']
811 for my $charge (@{$invoice->{misc_charges}}) {
812 my $eg_inv_item = Fieldmapper::acq::invoice_item->new;
814 my $amount = $charge->{charge_amount};
817 $logger->warn($log_prefix . "charge with no amount");
821 my $map = $charge_type_map{$charge->{charge_type}};
826 'Unknown charge type ' . $charge->{charge_type}
830 $eg_inv_item->inv_item_type($$map[0]);
831 $eg_inv_item->note($$map[1]);
832 $eg_inv_item->cost_billed($amount);
834 push @eg_inv_items, $eg_inv_item;
837 $logger->info($log_prefix .
838 sprintf("creating invoice with %d entries and %d items.",
839 scalar(@eg_inv_entries), scalar(@eg_inv_items)));
843 # save changes to acq.edi_message row
844 if (not $e->update_acq_edi_message($message)) {
846 $log_prefix . "couldn't update edi_message " . $message->id
852 if (not $e->create_acq_invoice($eg_inv)) {
853 $logger->error($log_prefix . "couldn't create invoice: " . $e->event);
857 # Now we have a pkey for our EG invoice, so set the invoice field on all
858 # our entries according and create those too.
859 my $eg_inv_id = $e->data->id;
860 foreach (@eg_inv_entries) {
861 $_->invoice($eg_inv_id);
862 if (not $e->create_acq_invoice_entry($_)) {
864 $log_prefix . "couldn't create entry against lineitem " .
865 $_->lineitem . ": " . $e->event
871 # Create any invoice items (taxes)
872 foreach (@eg_inv_items) {
873 $_->invoice($eg_inv_id);
874 if (not $e->create_acq_invoice_item($_)) {
876 $log_prefix . "couldn't create inv item: " . $e->event