Bug 20639: Add ILLOpacbackends syspref
[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 );
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     my $gir_occurrence = 0;
392     while ( $gir_occurrence < $quantity ) {
393         my $branch = $inv_line->girfield( 'branch', $gir_occurrence );
394         my $item = shift @{ $branch_map{$branch} };
395         if ($item) {
396             my $barcode = $inv_line->girfield( 'barcode', $gir_occurrence );
397             if ( $barcode && !$item->barcode ) {
398                 my $rs = $schema->resultset('Item')->search(
399                     {
400                         barcode => $barcode,
401                     }
402                 );
403                 if ( $rs->count > 0 ) {
404                     $logger->warn("Barcode $barcode is a duplicate");
405                 }
406                 else {
407
408                     $logger->trace("Adding barcode $barcode");
409                     $item->barcode($barcode);
410                 }
411             }
412
413             $item->update;
414         }
415         else {
416             $logger->warn("Unmatched item at branch:$branch");
417         }
418         ++$gir_occurrence;
419     }
420     return;
421
422 }
423
424 sub transfer_items {
425     my ( $schema, $inv_line, $order_from, $order_to ) = @_;
426
427     # Transfer x items from the orig order to a completed partial order
428     my $quantity = $inv_line->quantity;
429     my $gocc     = 0;
430     my %mapped_by_branch;
431     while ( $gocc < $quantity ) {
432         my $branch = $inv_line->girfield( 'branch', $gocc );
433         if ( !exists $mapped_by_branch{$branch} ) {
434             $mapped_by_branch{$branch} = 1;
435         }
436         else {
437             $mapped_by_branch{$branch}++;
438         }
439         ++$gocc;
440     }
441     my $logger = Log::Log4perl->get_logger();
442     my $o1     = $order_from->ordernumber;
443     my $o2     = $order_to->ordernumber;
444     $logger->warn("transferring $quantity copies from order $o1 to order $o2");
445
446     my @item_links = $schema->resultset('AqordersItem')->search(
447         {
448             ordernumber => $order_from->ordernumber,
449         }
450     );
451     foreach my $ilink (@item_links) {
452         my $ino      = $ilink->itemnumber;
453         my $item     = $schema->resultset('Item')->find( $ilink->itemnumber );
454         my $i_branch = $item->homebranch;
455         if ( exists $mapped_by_branch{$i_branch}
456             && $mapped_by_branch{$i_branch} > 0 )
457         {
458             $ilink->ordernumber( $order_to->ordernumber );
459             $ilink->update;
460             --$quantity;
461             --$mapped_by_branch{$i_branch};
462             $logger->warn("Transferred item $item");
463         }
464         else {
465             $logger->warn("Skipped item $item");
466         }
467         if ( $quantity < 1 ) {
468             last;
469         }
470     }
471
472     return;
473 }
474
475 sub process_quote {
476     my $quote = shift;
477
478     $quote->status('processing');
479     $quote->update;
480
481     my $edi = Koha::Edifact->new( { transmission => $quote->raw_msg, } );
482
483     my $messages       = $edi->message_array();
484     my $process_errors = 0;
485     my $logger         = Log::Log4perl->get_logger();
486     my $schema         = Koha::Database->new()->schema();
487     my $message_count  = 0;
488     my @added_baskets;    # if auto & multiple baskets need to order all
489
490     if ( @{$messages} && $quote->vendor_id ) {
491         foreach my $msg ( @{$messages} ) {
492             ++$message_count;
493             my $basketno =
494               NewBasket( $quote->vendor_id, 0, $quote->filename, q{},
495                 q{} . q{} );
496             push @added_baskets, $basketno;
497             if ( $message_count > 1 ) {
498                 my $m_filename = $quote->filename;
499                 $m_filename .= "_$message_count";
500                 $schema->resultset('EdifactMessage')->create(
501                     {
502                         message_type  => $quote->message_type,
503                         transfer_date => $quote->transfer_date,
504                         vendor_id     => $quote->vendor_id,
505                         edi_acct      => $quote->edi_acct,
506                         status        => 'recmsg',
507                         basketno      => $basketno,
508                         raw_msg       => q{},
509                         filename      => $m_filename,
510                     }
511                 );
512             }
513             else {
514                 $quote->basketno($basketno);
515             }
516             $logger->trace("Created basket :$basketno");
517             my $items  = $msg->lineitems();
518             my $refnum = $msg->message_refno;
519
520             for my $item ( @{$items} ) {
521                 if ( !quote_item( $item, $quote, $basketno ) ) {
522                     ++$process_errors;
523                 }
524             }
525         }
526     }
527     my $status = 'received';
528     if ($process_errors) {
529         $status = 'error';
530     }
531
532     $quote->status($status);
533     $quote->update;    # status and basketno link
534                        # Do we automatically generate orders for this vendor
535     my $v = $schema->resultset('VendorEdiAccount')->search(
536         {
537             vendor_id => $quote->vendor_id,
538         }
539     )->single;
540     if ( $v->auto_orders ) {
541         for my $b (@added_baskets) {
542             create_edi_order(
543                 {
544
545                     basketno => $b,
546                 }
547             );
548             CloseBasket($b);
549         }
550     }
551
552     return;
553 }
554
555 sub quote_item {
556     my ( $item, $quote, $basketno ) = @_;
557
558     my $schema = Koha::Database->new()->schema();
559     my $logger = Log::Log4perl->get_logger();
560
561     my $basket = Koha::Acquisition::Baskets->find( $basketno );
562     unless ( $basket ) {
563         $logger->error('Skipping order creation no valid basketno');
564         return;
565     }
566     $logger->trace( 'Checking db for matches with ', $item->item_number_id() );
567     my $bib = _check_for_existing_bib( $item->item_number_id() );
568     if ( !defined $bib ) {
569         $bib = {};
570         my $bib_record = _create_bib_from_quote( $item, $quote );
571         ( $bib->{biblionumber}, $bib->{biblioitemnumber} ) =
572           AddBiblio( $bib_record, q{} );
573         $logger->trace("New biblio added $bib->{biblionumber}");
574     }
575     else {
576         $logger->trace("Match found: $bib->{biblionumber}");
577     }
578
579     # Create an orderline
580     my $order_note = $item->{orderline_free_text};
581     $order_note ||= q{};
582     my $order_quantity = $item->quantity();
583     my $gir_count      = $item->number_of_girs();
584     $order_quantity ||= 1;    # quantity not necessarily present
585     if ( $gir_count > 1 ) {
586         if ( $gir_count != $order_quantity ) {
587             $logger->error(
588                 "Order for $order_quantity items, $gir_count segments present");
589         }
590         $order_quantity = 1;    # attempts to create an orderline for each gir
591     }
592     my $vendor = Koha::Acquisition::Booksellers->find( $quote->vendor_id );
593
594     # database definitions should set some of these defaults but dont
595     my $order_hash = {
596         biblionumber       => $bib->{biblionumber},
597         entrydate          => DateTime->now( time_zone => 'local' )->ymd(),
598         basketno           => $basketno,
599         listprice          => $item->price,
600         quantity           => $order_quantity,
601         quantityreceived   => 0,
602         order_vendornote   => q{},
603         order_internalnote => $order_note,
604         replacementprice   => $item->price,
605         rrp_tax_included   => $item->price,
606         rrp_tax_excluded   => $item->price,
607         ecost => _discounted_price( $quote->vendor->discount, $item->price ),
608         uncertainprice => 0,
609         sort1          => q{},
610         sort2          => q{},
611         currency       => $vendor->listprice(),
612     };
613
614     # suppliers references
615     if ( $item->reference() ) {
616         $order_hash->{suppliers_reference_number}    = $item->reference;
617         $order_hash->{suppliers_reference_qualifier} = 'QLI';
618     }
619     elsif ( $item->orderline_reference_number() ) {
620         $order_hash->{suppliers_reference_number} =
621           $item->orderline_reference_number;
622         $order_hash->{suppliers_reference_qualifier} = 'SLI';
623     }
624     if ( $item->item_number_id ) {    # suppliers ean
625         $order_hash->{line_item_id} = $item->item_number_id;
626     }
627
628     if ( $item->girfield('servicing_instruction') ) {
629         my $occ = 0;
630         my $txt = q{};
631         my $si;
632         while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
633             if ($occ) {
634                 $txt .= q{: };
635             }
636             $txt .= $si;
637             ++$occ;
638         }
639         $order_hash->{order_vendornote} = $txt;
640     }
641
642     if ( $item->internal_notes() ) {
643         if ( $order_hash->{order_internalnote} ) {    # more than ''
644             $order_hash->{order_internalnote} .= q{ };
645         }
646         $order_hash->{order_internalnote} .= $item->internal_notes;
647     }
648
649     my $budget = _get_budget( $schema, $item->girfield('fund_allocation') );
650
651     my $skip = '0';
652     if ( !$budget ) {
653         if ( $item->quantity > 1 ) {
654             carp 'Skipping line with no budget info';
655             $logger->trace('girfield skipped for invalid budget');
656             $skip++;
657         }
658         else {
659             carp 'Skipping line with no budget info';
660             $logger->trace('orderline skipped for invalid budget');
661             return;
662         }
663     }
664
665     my %ordernumber;
666     my %budgets;
667     my $item_hash;
668
669     if ( !$skip ) {
670         $order_hash->{budget_id} = $budget->budget_id;
671         my $first_order = $schema->resultset('Aqorder')->create($order_hash);
672         my $o           = $first_order->ordernumber();
673         $logger->trace("Order created :$o");
674
675         # should be done by database settings
676         $first_order->parent_ordernumber( $first_order->ordernumber() );
677         $first_order->update();
678
679         # add to $budgets to prevent duplicate orderlines
680         $budgets{ $budget->budget_id } = '1';
681
682         # record ordernumber against budget
683         $ordernumber{ $budget->budget_id } = $o;
684
685         if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
686             $item_hash = _create_item_from_quote( $item, $quote );
687
688             my $created = 0;
689             while ( $created < $order_quantity ) {
690                 my $itemnumber;
691                 ( $bib->{biblionumber}, $bib->{biblioitemnumber}, $itemnumber )
692                   = AddItem( $item_hash, $bib->{biblionumber} );
693                 $logger->trace("Added item:$itemnumber");
694                 $schema->resultset('AqordersItem')->create(
695                     {
696                         ordernumber => $first_order->ordernumber,
697                         itemnumber  => $itemnumber,
698                     }
699                 );
700                 ++$created;
701             }
702         }
703     }
704
705     if ( $order_quantity == 1 && $item->quantity > 1 ) {
706         my $occurrence = 1;    # occ zero already added
707         while ( $occurrence < $item->quantity ) {
708
709             # check budget code
710             $budget = _get_budget( $schema,
711                 $item->girfield( 'fund_allocation', $occurrence ) );
712
713             if ( !$budget ) {
714                 my $bad_budget =
715                   $item->girfield( 'fund_allocation', $occurrence );
716                 carp 'Skipping line with no budget info';
717                 $logger->trace(
718                     "girfield skipped for invalid budget:$bad_budget");
719                 ++$occurrence;    ## lets look at the next one not this one again
720                 next;
721             }
722
723             # add orderline for NEW budget in $budgets
724             if ( !exists $budgets{ $budget->budget_id } ) {
725
726                 # $order_hash->{quantity} = 1; by default above
727                 # we should handle both 1:1 GIR & 1:n GIR (with LQT values) here
728
729                 $order_hash->{budget_id} = $budget->budget_id;
730
731                 my $new_order =
732                   $schema->resultset('Aqorder')->create($order_hash);
733                 my $o = $new_order->ordernumber();
734                 $logger->trace("Order created :$o");
735
736                 # should be done by database settings
737                 $new_order->parent_ordernumber( $new_order->ordernumber() );
738                 $new_order->update();
739
740                 # add to $budgets to prevent duplicate orderlines
741                 $budgets{ $budget->budget_id } = '1';
742
743                 # record ordernumber against budget
744                 $ordernumber{ $budget->budget_id } = $o;
745
746                 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
747                     if ( !defined $item_hash ) {
748                         $item_hash = _create_item_from_quote( $item, $quote );
749                     }
750                     my $new_item = {
751                         itype =>
752                           $item->girfield( 'stock_category', $occurrence ),
753                         location =>
754                           $item->girfield( 'collection_code', $occurrence ),
755                         itemcallnumber =>
756                           $item->girfield( 'shelfmark', $occurrence )
757                           || $item->girfield( 'classification', $occurrence )
758                           || title_level_class($item),
759                         holdingbranch =>
760                           $item->girfield( 'branch', $occurrence ),
761                         homebranch => $item->girfield( 'branch', $occurrence ),
762                     };
763                     if ( $new_item->{itype} ) {
764                         $item_hash->{itype} = $new_item->{itype};
765                     }
766                     if ( $new_item->{location} ) {
767                         $item_hash->{location} = $new_item->{location};
768                     }
769                     if ( $new_item->{itemcallnumber} ) {
770                         $item_hash->{itemcallnumber} =
771                           $new_item->{itemcallnumber};
772                     }
773                     if ( $new_item->{holdingbranch} ) {
774                         $item_hash->{holdingbranch} =
775                           $new_item->{holdingbranch};
776                     }
777                     if ( $new_item->{homebranch} ) {
778                         $item_hash->{homebranch} = $new_item->{homebranch};
779                     }
780
781                     my $itemnumber;
782                     ( undef, undef, $itemnumber ) =
783                       AddItem( $item_hash, $bib->{biblionumber} );
784                     $logger->trace("New item $itemnumber added");
785                     $schema->resultset('AqordersItem')->create(
786                         {
787                             ordernumber => $new_order->ordernumber,
788                             itemnumber  => $itemnumber,
789                         }
790                     );
791                 }
792
793                 ++$occurrence;
794             }
795
796             # increment quantity in orderline for EXISTING budget in $budgets
797             else {
798                 my $row = $schema->resultset('Aqorder')->find(
799                     {
800                         ordernumber => $ordernumber{ $budget->budget_id }
801                     }
802                 );
803                 if ($row) {
804                     my $qty = $row->quantity;
805                     $qty++;
806                     $row->update(
807                         {
808                             quantity => $qty,
809                         }
810                     );
811                 }
812
813                 if ( $basket->effective_create_item eq 'ordering' ) {
814                     my $new_item = {
815                         notforloan       => -1,
816                         cn_sort          => q{},
817                         cn_source        => 'ddc',
818                         price            => $item->price,
819                         replacementprice => $item->price,
820                         itype =>
821                           $item->girfield( 'stock_category', $occurrence ),
822                         location =>
823                           $item->girfield( 'collection_code', $occurrence ),
824                         itemcallnumber =>
825                           $item->girfield( 'shelfmark', $occurrence )
826                           || $item->girfield( 'classification', $occurrence )
827                           || $item_hash->{itemcallnumber},
828                         holdingbranch =>
829                           $item->girfield( 'branch', $occurrence ),
830                         homebranch => $item->girfield( 'branch', $occurrence ),
831                     };
832                     my $itemnumber;
833                     ( undef, undef, $itemnumber ) =
834                       AddItem( $new_item, $bib->{biblionumber} );
835                     $logger->trace("New item $itemnumber added");
836                     $schema->resultset('AqordersItem')->create(
837                         {
838                             ordernumber => $ordernumber{ $budget->budget_id },
839                             itemnumber  => $itemnumber,
840                         }
841                     );
842                 }
843
844                 ++$occurrence;
845             }
846         }
847     }
848     return 1;
849
850 }
851
852 sub get_edifact_ean {
853
854     my $dbh = C4::Context->dbh;
855
856     my $eans = $dbh->selectcol_arrayref('select ean from edifact_ean');
857
858     return $eans->[0];
859 }
860
861 # We should not need to have a routine to do this here
862 sub _discounted_price {
863     my ( $discount, $price ) = @_;
864     return $price - ( ( $discount * $price ) / 100 );
865 }
866
867 sub _check_for_existing_bib {
868     my $isbn = shift;
869
870     my $search_isbn = $isbn;
871     $search_isbn =~ s/^\s*/%/xms;
872     $search_isbn =~ s/\s*$/%/xms;
873     my $dbh = C4::Context->dbh;
874     my $sth = $dbh->prepare(
875 'select biblionumber, biblioitemnumber from biblioitems where isbn like ?',
876     );
877     my $tuple_arr =
878       $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
879     if ( @{$tuple_arr} ) {
880         return $tuple_arr->[0];
881     }
882     elsif ( length($isbn) == 13 && $isbn !~ /^97[89]/ ) {
883         my $tarr = $dbh->selectall_arrayref(
884 'select biblionumber, biblioitemnumber from biblioitems where ean = ?',
885             { Slice => {} },
886             $isbn
887         );
888         if ( @{$tarr} ) {
889             return $tarr->[0];
890         }
891     }
892     else {
893         undef $search_isbn;
894         $isbn =~ s/\-//xmsg;
895         if ( $isbn =~ m/(\d{13})/xms ) {
896             my $b_isbn = Business::ISBN->new($1);
897             if ( $b_isbn && $b_isbn->is_valid ) {
898                 $search_isbn = $b_isbn->as_isbn10->as_string( [] );
899             }
900
901         }
902         elsif ( $isbn =~ m/(\d{9}[xX]|\d{10})/xms ) {
903             my $b_isbn = Business::ISBN->new($1);
904             if ( $b_isbn && $b_isbn->is_valid ) {
905                 $search_isbn = $b_isbn->as_isbn13->as_string( [] );
906             }
907
908         }
909         if ($search_isbn) {
910             $search_isbn = "%$search_isbn%";
911             $tuple_arr =
912               $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
913             if ( @{$tuple_arr} ) {
914                 return $tuple_arr->[0];
915             }
916         }
917     }
918     return;
919 }
920
921 # returns a budget obj or undef
922 # fact we need this shows what a mess Acq API is
923 sub _get_budget {
924     my ( $schema, $budget_code ) = @_;
925     my $period_rs = $schema->resultset('Aqbudgetperiod')->search(
926         {
927             budget_period_active => 1,
928         }
929     );
930
931     # db does not ensure budget code is unque
932     return $schema->resultset('Aqbudget')->single(
933         {
934             budget_code => $budget_code,
935             budget_period_id =>
936               { -in => $period_rs->get_column('budget_period_id')->as_query },
937         }
938     );
939 }
940
941 # try to get title level classification from incoming quote
942 sub title_level_class {
943     my ($item)         = @_;
944     my $class          = q{};
945     my $default_scheme = C4::Context->preference('DefaultClassificationSource');
946     if ( $default_scheme eq 'ddc' ) {
947         $class = $item->dewey_class();
948     }
949     elsif ( $default_scheme eq 'lcc' ) {
950         $class = $item->lc_class();
951     }
952     if ( !$class ) {
953         $class =
954              $item->girfield('shelfmark')
955           || $item->girfield('classification')
956           || q{};
957     }
958     return $class;
959 }
960
961 sub _create_bib_from_quote {
962
963     #TBD we should flag this for updating from an external source
964     #As biblio (&biblioitems) has no candidates flag in order
965     my ( $item, $quote ) = @_;
966     my $itemid = $item->item_number_id;
967     my $defalt_classification_source =
968       C4::Context->preference('DefaultClassificationSource');
969     my $bib_hash = {
970         'biblioitems.cn_source' => $defalt_classification_source,
971         'items.cn_source'       => $defalt_classification_source,
972         'items.notforloan'      => -1,
973         'items.cn_sort'         => q{},
974     };
975     $bib_hash->{'biblio.seriestitle'} = $item->series;
976
977     $bib_hash->{'biblioitems.publishercode'} = $item->publisher;
978     $bib_hash->{'biblioitems.publicationyear'} =
979       $bib_hash->{'biblio.copyrightdate'} = $item->publication_date;
980
981     $bib_hash->{'biblio.title'}         = $item->title;
982     $bib_hash->{'biblio.author'}        = $item->author;
983     $bib_hash->{'biblioitems.isbn'}     = $item->item_number_id;
984     $bib_hash->{'biblioitems.itemtype'} = $item->girfield('stock_category');
985
986     # If we have a 13 digit id we are assuming its an ean
987     # (it may also be an isbn or issn)
988     if ( $itemid =~ /^\d{13}$/ ) {
989         $bib_hash->{'biblioitems.ean'} = $itemid;
990         if ( $itemid =~ /^977/ ) {
991             $bib_hash->{'biblioitems.issn'} = $itemid;
992         }
993     }
994     for my $key ( keys %{$bib_hash} ) {
995         if ( !defined $bib_hash->{$key} ) {
996             delete $bib_hash->{$key};
997         }
998     }
999     return TransformKohaToMarc($bib_hash);
1000
1001 }
1002
1003 sub _create_item_from_quote {
1004     my ( $item, $quote ) = @_;
1005     my $defalt_classification_source =
1006       C4::Context->preference('DefaultClassificationSource');
1007     my $item_hash = {
1008         cn_source  => $defalt_classification_source,
1009         notforloan => -1,
1010         cn_sort    => q{},
1011     };
1012     $item_hash->{booksellerid} = $quote->vendor_id;
1013     $item_hash->{price}        = $item_hash->{replacementprice} = $item->price;
1014     $item_hash->{itype}        = $item->girfield('stock_category');
1015     $item_hash->{location}     = $item->girfield('collection_code');
1016
1017     my $note = {};
1018
1019     $item_hash->{itemcallnumber} =
1020          $item->girfield('shelfmark')
1021       || $item->girfield('classification')
1022       || title_level_class($item);
1023
1024     my $branch = $item->girfield('branch');
1025     $item_hash->{holdingbranch} = $item_hash->{homebranch} = $branch;
1026     return $item_hash;
1027 }
1028
1029 1;
1030 __END__
1031
1032 =head1 NAME
1033
1034 Koha::EDI
1035
1036 =head1 SYNOPSIS
1037
1038    Module exporting subroutines used in EDI processing for Koha
1039
1040 =head1 DESCRIPTION
1041
1042    Subroutines called by batch processing to handle Edifact
1043    messages of various types and related utilities
1044
1045 =head1 BUGS
1046
1047    These routines should really be methods of some object.
1048    get_edifact_ean is a stopgap which should be replaced
1049
1050 =head1 SUBROUTINES
1051
1052 =head2 process_quote
1053
1054     process_quote(quote_message);
1055
1056    passed a message object for a quote, parses it creating an order basket
1057    and orderlines in the database
1058    updates the message's status to received in the database and adds the
1059    link to basket
1060
1061 =head2 process_invoice
1062
1063     process_invoice(invoice_message)
1064
1065     passed a message object for an invoice, add the contained invoices
1066     and update the orderlines referred to in the invoice
1067     As an Edifact invoice is in effect a despatch note this receipts the
1068     appropriate quantities in the orders
1069
1070     no meaningful return value
1071
1072 =head2 process_ordrsp
1073
1074      process_ordrsp(ordrsp_message)
1075
1076      passed a message object for a supplier response, process the contents
1077      If an orderline is cancelled cancel the corresponding orderline in koha
1078      otherwise record the supplier message against it
1079
1080      no meaningful return value
1081
1082 =head2 create_edi_order
1083
1084     create_edi_order( { parameter_hashref } )
1085
1086     parameters must include basketno and ean
1087
1088     branchcode can optionally be passed
1089
1090     returns 1 on success undef otherwise
1091
1092     if the parameter noingest is set the formatted order is returned
1093     and not saved in the database. This functionality is intended for debugging only
1094
1095 =head2 receipt_items
1096
1097     receipt_items( schema_obj, invoice_line, ordernumber)
1098
1099     receipts the items recorded on this invoice line
1100
1101     no meaningful return
1102
1103 =head2 transfer_items
1104
1105     transfer_items(schema, invoice_line, originating_order, receiving_order)
1106
1107     Transfer the items covered by this invoice line from their original
1108     order to another order recording the partial fulfillment of the original
1109     order
1110
1111     no meaningful return
1112
1113 =head2 get_edifact_ean
1114
1115     $ean = get_edifact_ean();
1116
1117     routine to return the ean.
1118
1119 =head2 quote_item
1120
1121      quote_item(lineitem, quote_message);
1122
1123       Called by process_quote to handle an individual lineitem
1124      Generate the biblios and items if required and orderline linking to them
1125
1126      Returns 1 on success undef on error
1127
1128      Most usual cause of error is a line with no or incorrect budget codes
1129      which woild cause order creation to abort
1130      If other correct lines exist these are processed and the erroneous line os logged
1131
1132 =head2 title_level_class
1133
1134       classmark = title_level_class(edi_item)
1135
1136       Trys to return a title level classmark from a quote message line
1137       Will return a dewey or lcc classmark if one exists according to the
1138       value in DefaultClassificationSource syspref
1139
1140       If unable to returns the shelfmark or classification from the GIR segment
1141
1142       If all else fails returns empty string
1143
1144 =head2 _create_bib_from_quote
1145
1146        marc_record_obj = _create_bib_from_quote(lineitem, quote)
1147
1148        Returns a MARC::Record object based on the  info in the quote's lineitem
1149
1150 =head2 _create_item_from_quote
1151
1152        item_hashref = _create_item_from_quote( lineitem, quote)
1153
1154        returns a hashref representing the item fields specified in the quote
1155
1156 =head2 _get_invoiced_price
1157
1158       _get_invoiced_price(line_object)
1159
1160       Returns the net price or an equivalent calculated from line cost / qty
1161
1162 =head2 _discounted_price
1163
1164       ecost = _discounted_price(discount, item_price)
1165
1166       utility subroutine to return a price calculated from the
1167       vendors discount and quoted price
1168
1169 =head2 _check_for_existing_bib
1170
1171      (biblionumber, biblioitemnumber) = _check_for_existing_bib(isbn_or_ean)
1172
1173      passed an isbn or ean attempts to locate a match bib
1174      On success returns biblionumber and biblioitemnumber
1175      On failure returns undefined/an empty list
1176
1177 =head2 _get_budget
1178
1179      b = _get_budget(schema_obj, budget_code)
1180
1181      Returns the Aqbudget object for the active budget given the passed budget_code
1182      or undefined if one does not exist
1183
1184 =head1 AUTHOR
1185
1186    Colin Campbell <colin.campbell@ptfs-europe.com>
1187
1188
1189 =head1 COPYRIGHT
1190
1191    Copyright 2014,2015 PTFS-Europe Ltd
1192    This program is free software, You may redistribute it under
1193    under the terms of the GNU General Public License
1194
1195
1196 =cut