Bug 20473: Whitespace
[koha.git] / Koha / EDI.pm
1 package Koha::EDI;
2
3 # Copyright 2014,2015 PTFS-Europe Ltd
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use strict;
21 use warnings;
22 use base qw(Exporter);
23 use utf8;
24 use Carp qw( carp );
25 use English qw{ -no_match_vars };
26 use Business::ISBN;
27 use DateTime;
28 use C4::Context;
29 use Koha::Database;
30 use Koha::DateUtils qw( dt_from_string );
31 use C4::Acquisition qw( ModOrder NewBasket );
32 use C4::Suggestions qw( ModSuggestion );
33 use C4::Biblio qw(
34     AddBiblio
35     GetFrameworkCode
36     GetMarcFromKohaField
37     TransformKohaToMarc
38 );
39 use Koha::Edifact::Order;
40 use Koha::Edifact;
41 use C4::Log qw( logaction );
42 use Log::Log4perl;
43 use Text::Unidecode qw( unidecode );
44 use Koha::Plugins; # Adds plugin dirs to @INC
45 use Koha::Plugins::Handler;
46 use Koha::Acquisition::Baskets;
47 use Koha::Acquisition::Booksellers;
48
49 our $VERSION = 1.1;
50
51 our (@ISA, @EXPORT_OK);
52 BEGIN {
53     require Exporter;
54     @ISA = qw(Exporter);
55     @EXPORT_OK = qw(
56       process_quote
57       process_invoice
58       process_ordrsp
59       create_edi_order
60       get_edifact_ean
61     );
62 };
63
64 sub create_edi_order {
65     my $parameters = shift;
66     my $basketno   = $parameters->{basketno};
67     my $ean        = $parameters->{ean};
68     my $branchcode = $parameters->{branchcode};
69     my $noingest   = $parameters->{noingest};
70     if ( !$basketno || !$ean ) {
71         carp 'create_edi_order called with no basketno or ean';
72         return;
73     }
74
75     my $schema = Koha::Database->new()->schema();
76
77     my @orderlines = $schema->resultset('Aqorder')->search(
78         {
79             basketno    => $basketno,
80             orderstatus => 'new',
81         }
82     )->all;
83
84     if ( !@orderlines ) {
85         carp "No orderlines for basket $basketno";
86         return;
87     }
88
89     my $vendor = $schema->resultset('VendorEdiAccount')->search(
90         {
91             vendor_id => $orderlines[0]->basketno->booksellerid->id,
92         }
93     )->single;
94
95     my $ean_search_keys = { ean => $ean, };
96     if ($branchcode) {
97         $ean_search_keys->{branchcode} = $branchcode;
98     }
99     my $ean_obj =
100       $schema->resultset('EdifactEan')->search($ean_search_keys)->single;
101
102     # If no branch specific each can be found, look for a default ean
103     unless ($ean_obj) {
104         $ean_obj = $schema->resultset('EdifactEan')->search(
105             {
106                 ean        => $ean,
107                 branchcode => undef,
108             }
109         )->single;
110     }
111
112     my $dbh     = C4::Context->dbh;
113     my $arr_ref = $dbh->selectcol_arrayref(
114 'select id from edifact_messages where basketno = ? and message_type = \'QUOTE\'',
115         {}, $basketno
116     );
117     my $response = @{$arr_ref} ? 1 : 0;
118
119     my $edifact_order_params = {
120         orderlines  => \@orderlines,
121         vendor      => $vendor,
122         ean         => $ean_obj,
123         is_response => $response,
124     };
125
126     my $edifact;
127     if ( $vendor->plugin ) {
128         $edifact = Koha::Plugins::Handler->run(
129             {
130                 class  => $vendor->plugin,
131                 method => 'edifact_order',
132                 params => {
133                     params => $edifact_order_params,
134                 }
135             }
136         );
137     }
138     else {
139         $edifact = Koha::Edifact::Order->new($edifact_order_params);
140     }
141
142     return unless $edifact;
143
144     my $order_file = $edifact->encode();
145
146     # ingest result
147     if ($order_file) {
148         my $m = unidecode($order_file);  # remove diacritics and non-latin chars
149         if ($noingest) {                 # allows scripts to produce test files
150             return $m;
151         }
152         my $order = {
153             message_type  => 'ORDERS',
154             raw_msg       => $m,
155             vendor_id     => $vendor->vendor_id,
156             status        => 'Pending',
157             basketno      => $basketno,
158             filename      => $edifact->filename(),
159             transfer_date => $edifact->msg_date_string(),
160             edi_acct      => $vendor->id,
161
162         };
163         $schema->resultset('EdifactMessage')->create($order);
164         return 1;
165     }
166
167     return;
168 }
169
170 sub process_ordrsp {
171     my $response_message = shift;
172     $response_message->status('processing');
173     $response_message->update;
174     my $schema = Koha::Database->new()->schema();
175     my $logger = Log::Log4perl->get_logger();
176     my $vendor_acct;
177     my $edi =
178       Koha::Edifact->new( { transmission => $response_message->raw_msg, } );
179     my $messages = $edi->message_array();
180
181     if ( @{$messages} ) {
182         foreach my $msg ( @{$messages} ) {
183             my $lines = $msg->lineitems();
184             foreach my $line ( @{$lines} ) {
185                 my $ordernumber = $line->ordernumber();
186
187        # action cancelled:change_requested:no_action:accepted:not_found:recorded
188                 my $action = $line->action_notification();
189                 if ( $action eq 'cancelled' ) {
190                     my $reason = $line->coded_orderline_text();
191                     ModOrder(
192                         {
193                             ordernumber             => $ordernumber,
194                             cancellationreason      => $reason,
195                             orderstatus             => 'cancelled',
196                             datecancellationprinted => dt_from_string()->ymd(),
197                         }
198                     );
199                 }
200                 else {    # record order as due with possible further info
201
202                     my $report     = $line->coded_orderline_text();
203                     my $date_avail = $line->availability_date();
204                     $report ||= q{};
205                     if ($date_avail) {
206                         $report .= " Available: $date_avail";
207                     }
208                     ModOrder(
209                         {
210                             ordernumber      => $ordernumber,
211                             suppliers_report => $report,
212                         }
213                     );
214                 }
215             }
216         }
217     }
218
219     $response_message->status('received');
220     $response_message->update;
221     return;
222 }
223
224 sub process_invoice {
225     my $invoice_message = shift;
226     $invoice_message->status('processing');
227     $invoice_message->update;
228     my $schema = Koha::Database->new()->schema();
229     my $logger = Log::Log4perl->get_logger();
230     my $vendor_acct;
231
232     my $plugin_class = $invoice_message->edi_acct()->plugin();
233
234     # Plugin has its own invoice processor, only run it and not the standard invoice processor below
235     if ( $plugin_class ) {
236         eval "require $plugin_class"; # Import the class, eval is needed because requiring a string doesn't work like requiring a bareword
237         my $plugin = $plugin_class->new();
238         if ( $plugin->can('edifact_process_invoice') ) {
239             Koha::Plugins::Handler->run(
240                 {
241                     class  => $plugin_class,
242                     method => 'edifact_process_invoice',
243                     params => {
244                         invoice => $invoice_message,
245                     }
246                 }
247             );
248             return;
249         }
250     }
251
252     my $edi_plugin;
253     if ( $plugin_class ) {
254         $edi_plugin = Koha::Plugins::Handler->run(
255             {
256                 class  => $plugin_class,
257                 method => 'edifact',
258                 params => {
259                     invoice_message => $invoice_message,
260                     transmission => $invoice_message->raw_msg,
261                 }
262             }
263         );
264     }
265
266     my $edi = $edi_plugin ||
267       Koha::Edifact->new( { transmission => $invoice_message->raw_msg, } );
268
269     my $messages = $edi->message_array();
270
271     if ( @{$messages} ) {
272
273         # BGM contains an invoice number
274         foreach my $msg ( @{$messages} ) {
275             my $invoicenumber  = $msg->docmsg_number();
276             my $shipmentcharge = $msg->shipment_charge();
277             my $msg_date       = $msg->message_date;
278             my $tax_date       = $msg->tax_point_date;
279             if ( !defined $tax_date || $tax_date !~ m/^\d{8}/xms ) {
280                 $tax_date = $msg_date;
281             }
282
283             my $vendor_ean = $msg->supplier_ean;
284             if ( !defined $vendor_acct || $vendor_ean ne $vendor_acct->san ) {
285                 $vendor_acct = $schema->resultset('VendorEdiAccount')->search(
286                     {
287                         san => $vendor_ean,
288                     }
289                 )->single;
290             }
291             if ( !$vendor_acct ) {
292                 carp
293 "Cannot find vendor with ean $vendor_ean for invoice $invoicenumber in $invoice_message->filename";
294                 next;
295             }
296             $invoice_message->edi_acct( $vendor_acct->id );
297             $logger->trace("Adding invoice:$invoicenumber");
298             my $new_invoice = $schema->resultset('Aqinvoice')->create(
299                 {
300                     invoicenumber         => $invoicenumber,
301                     booksellerid          => $invoice_message->vendor_id,
302                     shipmentdate          => $msg_date,
303                     billingdate           => $tax_date,
304                     shipmentcost          => $shipmentcharge,
305                     shipmentcost_budgetid => $vendor_acct->shipment_budget,
306                     message_id            => $invoice_message->id,
307                 }
308             );
309             my $invoiceid = $new_invoice->invoiceid;
310             $logger->trace("Added as invoiceno :$invoiceid");
311             my $lines = $msg->lineitems();
312
313             foreach my $line ( @{$lines} ) {
314                 my $ordernumber = $line->ordernumber;
315                 if (!$ordernumber ) {
316                    $logger->trace( "Skipping invoice line, no associated ordernumber" );
317                    next;
318                 }
319
320                 $logger->trace( "Receipting order:$ordernumber Qty: ",
321                     $line->quantity );
322
323                 my $order = $schema->resultset('Aqorder')->find($ordernumber);
324                 if (my $bib = $order->biblionumber) {
325                     my $b = $bib->biblionumber;
326                     my $id = $line->item_number_id;
327                     $logger->trace("Updating bib:$b id:$id");
328                 }
329
330                 # ModReceiveOrder does not validate that $ordernumber exists validate here
331                 if ($order) {
332
333                     # check suggestions
334                     my $s = $schema->resultset('Suggestion')->search(
335                         {
336                             biblionumber => $order->biblionumber->biblionumber,
337                         }
338                     )->single;
339                     if ($s) {
340                         ModSuggestion(
341                             {
342                                 suggestionid => $s->suggestionid,
343                                 STATUS       => 'AVAILABLE',
344                             }
345                         );
346                     }
347                     # If quantity_invoiced is present use it in preference
348                     my $quantity = $line->quantity_invoiced;
349                     if (!$quantity) {
350                         $quantity = $line->quantity;
351                     }
352
353                     my ( $price, $price_excl_tax ) = _get_invoiced_price($line, $quantity);
354                     my $tax_rate = $line->tax_rate;
355                     if ($tax_rate && $tax_rate->{rate} != 0) {
356                        $tax_rate->{rate} /= 100;
357                     }
358
359                     if ( $order->quantity > $quantity ) {
360                         my $ordered = $order->quantity;
361
362                         # part receipt
363                         $order->orderstatus('partial');
364                         $order->quantity( $ordered - $quantity );
365                         $order->update;
366                         my $received_order = $order->copy(
367                             {
368                                 ordernumber            => undef,
369                                 quantity               => $quantity,
370                                 quantityreceived       => $quantity,
371                                 orderstatus            => 'complete',
372                                 unitprice              => $price,
373                                 unitprice_tax_included => $price,
374                                 unitprice_tax_excluded => $price_excl_tax,
375                                 invoiceid              => $invoiceid,
376                                 datereceived           => $msg_date,
377                                 tax_rate_on_receiving  => $tax_rate->{rate},
378                                 tax_value_on_receiving => $quantity * $price_excl_tax * $tax_rate->{rate},
379                             }
380                         );
381                         transfer_items( $schema, $line, $order,
382                             $received_order, $quantity );
383                         receipt_items( $schema, $line,
384                             $received_order->ordernumber, $quantity );
385                     }
386                     else {    # simple receipt all copies on order
387                         $order->quantityreceived( $quantity );
388                         $order->datereceived($msg_date);
389                         $order->invoiceid($invoiceid);
390                         $order->unitprice($price);
391                         $order->unitprice_tax_excluded($price_excl_tax);
392                         $order->unitprice_tax_included($price);
393                         $order->tax_rate_on_receiving($tax_rate->{rate});
394                         $order->tax_value_on_receiving( $quantity * $price_excl_tax * $tax_rate->{rate});
395                         $order->orderstatus('complete');
396                         $order->update;
397                         receipt_items( $schema, $line, $ordernumber, $quantity );
398                     }
399                 }
400                 else {
401                     $logger->error(
402                         "No order found for $ordernumber Invoice:$invoicenumber"
403                     );
404                     next;
405                 }
406
407             }
408
409         }
410     }
411
412     $invoice_message->status('received');
413     $invoice_message->update;    # status and basketno link
414     return;
415 }
416
417 sub _get_invoiced_price {
418     my $line       = shift;
419     my $qty        = shift;
420     my $line_total = $line->amt_total;
421     my $excl_tax   = $line->amt_lineitem;
422
423     # If no tax some suppliers omit the total owed
424     # If no total given calculate from cost exclusive of tax
425     # + tax amount (if present, sometimes omitted if 0 )
426     if ( !defined $line_total ) {
427         my $x = $line->amt_taxoncharge;
428         if ( !defined $x ) {
429             $x = 0;
430         }
431         $line_total = $excl_tax + $x;
432     }
433
434     # invoices give amounts per orderline, Koha requires that we store
435     # them per item
436     if ( $qty != 1 ) {
437         return ( $line_total / $qty, $excl_tax / $qty );
438     }
439     return ( $line_total, $excl_tax );    # return as is for most common case
440 }
441
442 sub receipt_items {
443     my ( $schema, $inv_line, $ordernumber, $quantity ) = @_;
444     my $logger   = Log::Log4perl->get_logger();
445
446     # itemnumber is not a foreign key ??? makes this a bit cumbersome
447     my @item_links = $schema->resultset('AqordersItem')->search(
448         {
449             ordernumber => $ordernumber,
450         }
451     );
452     my %branch_map;
453     foreach my $ilink (@item_links) {
454         my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
455         if ( !$item ) {
456             my $i = $ilink->itemnumber;
457             $logger->warn(
458                 "Cannot find aqorder item for $i :Order:$ordernumber");
459             next;
460         }
461         my $b = $item->get_column('homebranch');
462         if ( !exists $branch_map{$b} ) {
463             $branch_map{$b} = [];
464         }
465         push @{ $branch_map{$b} }, $item;
466     }
467
468     # Handling for 'AcqItemSetSubfieldsWhenReceived'
469     my @affects;
470     my $biblionumber;
471     my $itemfield;
472     if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
473         @affects = split q{\|},
474           C4::Context->preference("AcqItemSetSubfieldsWhenReceived");
475         if (@affects) {
476             my $order = Koha::Acquisition::Orders->find($ordernumber);
477             $biblionumber = $order->biblionumber;
478             my $frameworkcode = GetFrameworkCode($biblionumber);
479             ($itemfield) = GetMarcFromKohaField( 'items.itemnumber',
480                 $frameworkcode );
481         }
482     }
483
484     my $gir_occurrence = 0;
485     while ( $gir_occurrence < $quantity ) {
486         my $branch = $inv_line->girfield( 'branch', $gir_occurrence );
487         my $item = shift @{ $branch_map{$branch} };
488         if ($item) {
489             my $barcode = $inv_line->girfield( 'barcode', $gir_occurrence );
490             if ( $barcode && !$item->barcode ) {
491                 my $rs = $schema->resultset('Item')->search(
492                     {
493                         barcode => $barcode,
494                     }
495                 );
496                 if ( $rs->count > 0 ) {
497                     $logger->warn("Barcode $barcode is a duplicate");
498                 }
499                 else {
500
501                     $logger->trace("Adding barcode $barcode");
502                     $item->barcode($barcode);
503                 }
504             }
505
506             # Handling for 'AcqItemSetSubfieldsWhenReceived'
507             if (@affects) {
508                 my $item_marc = C4::Items::GetMarcItem( $biblionumber, $item->itemnumber );
509                 for my $affect (@affects) {
510                     my ( $sf, $v ) = split q{=}, $affect, 2;
511                     foreach ( $item_marc->field($itemfield) ) {
512                         $_->update( $sf => $v );
513                     }
514                 }
515                 C4::Items::ModItemFromMarc( $item_marc, $biblionumber, $item->itemnumber );
516             }
517
518             $item->update;
519         }
520         else {
521             $logger->warn("Unmatched item at branch:$branch");
522         }
523         ++$gir_occurrence;
524     }
525     return;
526
527 }
528
529 sub transfer_items {
530     my ( $schema, $inv_line, $order_from, $order_to, $quantity ) = @_;
531
532     # Transfer x items from the orig order to a completed partial order
533     my $gocc     = 0;
534     my %mapped_by_branch;
535     while ( $gocc < $quantity ) {
536         my $branch = $inv_line->girfield( 'branch', $gocc );
537         if ( !exists $mapped_by_branch{$branch} ) {
538             $mapped_by_branch{$branch} = 1;
539         }
540         else {
541             $mapped_by_branch{$branch}++;
542         }
543         ++$gocc;
544     }
545     my $logger = Log::Log4perl->get_logger();
546     my $o1     = $order_from->ordernumber;
547     my $o2     = $order_to->ordernumber;
548     $logger->warn("transferring $quantity copies from order $o1 to order $o2");
549
550     my @item_links = $schema->resultset('AqordersItem')->search(
551         {
552             ordernumber => $order_from->ordernumber,
553         }
554     );
555     foreach my $ilink (@item_links) {
556         my $ino      = $ilink->itemnumber;
557         my $item     = $schema->resultset('Item')->find( $ilink->itemnumber );
558         my $i_branch = $item->get_column('homebranch');
559         if ( exists $mapped_by_branch{$i_branch}
560             && $mapped_by_branch{$i_branch} > 0 )
561         {
562             $ilink->ordernumber( $order_to->ordernumber );
563             $ilink->update;
564             --$quantity;
565             --$mapped_by_branch{$i_branch};
566             $logger->warn("Transferred item $item");
567         }
568         else {
569             $logger->warn("Skipped item $item");
570         }
571         if ( $quantity < 1 ) {
572             last;
573         }
574     }
575
576     return;
577 }
578
579 sub process_quote {
580     my $quote = shift;
581
582     $quote->status('processing');
583     $quote->update;
584
585     my $edi = Koha::Edifact->new( { transmission => $quote->raw_msg, } );
586
587     my $messages       = $edi->message_array();
588     my $process_errors = 0;
589     my $logger         = Log::Log4perl->get_logger();
590     my $schema         = Koha::Database->new()->schema();
591     my $message_count  = 0;
592     my @added_baskets;    # if auto & multiple baskets need to order all
593
594     if ( @{$messages} && $quote->vendor_id ) {
595         foreach my $msg ( @{$messages} ) {
596             ++$message_count;
597             my $basketno =
598               NewBasket( $quote->vendor_id, 0, $quote->filename, q{},
599                 q{} . q{} );
600             push @added_baskets, $basketno;
601             if ( $message_count > 1 ) {
602                 my $m_filename = $quote->filename;
603                 $m_filename .= "_$message_count";
604                 $schema->resultset('EdifactMessage')->create(
605                     {
606                         message_type  => $quote->message_type,
607                         transfer_date => $quote->transfer_date,
608                         vendor_id     => $quote->vendor_id,
609                         edi_acct      => $quote->edi_acct,
610                         status        => 'recmsg',
611                         basketno      => $basketno,
612                         raw_msg       => q{},
613                         filename      => $m_filename,
614                     }
615                 );
616             }
617             else {
618                 $quote->basketno($basketno);
619             }
620             $logger->trace("Created basket :$basketno");
621             my $items  = $msg->lineitems();
622             my $refnum = $msg->message_refno;
623
624             for my $item ( @{$items} ) {
625                 if ( !quote_item( $item, $quote, $basketno ) ) {
626                     ++$process_errors;
627                 }
628             }
629         }
630     }
631     my $status = 'received';
632     if ($process_errors) {
633         $status = 'error';
634     }
635
636     $quote->status($status);
637     $quote->update;    # status and basketno link
638                        # Do we automatically generate orders for this vendor
639     my $v = $schema->resultset('VendorEdiAccount')->search(
640         {
641             vendor_id => $quote->vendor_id,
642         }
643     )->single;
644     if ( $v->auto_orders ) {
645         for my $b (@added_baskets) {
646             create_edi_order(
647                 {
648                     ean      => $messages->[0]->buyer_ean,
649                     basketno => $b,
650                 }
651             );
652             Koha::Acquisition::Baskets->find($b)->close;
653             # Log the approval
654             if (C4::Context->preference("AcquisitionLog")) {
655                 my $approved = Koha::Acquisition::Baskets->find( $b );
656                 logaction(
657                     'ACQUISITIONS',
658                     'APPROVE_BASKET',
659                     $b,
660                     to_json($approved->unblessed)
661                 );
662             }
663         }
664     }
665
666
667     return;
668 }
669
670 sub quote_item {
671     my ( $item, $quote, $basketno ) = @_;
672
673     my $schema = Koha::Database->new()->schema();
674     my $logger = Log::Log4perl->get_logger();
675
676     # $basketno is the return from AddBasket in the calling routine
677     # So this call should not fail unless that has
678     my $basket = Koha::Acquisition::Baskets->find( $basketno );
679     unless ( $basket ) {
680         $logger->error('Skipping order creation no valid basketno');
681         return;
682     }
683     $logger->trace( 'Checking db for matches with ', $item->item_number_id() );
684     my $bib = _check_for_existing_bib( $item->item_number_id() );
685     if ( !defined $bib ) {
686         $bib = {};
687         my $bib_record = _create_bib_from_quote( $item, $quote );
688         ( $bib->{biblionumber}, $bib->{biblioitemnumber} ) =
689           AddBiblio( $bib_record, q{} );
690         $logger->trace("New biblio added $bib->{biblionumber}");
691     }
692     else {
693         $logger->trace("Match found: $bib->{biblionumber}");
694     }
695
696     # Create an orderline
697     my $order_note = $item->{orderline_free_text};
698     $order_note ||= q{};
699     my $order_quantity = $item->quantity();
700     my $gir_count      = $item->number_of_girs();
701     $order_quantity ||= 1;    # quantity not necessarily present
702     if ( $gir_count > 1 ) {
703         if ( $gir_count != $order_quantity ) {
704             $logger->error(
705                 "Order for $order_quantity items, $gir_count segments present");
706         }
707         $order_quantity = 1;    # attempts to create an orderline for each gir
708     }
709     my $price  = $item->price_info;
710     # Howells do not send an info price but do have a gross price
711     if (!$price) {
712         $price = $item->price_gross;
713     }
714     my $vendor = Koha::Acquisition::Booksellers->find( $quote->vendor_id );
715
716     # NB quote will not include tax info it only contains the list price
717     my $ecost = _discounted_price( $vendor->discount, $price, $item->price_info_inclusive );
718
719     # database definitions should set some of these defaults but dont
720     my $order_hash = {
721         biblionumber       => $bib->{biblionumber},
722         entrydate          => dt_from_string()->ymd(),
723         basketno           => $basketno,
724         listprice          => $price,
725         quantity           => $order_quantity,
726         quantityreceived   => 0,
727         order_vendornote   => q{},
728         order_internalnote => q{},
729         replacementprice   => $price,
730         rrp_tax_included   => $price,
731         rrp_tax_excluded   => $price,
732         rrp                => $price,
733         ecost              => $ecost,
734         ecost_tax_included => $ecost,
735         ecost_tax_excluded => $ecost,
736         uncertainprice     => 0,
737         sort1              => q{},
738         sort2              => q{},
739         currency           => $vendor->listprice(),
740     };
741
742     # suppliers references
743     if ( $item->reference() ) {
744         $order_hash->{suppliers_reference_number}    = $item->reference;
745         $order_hash->{suppliers_reference_qualifier} = 'QLI';
746     }
747     elsif ( $item->orderline_reference_number() ) {
748         $order_hash->{suppliers_reference_number} =
749           $item->orderline_reference_number;
750         $order_hash->{suppliers_reference_qualifier} = 'SLI';
751     }
752     if ( $item->item_number_id ) {    # suppliers ean
753         $order_hash->{line_item_id} = $item->item_number_id;
754     }
755
756     if ( $item->girfield('servicing_instruction') ) {
757         my $occ = 0;
758         my $txt = q{};
759         my $si;
760         while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
761             if ($occ) {
762                 $txt .= q{: };
763             }
764             $txt .= $si;
765             ++$occ;
766         }
767     }
768     if ($order_note) {
769         $order_hash->{order_vendornote} = $order_note;
770     }
771
772     if ( $item->internal_notes() ) {
773         if ( $order_hash->{order_internalnote} ) {    # more than ''
774             $order_hash->{order_internalnote} .= q{ };
775         }
776         $order_hash->{order_internalnote} .= $item->internal_notes;
777     }
778
779     my $budget = _get_budget( $schema, $item->girfield('fund_allocation') );
780
781     my $skip = '0';
782     if ( !$budget ) {
783         if ( $item->quantity > 1 ) {
784             carp 'Skipping line with no budget info';
785             $logger->trace('girfield skipped for invalid budget');
786             $skip++;
787         }
788         else {
789             carp 'Skipping line with no budget info';
790             $logger->trace('orderline skipped for invalid budget');
791             return;
792         }
793     }
794
795     my %ordernumber;
796     my %budgets;
797     my $item_hash;
798
799     if ( !$skip ) {
800         $order_hash->{budget_id} = $budget->budget_id;
801         my $first_order = $schema->resultset('Aqorder')->create($order_hash);
802         my $o           = $first_order->ordernumber();
803         $logger->trace("Order created :$o");
804
805         # should be done by database settings
806         $first_order->parent_ordernumber( $first_order->ordernumber() );
807         $first_order->update();
808
809         # add to $budgets to prevent duplicate orderlines
810         $budgets{ $budget->budget_id } = '1';
811
812         # record ordernumber against budget
813         $ordernumber{ $budget->budget_id } = $o;
814
815         if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
816             $item_hash = _create_item_from_quote( $item, $quote );
817
818             my $created = 0;
819             while ( $created < $order_quantity ) {
820                 $item_hash->{biblionumber} = $bib->{biblionumber};
821                 $item_hash->{biblioitemnumber} = $bib->{biblioitemnumber};
822                 my $kitem = Koha::Item->new( $item_hash )->store;
823                 my $itemnumber = $kitem->itemnumber;
824                 $logger->trace("Added item:$itemnumber");
825                 $schema->resultset('AqordersItem')->create(
826                     {
827                         ordernumber => $first_order->ordernumber,
828                         itemnumber  => $itemnumber,
829                     }
830                 );
831                 ++$created;
832             }
833         }
834     }
835
836     if ( $order_quantity == 1 && $item->quantity > 1 ) {
837         my $occurrence = 1;    # occ zero already added
838         while ( $occurrence < $item->quantity ) {
839
840             # check budget code
841             $budget = _get_budget( $schema,
842                 $item->girfield( 'fund_allocation', $occurrence ) );
843
844             if ( !$budget ) {
845                 my $bad_budget =
846                   $item->girfield( 'fund_allocation', $occurrence );
847                 carp 'Skipping line with no budget info';
848                 $logger->trace(
849                     "girfield skipped for invalid budget:$bad_budget");
850                 ++$occurrence;    ## lets look at the next one not this one again
851                 next;
852             }
853
854             # add orderline for NEW budget in $budgets
855             if ( !exists $budgets{ $budget->budget_id } ) {
856
857                 # $order_hash->{quantity} = 1; by default above
858                 # we should handle both 1:1 GIR & 1:n GIR (with LQT values) here
859
860                 $order_hash->{budget_id} = $budget->budget_id;
861
862                 my $new_order =
863                   $schema->resultset('Aqorder')->create($order_hash);
864                 my $o = $new_order->ordernumber();
865                 $logger->trace("Order created :$o");
866
867                 # should be done by database settings
868                 $new_order->parent_ordernumber( $new_order->ordernumber() );
869                 $new_order->update();
870
871                 # add to $budgets to prevent duplicate orderlines
872                 $budgets{ $budget->budget_id } = '1';
873
874                 # record ordernumber against budget
875                 $ordernumber{ $budget->budget_id } = $o;
876
877                 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
878                     if ( !defined $item_hash ) {
879                         $item_hash = _create_item_from_quote( $item, $quote );
880                     }
881                     my $new_item = {
882                         itype =>
883                           $item->girfield( 'stock_category', $occurrence ),
884                         itemcallnumber =>
885                           $item->girfield( 'shelfmark', $occurrence )
886                           || $item->girfield( 'classification', $occurrence )
887                           || title_level_class($item),
888                         holdingbranch =>
889                           $item->girfield( 'branch', $occurrence ),
890                         homebranch => $item->girfield( 'branch', $occurrence ),
891                     };
892
893                     my $lsq_field = C4::Context->preference('EdifactLSQ');
894                     $new_item->{$lsq_field} = $item->girfield( 'sequence_code', $occurrence );
895
896                     if ( $new_item->{itype} ) {
897                         $item_hash->{itype} = $new_item->{itype};
898                     }
899                     if ( $new_item->{$lsq_field} ) {
900                         $item_hash->{$lsq_field} = $new_item->{$lsq_field};
901                     }
902                     if ( $new_item->{itemcallnumber} ) {
903                         $item_hash->{itemcallnumber} =
904                           $new_item->{itemcallnumber};
905                     }
906                     if ( $new_item->{holdingbranch} ) {
907                         $item_hash->{holdingbranch} =
908                           $new_item->{holdingbranch};
909                     }
910                     if ( $new_item->{homebranch} ) {
911                         $item_hash->{homebranch} = $new_item->{homebranch};
912                     }
913
914                     $item_hash->{biblionumber} = $bib->{biblionumber};
915                     $item_hash->{biblioitemnumber} = $bib->{biblioitemnumber};
916                     my $kitem = Koha::Item->new( $item_hash )->store;
917                     my $itemnumber = $kitem->itemnumber;
918                     $logger->trace("New item $itemnumber added");
919                     $schema->resultset('AqordersItem')->create(
920                         {
921                             ordernumber => $new_order->ordernumber,
922                             itemnumber  => $itemnumber,
923                         }
924                     );
925
926                     my $lrp =
927                       $item->girfield( 'library_rotation_plan', $occurrence );
928                     if ($lrp) {
929                         my $rota =
930                           Koha::StockRotationRotas->find( { title => $lrp },
931                             { key => 'stockrotationrotas_title' } );
932                         if ($rota) {
933                             $rota->add_item($itemnumber);
934                             $logger->trace("Item added to rota $rota->id");
935                         }
936                         else {
937                             $logger->error(
938                                 "No rota found matching $lrp in orderline");
939                         }
940                     }
941                 }
942
943                 ++$occurrence;
944             }
945
946             # increment quantity in orderline for EXISTING budget in $budgets
947             else {
948                 my $row = $schema->resultset('Aqorder')->find(
949                     {
950                         ordernumber => $ordernumber{ $budget->budget_id }
951                     }
952                 );
953                 if ($row) {
954                     my $qty = $row->quantity;
955                     $qty++;
956                     $row->update(
957                         {
958                             quantity => $qty,
959                         }
960                     );
961                 }
962
963                 # Do not use the basket level value as it is always NULL
964                 # See calling subs call to AddBasket
965                 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
966                     my $new_item = {
967                         notforloan       => -1,
968                         cn_sort          => q{},
969                         cn_source        => 'ddc',
970                         price            => $price,
971                         replacementprice => $price,
972                         itype =>
973                           $item->girfield( 'stock_category', $occurrence ),
974                         itemcallnumber =>
975                           $item->girfield( 'shelfmark', $occurrence )
976                           || $item->girfield( 'classification', $occurrence )
977                           || $item_hash->{itemcallnumber},
978                         holdingbranch =>
979                           $item->girfield( 'branch', $occurrence ),
980                         homebranch => $item->girfield( 'branch', $occurrence ),
981                     };
982                     my $lsq_field = C4::Context->preference('EdifactLSQ');
983                     $new_item->{$lsq_field} = $item->girfield( 'sequence_code', $occurrence );
984                     $new_item->{biblionumber} = $bib->{biblionumber};
985                     $new_item->{biblioitemnumber} = $bib->{biblioitemnumber};
986                     my $kitem = Koha::Item->new( $new_item )->store;
987                     my $itemnumber = $kitem->itemnumber;
988                     $logger->trace("New item $itemnumber added");
989                     $schema->resultset('AqordersItem')->create(
990                         {
991                             ordernumber => $ordernumber{ $budget->budget_id },
992                             itemnumber  => $itemnumber,
993                         }
994                     );
995
996                     my $lrp =
997                       $item->girfield( 'library_rotation_plan', $occurrence );
998                     if ($lrp) {
999                         my $rota =
1000                           Koha::StockRotationRotas->find( { title => $lrp },
1001                             { key => 'stockrotationrotas_title' } );
1002                         if ($rota) {
1003                             $rota->add_item($itemnumber);
1004                             $logger->trace("Item added to rota $rota->id");
1005                         }
1006                         else {
1007                             $logger->error(
1008                                 "No rota found matching $lrp in orderline");
1009                         }
1010                     }
1011                 }
1012
1013                 ++$occurrence;
1014             }
1015         }
1016     }
1017     return 1;
1018
1019 }
1020
1021 sub get_edifact_ean {
1022
1023     my $dbh = C4::Context->dbh;
1024
1025     my $eans = $dbh->selectcol_arrayref('select ean from edifact_ean');
1026
1027     return $eans->[0];
1028 }
1029
1030 # We should not need to have a routine to do this here
1031 sub _discounted_price {
1032     my ( $discount, $price, $discounted_price ) = @_;
1033     if (defined $discounted_price) {
1034         return $discounted_price;
1035     }
1036     if (!$price) {
1037         return 0;
1038     }
1039     return $price - ( ( $discount * $price ) / 100 );
1040 }
1041
1042 sub _check_for_existing_bib {
1043     my $isbn = shift;
1044
1045     my $search_isbn = $isbn;
1046     $search_isbn =~ s/^\s*/%/xms;
1047     $search_isbn =~ s/\s*$/%/xms;
1048     my $dbh = C4::Context->dbh;
1049     my $sth = $dbh->prepare(
1050 'select biblionumber, biblioitemnumber from biblioitems where isbn like ?',
1051     );
1052     my $tuple_arr =
1053       $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
1054     if ( @{$tuple_arr} ) {
1055         return $tuple_arr->[0];
1056     }
1057     elsif ( length($isbn) == 13 && $isbn !~ /^97[89]/ ) {
1058         my $tarr = $dbh->selectall_arrayref(
1059 'select biblionumber, biblioitemnumber from biblioitems where ean = ?',
1060             { Slice => {} },
1061             $isbn
1062         );
1063         if ( @{$tarr} ) {
1064             return $tarr->[0];
1065         }
1066     }
1067     else {
1068         undef $search_isbn;
1069         $isbn =~ s/\-//xmsg;
1070         if ( $isbn =~ m/(\d{13})/xms ) {
1071             my $b_isbn = Business::ISBN->new($1);
1072             if ( $b_isbn && $b_isbn->is_valid && $b_isbn->as_isbn10 ) {
1073                 $search_isbn = $b_isbn->as_isbn10->as_string( [] );
1074             }
1075
1076         }
1077         elsif ( $isbn =~ m/(\d{9}[xX]|\d{10})/xms ) {
1078             my $b_isbn = Business::ISBN->new($1);
1079             if ( $b_isbn && $b_isbn->is_valid ) {
1080                 $search_isbn = $b_isbn->as_isbn13->as_string( [] );
1081             }
1082
1083         }
1084         if ($search_isbn) {
1085             $search_isbn = "%$search_isbn%";
1086             $tuple_arr =
1087               $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
1088             if ( @{$tuple_arr} ) {
1089                 return $tuple_arr->[0];
1090             }
1091         }
1092     }
1093     return;
1094 }
1095
1096 # returns a budget obj or undef
1097 # fact we need this shows what a mess Acq API is
1098 sub _get_budget {
1099     my ( $schema, $budget_code ) = @_;
1100     my $period_rs = $schema->resultset('Aqbudgetperiod')->search(
1101         {
1102             budget_period_active => 1,
1103         }
1104     );
1105
1106     # db does not ensure budget code is unque
1107     return $schema->resultset('Aqbudget')->single(
1108         {
1109             budget_code => $budget_code,
1110             budget_period_id =>
1111               { -in => $period_rs->get_column('budget_period_id')->as_query },
1112         }
1113     );
1114 }
1115
1116 # try to get title level classification from incoming quote
1117 sub title_level_class {
1118     my ($item)         = @_;
1119     my $class          = q{};
1120     my $default_scheme = C4::Context->preference('DefaultClassificationSource');
1121     if ( $default_scheme eq 'ddc' ) {
1122         $class = $item->dewey_class();
1123     }
1124     elsif ( $default_scheme eq 'lcc' ) {
1125         $class = $item->lc_class();
1126     }
1127     if ( !$class ) {
1128         $class =
1129              $item->girfield('shelfmark')
1130           || $item->girfield('classification')
1131           || q{};
1132     }
1133     return $class;
1134 }
1135
1136 sub _create_bib_from_quote {
1137
1138     #TBD we should flag this for updating from an external source
1139     #As biblio (&biblioitems) has no candidates flag in order
1140     my ( $item, $quote ) = @_;
1141     my $itemid = $item->item_number_id;
1142     my $defalt_classification_source =
1143       C4::Context->preference('DefaultClassificationSource');
1144     my $bib_hash = {
1145         'biblioitems.cn_source' => $defalt_classification_source,
1146         'items.cn_source'       => $defalt_classification_source,
1147         'items.notforloan'      => -1,
1148         'items.cn_sort'         => q{},
1149     };
1150     $bib_hash->{'biblio.seriestitle'} = $item->series;
1151
1152     $bib_hash->{'biblioitems.publishercode'} = $item->publisher;
1153     $bib_hash->{'biblioitems.publicationyear'} =
1154       $bib_hash->{'biblio.copyrightdate'} = $item->publication_date;
1155
1156     $bib_hash->{'biblio.title'}         = $item->title;
1157     $bib_hash->{'biblio.author'}        = $item->author;
1158     $bib_hash->{'biblioitems.isbn'}     = $item->item_number_id;
1159     $bib_hash->{'biblioitems.itemtype'} = $item->girfield('stock_category');
1160
1161     # If we have a 13 digit id we are assuming its an ean
1162     # (it may also be an isbn or issn)
1163     if ( $itemid =~ /^\d{13}$/ ) {
1164         $bib_hash->{'biblioitems.ean'} = $itemid;
1165         if ( $itemid =~ /^977/ ) {
1166             $bib_hash->{'biblioitems.issn'} = $itemid;
1167         }
1168     }
1169     for my $key ( keys %{$bib_hash} ) {
1170         if ( !defined $bib_hash->{$key} ) {
1171             delete $bib_hash->{$key};
1172         }
1173     }
1174     return TransformKohaToMarc($bib_hash);
1175
1176 }
1177
1178 sub _create_item_from_quote {
1179     my ( $item, $quote ) = @_;
1180     my $defalt_classification_source =
1181       C4::Context->preference('DefaultClassificationSource');
1182     my $item_hash = {
1183         cn_source  => $defalt_classification_source,
1184         notforloan => -1,
1185         cn_sort    => q{},
1186     };
1187     $item_hash->{booksellerid} = $quote->vendor_id;
1188     $item_hash->{price}        = $item_hash->{replacementprice} = $item->price;
1189     $item_hash->{itype}        = $item->girfield('stock_category');
1190     my $lsq_field = C4::Context->preference('EdifactLSQ');
1191     $item_hash->{$lsq_field}     = $item->girfield('sequence_code');
1192
1193     my $note = {};
1194
1195     $item_hash->{itemcallnumber} =
1196          $item->girfield('shelfmark')
1197       || $item->girfield('classification')
1198       || title_level_class($item);
1199
1200     my $branch = $item->girfield('branch');
1201     $item_hash->{holdingbranch} = $item_hash->{homebranch} = $branch;
1202     return $item_hash;
1203 }
1204
1205 1;
1206 __END__
1207
1208 =head1 NAME
1209
1210 Koha::EDI
1211
1212 =head1 SYNOPSIS
1213
1214    Module exporting subroutines used in EDI processing for Koha
1215
1216 =head1 DESCRIPTION
1217
1218    Subroutines called by batch processing to handle Edifact
1219    messages of various types and related utilities
1220
1221 =head1 BUGS
1222
1223    These routines should really be methods of some object.
1224    get_edifact_ean is a stopgap which should be replaced
1225
1226 =head1 SUBROUTINES
1227
1228 =head2 process_quote
1229
1230     process_quote(quote_message);
1231
1232    passed a message object for a quote, parses it creating an order basket
1233    and orderlines in the database
1234    updates the message's status to received in the database and adds the
1235    link to basket
1236
1237 =head2 process_invoice
1238
1239     process_invoice(invoice_message)
1240
1241     passed a message object for an invoice, add the contained invoices
1242     and update the orderlines referred to in the invoice
1243     As an Edifact invoice is in effect a despatch note this receipts the
1244     appropriate quantities in the orders
1245
1246     no meaningful return value
1247
1248 =head2 process_ordrsp
1249
1250      process_ordrsp(ordrsp_message)
1251
1252      passed a message object for a supplier response, process the contents
1253      If an orderline is cancelled cancel the corresponding orderline in koha
1254      otherwise record the supplier message against it
1255
1256      no meaningful return value
1257
1258 =head2 create_edi_order
1259
1260     create_edi_order( { parameter_hashref } )
1261
1262     parameters must include basketno and ean
1263
1264     branchcode can optionally be passed
1265
1266     returns 1 on success undef otherwise
1267
1268     if the parameter noingest is set the formatted order is returned
1269     and not saved in the database. This functionality is intended for debugging only
1270
1271 =head2 receipt_items
1272
1273     receipt_items( schema_obj, invoice_line, ordernumber, $quantity)
1274
1275     receipts the items recorded on this invoice line
1276
1277     no meaningful return
1278
1279 =head2 transfer_items
1280
1281     transfer_items(schema, invoice_line, originating_order, receiving_order, $quantity)
1282
1283     Transfer the items covered by this invoice line from their original
1284     order to another order recording the partial fulfillment of the original
1285     order
1286
1287     no meaningful return
1288
1289 =head2 get_edifact_ean
1290
1291     $ean = get_edifact_ean();
1292
1293     routine to return the ean.
1294
1295 =head2 quote_item
1296
1297      quote_item(lineitem, quote_message);
1298
1299       Called by process_quote to handle an individual lineitem
1300      Generate the biblios and items if required and orderline linking to them
1301
1302      Returns 1 on success undef on error
1303
1304      Most usual cause of error is a line with no or incorrect budget codes
1305      which woild cause order creation to abort
1306      If other correct lines exist these are processed and the erroneous line os logged
1307
1308 =head2 title_level_class
1309
1310       classmark = title_level_class(edi_item)
1311
1312       Trys to return a title level classmark from a quote message line
1313       Will return a dewey or lcc classmark if one exists according to the
1314       value in DefaultClassificationSource syspref
1315
1316       If unable to returns the shelfmark or classification from the GIR segment
1317
1318       If all else fails returns empty string
1319
1320 =head2 _create_bib_from_quote
1321
1322        marc_record_obj = _create_bib_from_quote(lineitem, quote)
1323
1324        Returns a MARC::Record object based on the  info in the quote's lineitem
1325
1326 =head2 _create_item_from_quote
1327
1328        item_hashref = _create_item_from_quote( lineitem, quote)
1329
1330        returns a hashref representing the item fields specified in the quote
1331
1332 =head2 _get_invoiced_price
1333
1334       (price, price_tax_excluded) = _get_invoiced_price(line_object, $quantity)
1335
1336       Returns an array of unitprice and unitprice_tax_excluded derived from the lineitem
1337       monetary fields
1338
1339 =head2 _discounted_price
1340
1341       ecost = _discounted_price(discount, item_price, discounted_price)
1342
1343       utility subroutine to return a price calculated from the
1344       vendors discount and quoted price
1345       if invoice has a field containing discounted price that is returned
1346       instead of recalculating
1347
1348 =head2 _check_for_existing_bib
1349
1350      (biblionumber, biblioitemnumber) = _check_for_existing_bib(isbn_or_ean)
1351
1352      passed an isbn or ean attempts to locate a match bib
1353      On success returns biblionumber and biblioitemnumber
1354      On failure returns undefined/an empty list
1355
1356 =head2 _get_budget
1357
1358      b = _get_budget(schema_obj, budget_code)
1359
1360      Returns the Aqbudget object for the active budget given the passed budget_code
1361      or undefined if one does not exist
1362
1363 =head1 AUTHOR
1364
1365    Colin Campbell <colin.campbell@ptfs-europe.com>
1366
1367
1368 =head1 COPYRIGHT
1369
1370    Copyright 2014,2015 PTFS-Europe Ltd
1371    This program is free software, You may redistribute it under
1372    under the terms of the GNU General Public License
1373
1374
1375 =cut