Port INVOIC to EDIReader
authorBill Erickson <berick@esilibrary.com>
Wed, 3 Oct 2012 14:29:01 +0000 (10:29 -0400)
committerBill Erickson <berick@esilibrary.com>
Wed, 3 Oct 2012 14:29:01 +0000 (10:29 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/EDI.pm

index 09cdd87..ee50f41 100644 (file)
@@ -177,7 +177,7 @@ sub process_retrieval {
         $incoming->account($account->id);
         $incoming->edi($content);
         $incoming->message_type($msg_hash->{message_type});
-        $incoming->jedi(OpenSRF::Utils::JSON->perl2JSON($msg_hash)); # meh
+        $incoming->jedi(OpenSRF::Utils::JSON->perl2JSON($msg_hash)); # jedi-2.0
 
         if ($msg_hash->{purchase_order}) {
             $logger->info("EDI: processing message for PO " . $msg_hash->{purchase_order});
@@ -189,6 +189,8 @@ sub process_retrieval {
             $logger->error("EDI: unable to create edi_message " . $e->die_event);
             next;
         }
+        # refresh to pickup create_date, etc.
+        $incoming = $e->retrieve_acq_edi_message($incoming->id);
         $e->xact_commit;
 
         # since there's a fair chance of unhandled problems 
@@ -432,10 +434,11 @@ sub process_parsed_msg {
     my ($class, $account, $incoming, $msg_hash) = @_;
 
     if ($incoming->message_type eq 'INVOIC') {
-        # handle invoice
-        return;
+        return $class->create_acq_invoice_from_edi(
+            $msg_hash, $account->provider, $incoming);
     }
 
+    # ORDRSP
     for my $li_hash (@{$msg_hash->{lineitems}}) {
         my $e = new_editor(xact => 1);
 
@@ -573,11 +576,13 @@ sub process_parsed_msg {
 # process_jedi().
 # Return boolean success indicator.
 sub create_acq_invoice_from_edi {
-    my ($class, $e, $invoice, $provider, $message) = @_;
-    # $invoice is O::U::LooseEDI::Message, representing the EDI invoice message.
+    my ($class, $invoice, $provider, $message) = @_;
+    # $invoice is O::U::EDIReader hash
     # $provider is only a pkey
     # $message is Fieldmapper::acq::edi_message
 
+    my $e = new_editor();
+
     my $log_prefix = "create_acq_invoice_from_edi(..., <acq.edi_message #" .
         $message->id . ">): ";
 
@@ -588,20 +593,16 @@ sub create_acq_invoice_from_edi {
                                     # distinguish provider and shipper?
     $eg_inv->recv_method("EDI");
 
-    # Find the buyer's identifier in the invoice.
-    my $buyer_san;
-    foreach (@{$invoice->{SG2}}) {
-        my $nad = $_->{NAD}[0];
-        if ($nad->{3035} eq 'BY' and $nad->{C082}{3055} eq '91') {
-            $buyer_san = $nad->{C082}{3039};
-        }
-    }
+    my $buyer_san = $invoice->{buyer_san};
 
     if (not $buyer_san) {
         $logger->error($log_prefix . "could not find buyer SAN in INVOIC");
         return 0;
     }
 
+    # some vendors encode the SAN as "$SAN $vendcode"
+    $buyer_san =~ s/\s.*//g;
+
     # Find the matching org unit based on SAN via 'aoa' table.
     my $addrs =
         $e->search_actor_org_address({valid => "t", san => $buyer_san});
@@ -617,130 +618,61 @@ sub create_acq_invoice_from_edi {
     # XXX Should we verify that this matches PO ordering agency later?
     $eg_inv->receiver($addrs->[0]->org_unit);
 
-    try {
-        $eg_inv->inv_ident($invoice->{BGM}[0]{1004});
-    } catch Error with {
+    $eg_inv->inv_ident($invoice->{invoice_ident});
+
+    if (!$eg_inv->inv_ident) {
         $logger->error(
             $log_prefix . "no invoice ID # in INVOIC message; " . shift
         );
-    };
-    return 0 unless $eg_inv->inv_ident;
+        return 0;
+    }
 
     my @eg_inv_entries;
 
-    # The invoice message will have once instance of segment group 25
-    # per lineitem.
-    foreach my $sg25 (@{ $invoice->{SG25} }) {
-        # quantity
-        my $c186 = $sg25->{QTY}[0]{C186};
-        my $quantity = $c186->{6060};
-        # $c186->{6411} will probably say 'PCE', but need we check it?
-
-        # identifiers (typically ISBN for us, and we may not need these)
-        my @identifiers = ();
-        #   from LIN...
-        try {
-            my $c212 = $sg25->{LIN}[0]{C212};
-            push @identifiers, [$c212->{7143}, $c212->{7140}] if
-                $c212 and ref $c212 eq 'HASH';
-        } catch Error with {
-            # move on
-        };
-
-        #   from PIA...
-        try {
-            foreach my $pia (@{ $sg25->{PIA} }) {
-                foreach my $h (@{$pia->{C212}}) {
-                    push @identifiers, [$h->{7143}, $h->{7140}];
-                }
-            }
-        } catch Error with {
-            # move on
-        };
-
-        # @identifiers now contains lists of, say,
-        # ['IB',   '0786222735'], # ISBN 10
-        # ['EN','9780786222735']  # ISBN 13
-
-        # Segment Group 26-47 are all descendants of SG25.
+    $message->purchase_order($invoice->{purchase_order});
 
-        # Segment Group 26 concerns *lineitem* price (i.e, total for all copies
-        # on this lineitem).
+    for my $lineitem (@{$invoice->{lineitems}}) {
+        my $li_id = $lineitem->{id};
 
-        my $lineitem_price = $sg25->{SG26}[0]{MOA}[0]{C516}{5004};
+        if (!$li_id) {
+            $logger->warn($log_prefix . "no lineitem ID");
+            next;
+        }
 
-        # Segment Group 28 concerns *unit* (lineitem detail) price.  We may
-        # not actually use this.  TBD.
-        my $per_unit_price;
-        foreach my $sg28 (@{$sg25->{SG28}}) {
-            my $c509 = $sg28->{PRI}[0]{C509};
-            my ($price_qualifier, $price_qualifier_type);
-            ($per_unit_price, $price_qualifier, $price_qualifier_type) = (
-                $c509->{5118}, $c509->{5125}, $c509->{5387}
-            );
+        my $li = $e->retrieve_acq_lineitem($li_id);
 
-            # price_qualifier=AAA seems to be the price to use.  Otherwise,
-            # take what we can get.
-            last if $price_qualifier eq 'AAA';
+        if (!$li) {
+            $logger->warn($log_prefix . 
+                "no LI found with ID: $li_id : " . $e->event);
+            return 0;
         }
 
-        # Segment Group 29 will have references to LI and PO numbers
-        my $acq_identifiers = {};
-        foreach my $sg29 (@{$sg25->{SG29}}) {
-            foreach my $rff (@{$sg29->{RFF}}) {
-                my $c506 = $rff->{C506};
-                if ($c506->{1153} eq 'ON') {
-                    $acq_identifiers->{po} = $c506->{1154};
-                } elsif ($c506->{1153} eq 'LI') {
-                    my ($po, $li) = split m./., $c506->{1154};
-                    if ($po and $li) {
-                        if ($acq_identifiers->{po}) {
-                            $logger->warn(
-                                $log_prefix .
-                                "RFFs within lineitem disagree on PO # ?"
-                            ) unless $acq_identifiers->{po} eq $po;
-                        }
-                        $acq_identifiers->{li} = $li;
-                        $acq_identifiers->{po} = $po;
-                    } else {
-                        $logger->warn(
-                            $log_prefix .
-                            "RFF 1154 doesn't match expectations (.+/.+) " .
-                            "where 1153 is 'LI'"
-                        );
-                    }
-                }
-            }
+        my ($quant) = grep {$_->{code} eq '47'} @{$lineitem->{quantities}};
+        my $quantity = ($quant) ? $quant->{quantity} : 0;
+        
+        if (!$quantity) {
+            $logger->warn($log_prefix . 
+                "no invoice quantity specified for LI $li_id");
+            next;
         }
 
-        if ($acq_identifiers->{po}) {
-            # First PO number seen in INVOIC sets the purchase_order field for
-            # the entry in acq.edi_message (which model may need a rethink).
+        # NOTE: if needed, we also have $lineitem->{net_unit_price}
+        # and $lineitem->{gross_unit_price}
+        my $lineitem_price = $lineitem->{amount_billed};
 
-            $message->purchase_order($acq_identifiers->{po}) unless
-                $message->purchase_order;
-        } else {
-            $logger->warn(
-                $log_prefix .
-                "SG29 missing or refers to no purchase order that we can tell"
-            );
-        }
-        if (not $acq_identifiers->{li}) {
-            $logger->warn(
-                $log_prefix .
-                "SG29 missing or refers to no lineitem that we can tell"
-            );
-        }
+        # if the top-level PO value is unset, get it from the first LI
+        $message->purchase_order($li->purchase_order)
+            unless $message->purchase_order;
 
         my $eg_inv_entry = Fieldmapper::acq::invoice_entry->new;
         $eg_inv_entry->inv_item_count($quantity);
 
         # XXX Validate by making sure the LI is on-order and belongs to
         # the right provider and ordering agency and all that.
-        $eg_inv_entry->lineitem($acq_identifiers->{li});
+        $eg_inv_entry->lineitem($li_id);
 
         # XXX Do we actually need to link to PO directly here?
-        $eg_inv_entry->purchase_order($acq_identifiers->{po});
+        $eg_inv_entry->purchase_order($li->purchase_order);
 
         # This is the total price for all units billed, not per-unit.
         $eg_inv_entry->cost_billed($lineitem_price);
@@ -750,26 +682,41 @@ sub create_acq_invoice_from_edi {
 
     my @eg_inv_items;
 
-    # Find any taxes applied to the whole invoice.
-    try {
-        if ($invoice->{SG50}) {
-            foreach my $sg50 (@{ $invoice->{SG50} }) {
-                if ($sg50->{TAX} and $sg50->{MOA}) {
-                    my $tax_amount = $sg50->{MOA}[0]{C516}{5004};
+    my %charge_type_map = (
+        'TX' => ['TAX', 'Tax from electronic invoice'],
+        'CA' => ['PRO', 'Cataloging services'], 
+        'DL' => ['SHP', 'Delivery']
+    );
 
-                    my $eg_inv_item = Fieldmapper::acq::invoice_item->new;
-                    $eg_inv_item->inv_item_type('TAX');
-                    $eg_inv_item->cost_billed($tax_amount);
-                    # XXX i18n somehow? or maybe omit the note.
-                    $eg_inv_item->note('Tax from electronic invoice');
+    for my $charge (@{$invoice->{misc_charges}}) {
+        my $eg_inv_item = Fieldmapper::acq::invoice_item->new;
 
-                    push @eg_inv_items, $eg_inv_item;
-                }
-            }
+        my $amount = $charge->{charge_amount};
+
+        if (!$amount) {
+            $logger->warn($log_prefix . "charge with no amount");
+            next;
         }
-    } catch Error with {
-        # move on
-    };
+
+        my $map = $charge_type_map{$charge->{charge_type}};
+
+        if (!$map) {
+            $map = [
+                'PRO',
+                'Unknown charge type ' .  $charge->{charge_type}
+            ];
+        }
+
+        $eg_inv_item->inv_item_type($$map[0]);
+        $eg_inv_item->note($$map[1]);
+        $eg_inv_item->cost_billed($amount);
+
+        push @eg_inv_items, $eg_inv_item;
+    }
+
+    $logger->info($log_prefix . 
+        sprintf("creating invoice with %d entries and %d items.",
+            scalar(@eg_inv_entries), scalar(@eg_inv_items)));
 
     $e->xact_begin;