Bug 18267: Refactored Edifact Price Calculations
[koha.git] / Koha / EDI.pm
blob2de2b8bf087b28b03e5b06ea914024ad305780db
1 package Koha::EDI;
3 # Copyright 2014,2015 PTFS-Europe Ltd
5 # This file is part of Koha.
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.
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.
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>.
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 Koha::DateUtils;
31 use C4::Acquisition qw( NewBasket ModOrder);
32 use C4::Suggestions qw( ModSuggestion );
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;
42 our $VERSION = 1.1;
43 our @EXPORT_OK =
44 qw( process_quote process_invoice process_ordrsp create_edi_order get_edifact_ean );
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;
57 my $schema = Koha::Database->new()->schema();
59 my @orderlines = $schema->resultset('Aqorder')->search(
61 basketno => $basketno,
62 orderstatus => 'new',
64 )->all;
66 if ( !@orderlines ) {
67 carp "No orderlines for basket $basketno";
68 return;
71 my $vendor = $schema->resultset('VendorEdiAccount')->search(
73 vendor_id => $orderlines[0]->basketno->booksellerid->id,
75 )->single;
77 my $ean_search_keys = { ean => $ean, };
78 if ($branchcode) {
79 $ean_search_keys->{branchcode} = $branchcode;
81 my $ean_obj =
82 $schema->resultset('EdifactEan')->search($ean_search_keys)->single;
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(
88 ean => $ean,
89 branchcode => undef,
91 )->single;
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
99 my $response = @{$arr_ref} ? 1 : 0;
101 my $edifact_order_params = {
102 orderlines => \@orderlines,
103 vendor => $vendor,
104 ean => $ean_obj,
105 is_response => $response,
108 my $edifact;
109 if ( $vendor->plugin ) {
110 $edifact = Koha::Plugins::Handler->run(
112 class => $vendor->plugin,
113 method => 'edifact_order',
114 params => {
115 params => $edifact_order_params,
120 else {
121 $edifact = Koha::Edifact::Order->new($edifact_order_params);
124 return unless $edifact;
126 my $order_file = $edifact->encode();
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;
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,
145 $schema->resultset('EdifactMessage')->create($order);
146 return 1;
149 return;
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();
163 if ( @{$messages} ) {
164 foreach my $msg ( @{$messages} ) {
165 my $lines = $msg->lineitems();
166 foreach my $line ( @{$lines} ) {
167 my $ordernumber = $line->ordernumber();
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(
175 ordernumber => $ordernumber,
176 cancellationreason => $reason,
177 orderstatus => 'cancelled',
178 datecancellationprinted => dt_from_string()->ymd(),
182 else { # record order as due with possible further info
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";
190 ModOrder(
192 ordernumber => $ordernumber,
193 suppliers_report => $report,
201 $response_message->status('received');
202 $response_message->update;
203 return;
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;
214 my $plugin_class = $invoice_message->edi_acct()->plugin();
216 # Plugin has its own invoice processor, only run it and not the standard invoice processor below
217 if ( $plugin_class ) {
218 my $plugin = $plugin_class->new();
219 if ( $plugin->can('edifact_process_invoice') ) {
220 Koha::Plugins::Handler->run(
222 class => $plugin_class,
223 method => 'edifact_process_invoice',
224 params => {
225 invoice => $invoice_message,
229 return;
233 my $edi_plugin;
234 if ( $plugin_class ) {
235 $edi_plugin = Koha::Plugins::Handler->run(
237 class => $plugin_class,
238 method => 'edifact',
239 params => {
240 invoice_message => $invoice_message,
241 transmission => $invoice_message->raw_msg,
247 my $edi = $edi_plugin ||
248 Koha::Edifact->new( { transmission => $invoice_message->raw_msg, } );
250 my $messages = $edi->message_array();
252 if ( @{$messages} ) {
254 # BGM contains an invoice number
255 foreach my $msg ( @{$messages} ) {
256 my $invoicenumber = $msg->docmsg_number();
257 my $shipmentcharge = $msg->shipment_charge();
258 my $msg_date = $msg->message_date;
259 my $tax_date = $msg->tax_point_date;
260 if ( !defined $tax_date || $tax_date !~ m/^\d{8}/xms ) {
261 $tax_date = $msg_date;
264 my $vendor_ean = $msg->supplier_ean;
265 if ( !defined $vendor_acct || $vendor_ean ne $vendor_acct->san ) {
266 $vendor_acct = $schema->resultset('VendorEdiAccount')->search(
268 san => $vendor_ean,
270 )->single;
272 if ( !$vendor_acct ) {
273 carp
274 "Cannot find vendor with ean $vendor_ean for invoice $invoicenumber in $invoice_message->filename";
275 next;
277 $invoice_message->edi_acct( $vendor_acct->id );
278 $logger->trace("Adding invoice:$invoicenumber");
279 my $new_invoice = $schema->resultset('Aqinvoice')->create(
281 invoicenumber => $invoicenumber,
282 booksellerid => $invoice_message->vendor_id,
283 shipmentdate => $msg_date,
284 billingdate => $tax_date,
285 shipmentcost => $shipmentcharge,
286 shipmentcost_budgetid => $vendor_acct->shipment_budget,
287 message_id => $invoice_message->id,
290 my $invoiceid = $new_invoice->invoiceid;
291 $logger->trace("Added as invoiceno :$invoiceid");
292 my $lines = $msg->lineitems();
294 foreach my $line ( @{$lines} ) {
295 my $ordernumber = $line->ordernumber;
296 $logger->trace( "Receipting order:$ordernumber Qty: ",
297 $line->quantity );
299 my $order = $schema->resultset('Aqorder')->find($ordernumber);
301 # ModReceiveOrder does not validate that $ordernumber exists validate here
302 if ($order) {
304 # check suggestions
305 my $s = $schema->resultset('Suggestion')->search(
307 biblionumber => $order->biblionumber->biblionumber,
309 )->single;
310 if ($s) {
311 ModSuggestion(
313 suggestionid => $s->suggestionid,
314 STATUS => 'AVAILABLE',
318 # If quantity_invoiced is present use it in preference
319 my $quantity = $line->quantity_invoiced;
320 if (!$quantity) {
321 $quantity = $line->quantity;
324 my ( $price, $price_excl_tax ) = _get_invoiced_price($line, $quantity);
325 my $tax_rate = $line->tax_rate;
326 if ($tax_rate && $tax_rate->{rate} != 0) {
327 $tax_rate->{rate} /= 100;
330 if ( $order->quantity > $quantity ) {
331 my $ordered = $order->quantity;
333 # part receipt
334 $order->orderstatus('partial');
335 $order->quantity( $ordered - $quantity );
336 $order->update;
337 my $received_order = $order->copy(
339 ordernumber => undef,
340 quantity => $quantity,
341 quantityreceived => $quantity,
342 orderstatus => 'complete',
343 unitprice => $price,
344 unitprice_tax_included => $price,
345 unitprice_tax_excluded => $price_excl_tax,
346 invoiceid => $invoiceid,
347 datereceived => $msg_date,
348 tax_rate_on_receiving => $tax_rate->{rate},
349 tax_value_on_receiving => $quantity * $price_excl_tax * $tax_rate->{rate},
352 transfer_items( $schema, $line, $order,
353 $received_order, $quantity );
354 receipt_items( $schema, $line,
355 $received_order->ordernumber, $quantity );
357 else { # simple receipt all copies on order
358 $order->quantityreceived( $quantity );
359 $order->datereceived($msg_date);
360 $order->invoiceid($invoiceid);
361 $order->unitprice($price);
362 $order->unitprice_tax_excluded($price_excl_tax);
363 $order->unitprice_tax_included($price);
364 $order->tax_rate_on_receiving($tax_rate->{rate});
365 $order->tax_value_on_receiving( $quantity * $price_excl_tax * $tax_rate->{rate});
366 $order->orderstatus('complete');
367 $order->update;
368 receipt_items( $schema, $line, $ordernumber, $quantity );
371 else {
372 $logger->error(
373 "No order found for $ordernumber Invoice:$invoicenumber"
375 next;
383 $invoice_message->status('received');
384 $invoice_message->update; # status and basketno link
385 return;
388 sub _get_invoiced_price {
389 my $line = shift;
390 my $qty = shift;
391 my $line_total = $line->amt_total;
392 my $excl_tax = $line->amt_lineitem;
394 # If no tax some suppliers omit the total owed
395 # If no total given calculate from cost exclusive of tax
396 # + tax amount (if present, sometimes omitted if 0 )
397 if ( !defined $line_total ) {
398 my $x = $line->amt_taxoncharge;
399 if ( !defined $x ) {
400 $x = 0;
402 $line_total = $excl_tax + $x;
405 # invoices give amounts per orderline, Koha requires that we store
406 # them per item
407 if ( $qty != 1 ) {
408 return ( $line_total / $qty, $excl_tax / $qty );
410 return ( $line_total, $excl_tax ); # return as is for most common case
413 sub receipt_items {
414 my ( $schema, $inv_line, $ordernumber, $quantity ) = @_;
415 my $logger = Log::Log4perl->get_logger();
417 # itemnumber is not a foreign key ??? makes this a bit cumbersome
418 my @item_links = $schema->resultset('AqordersItem')->search(
420 ordernumber => $ordernumber,
423 my %branch_map;
424 foreach my $ilink (@item_links) {
425 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
426 if ( !$item ) {
427 my $i = $ilink->itemnumber;
428 $logger->warn(
429 "Cannot find aqorder item for $i :Order:$ordernumber");
430 next;
432 my $b = $item->homebranch->branchcode;
433 if ( !exists $branch_map{$b} ) {
434 $branch_map{$b} = [];
436 push @{ $branch_map{$b} }, $item;
439 # Handling for 'AcqItemSetSubfieldsWhenReceived'
440 my @affects;
441 my $biblionumber;
442 my $itemfield;
443 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
444 @affects = split q{\|},
445 C4::Context->preference("AcqItemSetSubfieldsWhenReceived");
446 if (@affects) {
447 my $order = Koha::Acquisition::Orders->find($ordernumber);
448 $biblionumber = $order->biblionumber;
449 my $frameworkcode = GetFrameworkCode($biblionumber);
450 ($itemfield) = GetMarcFromKohaField( 'items.itemnumber',
451 $frameworkcode );
455 my $gir_occurrence = 0;
456 while ( $gir_occurrence < $quantity ) {
457 my $branch = $inv_line->girfield( 'branch', $gir_occurrence );
458 my $item = shift @{ $branch_map{$branch} };
459 if ($item) {
460 my $barcode = $inv_line->girfield( 'barcode', $gir_occurrence );
461 if ( $barcode && !$item->barcode ) {
462 my $rs = $schema->resultset('Item')->search(
464 barcode => $barcode,
467 if ( $rs->count > 0 ) {
468 $logger->warn("Barcode $barcode is a duplicate");
470 else {
472 $logger->trace("Adding barcode $barcode");
473 $item->barcode($barcode);
477 # Handling for 'AcqItemSetSubfieldsWhenReceived'
478 if (@affects) {
479 my $item_marc = C4::Items::GetMarcItem( $biblionumber, $item->itemnumber );
480 for my $affect (@affects) {
481 my ( $sf, $v ) = split q{=}, $affect, 2;
482 foreach ( $item_marc->field($itemfield) ) {
483 $_->update( $sf => $v );
486 C4::Items::ModItemFromMarc( $item_marc, $biblionumber, $item->itemnumber );
489 $item->update;
491 else {
492 $logger->warn("Unmatched item at branch:$branch");
494 ++$gir_occurrence;
496 return;
500 sub transfer_items {
501 my ( $schema, $inv_line, $order_from, $order_to, $quantity ) = @_;
503 # Transfer x items from the orig order to a completed partial order
504 my $gocc = 0;
505 my %mapped_by_branch;
506 while ( $gocc < $quantity ) {
507 my $branch = $inv_line->girfield( 'branch', $gocc );
508 if ( !exists $mapped_by_branch{$branch} ) {
509 $mapped_by_branch{$branch} = 1;
511 else {
512 $mapped_by_branch{$branch}++;
514 ++$gocc;
516 my $logger = Log::Log4perl->get_logger();
517 my $o1 = $order_from->ordernumber;
518 my $o2 = $order_to->ordernumber;
519 $logger->warn("transferring $quantity copies from order $o1 to order $o2");
521 my @item_links = $schema->resultset('AqordersItem')->search(
523 ordernumber => $order_from->ordernumber,
526 foreach my $ilink (@item_links) {
527 my $ino = $ilink->itemnumber;
528 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
529 my $i_branch = $item->homebranch;
530 if ( exists $mapped_by_branch{$i_branch}
531 && $mapped_by_branch{$i_branch} > 0 )
533 $ilink->ordernumber( $order_to->ordernumber );
534 $ilink->update;
535 --$quantity;
536 --$mapped_by_branch{$i_branch};
537 $logger->warn("Transferred item $item");
539 else {
540 $logger->warn("Skipped item $item");
542 if ( $quantity < 1 ) {
543 last;
547 return;
550 sub process_quote {
551 my $quote = shift;
553 $quote->status('processing');
554 $quote->update;
556 my $edi = Koha::Edifact->new( { transmission => $quote->raw_msg, } );
558 my $messages = $edi->message_array();
559 my $process_errors = 0;
560 my $logger = Log::Log4perl->get_logger();
561 my $schema = Koha::Database->new()->schema();
562 my $message_count = 0;
563 my @added_baskets; # if auto & multiple baskets need to order all
565 if ( @{$messages} && $quote->vendor_id ) {
566 foreach my $msg ( @{$messages} ) {
567 ++$message_count;
568 my $basketno =
569 NewBasket( $quote->vendor_id, 0, $quote->filename, q{},
570 q{} . q{} );
571 push @added_baskets, $basketno;
572 if ( $message_count > 1 ) {
573 my $m_filename = $quote->filename;
574 $m_filename .= "_$message_count";
575 $schema->resultset('EdifactMessage')->create(
577 message_type => $quote->message_type,
578 transfer_date => $quote->transfer_date,
579 vendor_id => $quote->vendor_id,
580 edi_acct => $quote->edi_acct,
581 status => 'recmsg',
582 basketno => $basketno,
583 raw_msg => q{},
584 filename => $m_filename,
588 else {
589 $quote->basketno($basketno);
591 $logger->trace("Created basket :$basketno");
592 my $items = $msg->lineitems();
593 my $refnum = $msg->message_refno;
595 for my $item ( @{$items} ) {
596 if ( !quote_item( $item, $quote, $basketno ) ) {
597 ++$process_errors;
602 my $status = 'received';
603 if ($process_errors) {
604 $status = 'error';
607 $quote->status($status);
608 $quote->update; # status and basketno link
609 # Do we automatically generate orders for this vendor
610 my $v = $schema->resultset('VendorEdiAccount')->search(
612 vendor_id => $quote->vendor_id,
614 )->single;
615 if ( $v->auto_orders ) {
616 for my $b (@added_baskets) {
617 create_edi_order(
619 ean => $messages->[0]->buyer_ean,
620 basketno => $b,
623 Koha::Acquisition::Baskets->find($b)->close;
627 return;
630 sub quote_item {
631 my ( $item, $quote, $basketno ) = @_;
633 my $schema = Koha::Database->new()->schema();
634 my $logger = Log::Log4perl->get_logger();
636 # $basketno is the return from AddBasket in the calling routine
637 # So this call should not fail unless that has
638 my $basket = Koha::Acquisition::Baskets->find( $basketno );
639 unless ( $basket ) {
640 $logger->error('Skipping order creation no valid basketno');
641 return;
643 $logger->trace( 'Checking db for matches with ', $item->item_number_id() );
644 my $bib = _check_for_existing_bib( $item->item_number_id() );
645 if ( !defined $bib ) {
646 $bib = {};
647 my $bib_record = _create_bib_from_quote( $item, $quote );
648 ( $bib->{biblionumber}, $bib->{biblioitemnumber} ) =
649 AddBiblio( $bib_record, q{} );
650 $logger->trace("New biblio added $bib->{biblionumber}");
652 else {
653 $logger->trace("Match found: $bib->{biblionumber}");
656 # Create an orderline
657 my $order_note = $item->{orderline_free_text};
658 $order_note ||= q{};
659 my $order_quantity = $item->quantity();
660 my $gir_count = $item->number_of_girs();
661 $order_quantity ||= 1; # quantity not necessarily present
662 if ( $gir_count > 1 ) {
663 if ( $gir_count != $order_quantity ) {
664 $logger->error(
665 "Order for $order_quantity items, $gir_count segments present");
667 $order_quantity = 1; # attempts to create an orderline for each gir
669 my $price = $item->price_info;
670 # Howells do not send an info price but do have a gross price
671 if (!$price) {
672 $price = $item->price_gross;
674 my $vendor = Koha::Acquisition::Booksellers->find( $quote->vendor_id );
676 # NB quote will not include tax info it only contains the list price
677 my $ecost = _discounted_price( $vendor->discount, $price, $item->price_info_inclusive );
679 # database definitions should set some of these defaults but dont
680 my $order_hash = {
681 biblionumber => $bib->{biblionumber},
682 entrydate => dt_from_string()->ymd(),
683 basketno => $basketno,
684 listprice => $price,
685 quantity => $order_quantity,
686 quantityreceived => 0,
687 order_vendornote => q{},
688 order_internalnote => $order_note,
689 replacementprice => $price,
690 rrp_tax_included => $price,
691 rrp_tax_excluded => $price,
692 rrp => $price,
693 ecost => $ecost,
694 ecost_tax_included => $ecost,
695 ecost_tax_excluded => $ecost,
696 uncertainprice => 0,
697 sort1 => q{},
698 sort2 => q{},
699 currency => $vendor->listprice(),
702 # suppliers references
703 if ( $item->reference() ) {
704 $order_hash->{suppliers_reference_number} = $item->reference;
705 $order_hash->{suppliers_reference_qualifier} = 'QLI';
707 elsif ( $item->orderline_reference_number() ) {
708 $order_hash->{suppliers_reference_number} =
709 $item->orderline_reference_number;
710 $order_hash->{suppliers_reference_qualifier} = 'SLI';
712 if ( $item->item_number_id ) { # suppliers ean
713 $order_hash->{line_item_id} = $item->item_number_id;
716 if ( $item->girfield('servicing_instruction') ) {
717 my $occ = 0;
718 my $txt = q{};
719 my $si;
720 while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
721 if ($occ) {
722 $txt .= q{: };
724 $txt .= $si;
725 ++$occ;
727 $order_hash->{order_vendornote} = $txt;
730 if ( $item->internal_notes() ) {
731 if ( $order_hash->{order_internalnote} ) { # more than ''
732 $order_hash->{order_internalnote} .= q{ };
734 $order_hash->{order_internalnote} .= $item->internal_notes;
737 my $budget = _get_budget( $schema, $item->girfield('fund_allocation') );
739 my $skip = '0';
740 if ( !$budget ) {
741 if ( $item->quantity > 1 ) {
742 carp 'Skipping line with no budget info';
743 $logger->trace('girfield skipped for invalid budget');
744 $skip++;
746 else {
747 carp 'Skipping line with no budget info';
748 $logger->trace('orderline skipped for invalid budget');
749 return;
753 my %ordernumber;
754 my %budgets;
755 my $item_hash;
757 if ( !$skip ) {
758 $order_hash->{budget_id} = $budget->budget_id;
759 my $first_order = $schema->resultset('Aqorder')->create($order_hash);
760 my $o = $first_order->ordernumber();
761 $logger->trace("Order created :$o");
763 # should be done by database settings
764 $first_order->parent_ordernumber( $first_order->ordernumber() );
765 $first_order->update();
767 # add to $budgets to prevent duplicate orderlines
768 $budgets{ $budget->budget_id } = '1';
770 # record ordernumber against budget
771 $ordernumber{ $budget->budget_id } = $o;
773 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
774 $item_hash = _create_item_from_quote( $item, $quote );
776 my $created = 0;
777 while ( $created < $order_quantity ) {
778 $item_hash->{biblionumber} = $bib->{biblionumber};
779 $item_hash->{biblioitemnumber} = $bib->{biblioitemnumber};
780 my $kitem = Koha::Item->new( $item_hash )->store;
781 my $itemnumber = $kitem->itemnumber;
782 $logger->trace("Added item:$itemnumber");
783 $schema->resultset('AqordersItem')->create(
785 ordernumber => $first_order->ordernumber,
786 itemnumber => $itemnumber,
789 ++$created;
794 if ( $order_quantity == 1 && $item->quantity > 1 ) {
795 my $occurrence = 1; # occ zero already added
796 while ( $occurrence < $item->quantity ) {
798 # check budget code
799 $budget = _get_budget( $schema,
800 $item->girfield( 'fund_allocation', $occurrence ) );
802 if ( !$budget ) {
803 my $bad_budget =
804 $item->girfield( 'fund_allocation', $occurrence );
805 carp 'Skipping line with no budget info';
806 $logger->trace(
807 "girfield skipped for invalid budget:$bad_budget");
808 ++$occurrence; ## lets look at the next one not this one again
809 next;
812 # add orderline for NEW budget in $budgets
813 if ( !exists $budgets{ $budget->budget_id } ) {
815 # $order_hash->{quantity} = 1; by default above
816 # we should handle both 1:1 GIR & 1:n GIR (with LQT values) here
818 $order_hash->{budget_id} = $budget->budget_id;
820 my $new_order =
821 $schema->resultset('Aqorder')->create($order_hash);
822 my $o = $new_order->ordernumber();
823 $logger->trace("Order created :$o");
825 # should be done by database settings
826 $new_order->parent_ordernumber( $new_order->ordernumber() );
827 $new_order->update();
829 # add to $budgets to prevent duplicate orderlines
830 $budgets{ $budget->budget_id } = '1';
832 # record ordernumber against budget
833 $ordernumber{ $budget->budget_id } = $o;
835 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
836 if ( !defined $item_hash ) {
837 $item_hash = _create_item_from_quote( $item, $quote );
839 my $new_item = {
840 itype =>
841 $item->girfield( 'stock_category', $occurrence ),
842 location =>
843 $item->girfield( 'collection_code', $occurrence ),
844 itemcallnumber =>
845 $item->girfield( 'shelfmark', $occurrence )
846 || $item->girfield( 'classification', $occurrence )
847 || title_level_class($item),
848 holdingbranch =>
849 $item->girfield( 'branch', $occurrence ),
850 homebranch => $item->girfield( 'branch', $occurrence ),
852 if ( $new_item->{itype} ) {
853 $item_hash->{itype} = $new_item->{itype};
855 if ( $new_item->{location} ) {
856 $item_hash->{location} = $new_item->{location};
858 if ( $new_item->{itemcallnumber} ) {
859 $item_hash->{itemcallnumber} =
860 $new_item->{itemcallnumber};
862 if ( $new_item->{holdingbranch} ) {
863 $item_hash->{holdingbranch} =
864 $new_item->{holdingbranch};
866 if ( $new_item->{homebranch} ) {
867 $item_hash->{homebranch} = $new_item->{homebranch};
870 $item_hash->{biblionumber} = $bib->{biblionumber};
871 $item_hash->{biblioitemnumber} = $bib->{biblioitemnumber};
872 my $kitem = Koha::Item->new( $item_hash )->store;
873 my $itemnumber = $kitem->itemnumber;
874 $logger->trace("New item $itemnumber added");
875 $schema->resultset('AqordersItem')->create(
877 ordernumber => $new_order->ordernumber,
878 itemnumber => $itemnumber,
882 my $lrp =
883 $item->girfield( 'library_rotation_plan', $occurrence );
884 if ($lrp) {
885 my $rota =
886 Koha::StockRotationRotas->find( { title => $lrp },
887 { key => 'stockrotationrotas_title' } );
888 if ($rota) {
889 $rota->add_item($itemnumber);
890 $logger->trace("Item added to rota $rota->id");
892 else {
893 $logger->error(
894 "No rota found matching $lrp in orderline");
899 ++$occurrence;
902 # increment quantity in orderline for EXISTING budget in $budgets
903 else {
904 my $row = $schema->resultset('Aqorder')->find(
906 ordernumber => $ordernumber{ $budget->budget_id }
909 if ($row) {
910 my $qty = $row->quantity;
911 $qty++;
912 $row->update(
914 quantity => $qty,
919 # Do not use the basket level value as it is always NULL
920 # See calling subs call to AddBasket
921 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
922 my $new_item = {
923 notforloan => -1,
924 cn_sort => q{},
925 cn_source => 'ddc',
926 price => $price,
927 replacementprice => $price,
928 itype =>
929 $item->girfield( 'stock_category', $occurrence ),
930 location =>
931 $item->girfield( 'collection_code', $occurrence ),
932 itemcallnumber =>
933 $item->girfield( 'shelfmark', $occurrence )
934 || $item->girfield( 'classification', $occurrence )
935 || $item_hash->{itemcallnumber},
936 holdingbranch =>
937 $item->girfield( 'branch', $occurrence ),
938 homebranch => $item->girfield( 'branch', $occurrence ),
940 $new_item->{biblionumber} = $bib->{biblionumber};
941 $new_item->{biblioitemnumber} = $bib->{biblioitemnumber};
942 my $kitem = Koha::Item->new( $new_item )->store;
943 my $itemnumber = $kitem->itemnumber;
944 $logger->trace("New item $itemnumber added");
945 $schema->resultset('AqordersItem')->create(
947 ordernumber => $ordernumber{ $budget->budget_id },
948 itemnumber => $itemnumber,
952 my $lrp =
953 $item->girfield( 'library_rotation_plan', $occurrence );
954 if ($lrp) {
955 my $rota =
956 Koha::StockRotationRotas->find( { title => $lrp },
957 { key => 'stockrotationrotas_title' } );
958 if ($rota) {
959 $rota->add_item($itemnumber);
960 $logger->trace("Item added to rota $rota->id");
962 else {
963 $logger->error(
964 "No rota found matching $lrp in orderline");
969 ++$occurrence;
973 return 1;
977 sub get_edifact_ean {
979 my $dbh = C4::Context->dbh;
981 my $eans = $dbh->selectcol_arrayref('select ean from edifact_ean');
983 return $eans->[0];
986 # We should not need to have a routine to do this here
987 sub _discounted_price {
988 my ( $discount, $price, $discounted_price ) = @_;
989 if (defined $discounted_price) {
990 return $discounted_price;
992 if (!$price) {
993 return 0;
995 return $price - ( ( $discount * $price ) / 100 );
998 sub _check_for_existing_bib {
999 my $isbn = shift;
1001 my $search_isbn = $isbn;
1002 $search_isbn =~ s/^\s*/%/xms;
1003 $search_isbn =~ s/\s*$/%/xms;
1004 my $dbh = C4::Context->dbh;
1005 my $sth = $dbh->prepare(
1006 'select biblionumber, biblioitemnumber from biblioitems where isbn like ?',
1008 my $tuple_arr =
1009 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
1010 if ( @{$tuple_arr} ) {
1011 return $tuple_arr->[0];
1013 elsif ( length($isbn) == 13 && $isbn !~ /^97[89]/ ) {
1014 my $tarr = $dbh->selectall_arrayref(
1015 'select biblionumber, biblioitemnumber from biblioitems where ean = ?',
1016 { Slice => {} },
1017 $isbn
1019 if ( @{$tarr} ) {
1020 return $tarr->[0];
1023 else {
1024 undef $search_isbn;
1025 $isbn =~ s/\-//xmsg;
1026 if ( $isbn =~ m/(\d{13})/xms ) {
1027 my $b_isbn = Business::ISBN->new($1);
1028 if ( $b_isbn && $b_isbn->is_valid && $b_isbn->as_isbn10 ) {
1029 $search_isbn = $b_isbn->as_isbn10->as_string( [] );
1033 elsif ( $isbn =~ m/(\d{9}[xX]|\d{10})/xms ) {
1034 my $b_isbn = Business::ISBN->new($1);
1035 if ( $b_isbn && $b_isbn->is_valid ) {
1036 $search_isbn = $b_isbn->as_isbn13->as_string( [] );
1040 if ($search_isbn) {
1041 $search_isbn = "%$search_isbn%";
1042 $tuple_arr =
1043 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
1044 if ( @{$tuple_arr} ) {
1045 return $tuple_arr->[0];
1049 return;
1052 # returns a budget obj or undef
1053 # fact we need this shows what a mess Acq API is
1054 sub _get_budget {
1055 my ( $schema, $budget_code ) = @_;
1056 my $period_rs = $schema->resultset('Aqbudgetperiod')->search(
1058 budget_period_active => 1,
1062 # db does not ensure budget code is unque
1063 return $schema->resultset('Aqbudget')->single(
1065 budget_code => $budget_code,
1066 budget_period_id =>
1067 { -in => $period_rs->get_column('budget_period_id')->as_query },
1072 # try to get title level classification from incoming quote
1073 sub title_level_class {
1074 my ($item) = @_;
1075 my $class = q{};
1076 my $default_scheme = C4::Context->preference('DefaultClassificationSource');
1077 if ( $default_scheme eq 'ddc' ) {
1078 $class = $item->dewey_class();
1080 elsif ( $default_scheme eq 'lcc' ) {
1081 $class = $item->lc_class();
1083 if ( !$class ) {
1084 $class =
1085 $item->girfield('shelfmark')
1086 || $item->girfield('classification')
1087 || q{};
1089 return $class;
1092 sub _create_bib_from_quote {
1094 #TBD we should flag this for updating from an external source
1095 #As biblio (&biblioitems) has no candidates flag in order
1096 my ( $item, $quote ) = @_;
1097 my $itemid = $item->item_number_id;
1098 my $defalt_classification_source =
1099 C4::Context->preference('DefaultClassificationSource');
1100 my $bib_hash = {
1101 'biblioitems.cn_source' => $defalt_classification_source,
1102 'items.cn_source' => $defalt_classification_source,
1103 'items.notforloan' => -1,
1104 'items.cn_sort' => q{},
1106 $bib_hash->{'biblio.seriestitle'} = $item->series;
1108 $bib_hash->{'biblioitems.publishercode'} = $item->publisher;
1109 $bib_hash->{'biblioitems.publicationyear'} =
1110 $bib_hash->{'biblio.copyrightdate'} = $item->publication_date;
1112 $bib_hash->{'biblio.title'} = $item->title;
1113 $bib_hash->{'biblio.author'} = $item->author;
1114 $bib_hash->{'biblioitems.isbn'} = $item->item_number_id;
1115 $bib_hash->{'biblioitems.itemtype'} = $item->girfield('stock_category');
1117 # If we have a 13 digit id we are assuming its an ean
1118 # (it may also be an isbn or issn)
1119 if ( $itemid =~ /^\d{13}$/ ) {
1120 $bib_hash->{'biblioitems.ean'} = $itemid;
1121 if ( $itemid =~ /^977/ ) {
1122 $bib_hash->{'biblioitems.issn'} = $itemid;
1125 for my $key ( keys %{$bib_hash} ) {
1126 if ( !defined $bib_hash->{$key} ) {
1127 delete $bib_hash->{$key};
1130 return TransformKohaToMarc($bib_hash);
1134 sub _create_item_from_quote {
1135 my ( $item, $quote ) = @_;
1136 my $defalt_classification_source =
1137 C4::Context->preference('DefaultClassificationSource');
1138 my $item_hash = {
1139 cn_source => $defalt_classification_source,
1140 notforloan => -1,
1141 cn_sort => q{},
1143 $item_hash->{booksellerid} = $quote->vendor_id;
1144 $item_hash->{price} = $item_hash->{replacementprice} = $item->price;
1145 $item_hash->{itype} = $item->girfield('stock_category');
1146 $item_hash->{location} = $item->girfield('collection_code');
1148 my $note = {};
1150 $item_hash->{itemcallnumber} =
1151 $item->girfield('shelfmark')
1152 || $item->girfield('classification')
1153 || title_level_class($item);
1155 my $branch = $item->girfield('branch');
1156 $item_hash->{holdingbranch} = $item_hash->{homebranch} = $branch;
1157 return $item_hash;
1161 __END__
1163 =head1 NAME
1165 Koha::EDI
1167 =head1 SYNOPSIS
1169 Module exporting subroutines used in EDI processing for Koha
1171 =head1 DESCRIPTION
1173 Subroutines called by batch processing to handle Edifact
1174 messages of various types and related utilities
1176 =head1 BUGS
1178 These routines should really be methods of some object.
1179 get_edifact_ean is a stopgap which should be replaced
1181 =head1 SUBROUTINES
1183 =head2 process_quote
1185 process_quote(quote_message);
1187 passed a message object for a quote, parses it creating an order basket
1188 and orderlines in the database
1189 updates the message's status to received in the database and adds the
1190 link to basket
1192 =head2 process_invoice
1194 process_invoice(invoice_message)
1196 passed a message object for an invoice, add the contained invoices
1197 and update the orderlines referred to in the invoice
1198 As an Edifact invoice is in effect a despatch note this receipts the
1199 appropriate quantities in the orders
1201 no meaningful return value
1203 =head2 process_ordrsp
1205 process_ordrsp(ordrsp_message)
1207 passed a message object for a supplier response, process the contents
1208 If an orderline is cancelled cancel the corresponding orderline in koha
1209 otherwise record the supplier message against it
1211 no meaningful return value
1213 =head2 create_edi_order
1215 create_edi_order( { parameter_hashref } )
1217 parameters must include basketno and ean
1219 branchcode can optionally be passed
1221 returns 1 on success undef otherwise
1223 if the parameter noingest is set the formatted order is returned
1224 and not saved in the database. This functionality is intended for debugging only
1226 =head2 receipt_items
1228 receipt_items( schema_obj, invoice_line, ordernumber, $quantity)
1230 receipts the items recorded on this invoice line
1232 no meaningful return
1234 =head2 transfer_items
1236 transfer_items(schema, invoice_line, originating_order, receiving_order, $quantity)
1238 Transfer the items covered by this invoice line from their original
1239 order to another order recording the partial fulfillment of the original
1240 order
1242 no meaningful return
1244 =head2 get_edifact_ean
1246 $ean = get_edifact_ean();
1248 routine to return the ean.
1250 =head2 quote_item
1252 quote_item(lineitem, quote_message);
1254 Called by process_quote to handle an individual lineitem
1255 Generate the biblios and items if required and orderline linking to them
1257 Returns 1 on success undef on error
1259 Most usual cause of error is a line with no or incorrect budget codes
1260 which woild cause order creation to abort
1261 If other correct lines exist these are processed and the erroneous line os logged
1263 =head2 title_level_class
1265 classmark = title_level_class(edi_item)
1267 Trys to return a title level classmark from a quote message line
1268 Will return a dewey or lcc classmark if one exists according to the
1269 value in DefaultClassificationSource syspref
1271 If unable to returns the shelfmark or classification from the GIR segment
1273 If all else fails returns empty string
1275 =head2 _create_bib_from_quote
1277 marc_record_obj = _create_bib_from_quote(lineitem, quote)
1279 Returns a MARC::Record object based on the info in the quote's lineitem
1281 =head2 _create_item_from_quote
1283 item_hashref = _create_item_from_quote( lineitem, quote)
1285 returns a hashref representing the item fields specified in the quote
1287 =head2 _get_invoiced_price
1289 (price, price_tax_excluded) = _get_invoiced_price(line_object, $quantity)
1291 Returns an array of unitprice and unitprice_tax_excluded derived from the lineitem
1292 monetary fields
1294 =head2 _discounted_price
1296 ecost = _discounted_price(discount, item_price, discounted_price)
1298 utility subroutine to return a price calculated from the
1299 vendors discount and quoted price
1300 if invoice has a field containing discounted price that is returned
1301 instead of recalculating
1303 =head2 _check_for_existing_bib
1305 (biblionumber, biblioitemnumber) = _check_for_existing_bib(isbn_or_ean)
1307 passed an isbn or ean attempts to locate a match bib
1308 On success returns biblionumber and biblioitemnumber
1309 On failure returns undefined/an empty list
1311 =head2 _get_budget
1313 b = _get_budget(schema_obj, budget_code)
1315 Returns the Aqbudget object for the active budget given the passed budget_code
1316 or undefined if one does not exist
1318 =head1 AUTHOR
1320 Colin Campbell <colin.campbell@ptfs-europe.com>
1323 =head1 COPYRIGHT
1325 Copyright 2014,2015 PTFS-Europe Ltd
1326 This program is free software, You may redistribute it under
1327 under the terms of the GNU General Public License
1330 =cut