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