Acq: Fix EDI invoices for ULS, improve troubleshootability
authorLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Tue, 5 Feb 2013 22:14:16 +0000 (17:14 -0500)
committerLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Tue, 5 Feb 2013 22:19:17 +0000 (17:19 -0500)
1) Taxes appear in different, but still valid way in ULS invoices than in
invoices from other vendors observed to date.

2) Invoices from ULS use MOA 203 to indicate unit price instead of the
usual meaning of whole-lineitem price.

3) Now abuse acq.invoice.note to leave better troubleshooting
breadcrumbs.

Signed-off-by: Lebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/EDI.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIReader.pm

index a6b65a8..4d8edcf 100644 (file)
@@ -47,6 +47,14 @@ my %map = (
     path     => 'remote_path',
 );
 
+my $VENDOR_KLUDGE_MAP = {
+    INVOIC => {
+        amount_billed_is_per_unit => [1699342]
+    },
+    ORDRSP => {
+    }
+};
+
 
 __PACKAGE__->register_method(
        method    => 'retrieve',
@@ -827,6 +835,19 @@ sub edi_date_to_iso {
 }
 
 
+# Return hash with a key for every kludge that should apply for this
+# msg_type (INVOIC,ORDRSP) and this vendor SAN.
+sub get_kludges {
+    my ($class, $msg_type, $vendor_san) = @_;
+
+    my @kludges;
+    while (my ($kludge, $vendors) = each %{$VENDOR_KLUDGE_MAP->{$msg_type}}) {
+        push @kludges, $kludge if grep { $_ eq $vendor_san } @$vendors;
+    }
+
+    return map { $_ => 1 } @kludges;
+}
+
 # create_acq_invoice_from_edi() does what it sounds like it does for INVOIC
 # messages.  For similar operation on ORDRSP messages, see the guts of
 # process_jedi().
@@ -842,8 +863,26 @@ sub create_acq_invoice_from_edi {
     my $log_prefix = "create_acq_invoice_from_edi(..., <acq.edi_message #" .
         $message->id . ">): ";
 
+    my %msg_kludges;
+    if ($msg_data->{vendor_san}) {
+        %msg_kludges = $class->get_kludges('INVOIC', $msg_data->{vendor_san});
+    } else {
+        $logger->warn($log_prefix . "no vendor_san field!");
+    }
+
     my $eg_inv = Fieldmapper::acq::invoice->new;
 
+    # Some troubleshooting aids.  Yeah we should have made appropriate links
+    # for this in the schema, but this is better than nothing.  Probably
+    # *don't* try to i18n this.
+    $eg_inv->note("Generated from acq.edi_message #" . $message->id . ".");
+    if (%msg_kludges) {
+        $eg_inv->note(
+            $eg_inv->note .
+            " Vendor kludges: " . join(", ", keys(%msg_kludges)) . "."
+        );
+    }
+
     $eg_inv->provider($provider);
     $eg_inv->shipper($provider);    # XXX Do we really have a meaningful way to
                                     # distinguish provider and shipper?
@@ -903,6 +942,8 @@ sub create_acq_invoice_from_edi {
         # and $lineitem->{gross_unit_price}
         my $lineitem_price = $lineitem->{amount_billed};
 
+        $lineitem_price *= $quantity if $msg_kludges{amount_billed_is_per_unit};
+
         # if the top-level PO value is unset, get it from the first LI
         $message->purchase_order($li->purchase_order)
             unless $message->purchase_order;
@@ -930,6 +971,10 @@ sub create_acq_invoice_from_edi {
         push @eg_inv_cancel_lis, 
             {lineitem => $li, quantity => $quantity} 
             if $li->cancel_reason;
+
+        # The EDIReader class does detect certain per-lineitem taxes, but
+        # we'll ignore them for now, as the only sample invoices I've yet seen
+        # containing them also had a final cumulative tax at the end.
     }
 
     my @eg_inv_items;
@@ -937,31 +982,33 @@ sub create_acq_invoice_from_edi {
     my %charge_type_map = (
         'TX' => ['TAX', 'Tax from electronic invoice'],
         'CA' => ['PRO', 'Cataloging services'], 
-        'DL' => ['SHP', 'Delivery']
-    );
+        'DL' => ['SHP', 'Delivery'],
+        'GST' => ['TAX', 'Goods and services tax']
+    ); # XXX i18n, somehow
 
-    for my $charge (@{$msg_data->{misc_charges}}) {
+    for my $charge (@{$msg_data->{misc_charges}}, @{$msg_data->{taxes}}) {
         my $eg_inv_item = Fieldmapper::acq::invoice_item->new;
 
-        my $amount = $charge->{charge_amount};
+        my $amount = $charge->{amount};
 
         if (!$amount) {
             $logger->warn($log_prefix . "charge with no amount");
             next;
         }
 
-        my $map = $charge_type_map{$charge->{charge_type}};
+        my $map = $charge_type_map{$charge->{type}};
 
         if (!$map) {
             $map = [
                 'PRO',
-                'Unknown charge type ' .  $charge->{charge_type}
+                'Unknown charge type ' .  $charge->{type}
             ];
         }
 
         $eg_inv_item->inv_item_type($$map[0]);
-        $eg_inv_item->note($$map[1]);
+        $eg_inv_item->title($$map[1]);  # title is user-visible; note isn't.
         $eg_inv_item->cost_billed($amount);
+        $eg_inv_item->amount_paid($amount);
 
         push @eg_inv_items, $eg_inv_item;
     }
index 5a28b8f..3a3ecc6 100644 (file)
@@ -17,6 +17,7 @@ use strict; use warnings;
 
 my $NEW_MSG_RE = '^UNH'; # starts a new message
 my $NEW_LIN_RE = '^LIN'; # starts a new line item
+my $END_ALL_LIN = '^UNS'; # no more lineitems after this
 
 my %edi_fields = (
     message_type    => qr/^UNH\+[A-z0-9]+\+(\S{6})/,
@@ -54,8 +55,15 @@ my %edi_li_quant_fields = (
 );
 
 my %edi_charge_fields = (
-    charge_type   => qr/^ALC\+C\++([^\+]+)/,
-    charge_amount => qr/^MOA\+(?:8|131|304):([^:]+)/
+    type   => qr/^ALC\+C\++([^\+]+)/,
+    amount => qr/^MOA\+(?:8|131|304):([^:]+)/
+);
+
+# This may need to be liberalized later, but it works for the only example I
+# have so far.
+my %edi_tax_fields = (
+    type   => qr/^TAX\+7\+([^\+]+)/,
+    amount => qr/^MOA\+124:([^:]+)/
 );
 
 sub new {
@@ -92,7 +100,7 @@ sub read {
         # - starting a new message
 
         if (/$NEW_MSG_RE/) { 
-            $msg = {lineitems => [], misc_charges => []};
+            $msg = {lineitems => [], misc_charges => [], taxes => []};
             push(@msgs, $msg);
         }
 
@@ -139,7 +147,7 @@ sub read {
 
         # - starting a new misc. charge
 
-        if (/$edi_charge_fields{charge_type}/) {
+        if (/$edi_charge_fields{type}/) {
             $msg->{_current_charge} = {};
             push (@{$msg->{misc_charges}}, $msg->{_current_charge});
         }
@@ -152,6 +160,36 @@ sub read {
                     if /$edi_charge_fields{$field}/;
             }
         }
+
+        # - starting a new tax charge.  Taxes wind up on current lineitem if
+        # any, otherwise in the top-level taxes array
+
+        if (/$edi_tax_fields{type}/) {
+            $msg->{_current_tax} = {};
+            if ($msg->{_current_li}) {
+                $msg->{_current_li}{tax} = $msg->{_current_tax}
+            } else {
+                push (@{$msg->{taxes}}, $msg->{_current_tax});
+            }
+        }
+
+        # - extract tax field
+
+        if (my $tax = $msg->{_current_tax}) {
+            for my $field (keys %edi_tax_fields) {
+                ($tax->{$field}) = $_ =~ /$edi_tax_fields{$field}/
+                    if /$edi_tax_fields{$field}/;
+            }
+        }
+
+        # This helps avoid associating taxes and charges at the end of the
+        # message with the final lineitem inapporiately.
+        if (/$END_ALL_LIN/) {
+            # remove the state-maintenance keys
+            foreach (grep /^_/, keys %$msg) {
+                delete $msg->{$_};
+            }
+        }
     }
 
     # remove the state-maintenance keys