Bug 24031: Add safety checks in Koha::Plugins::call
[koha.git] / Koha / EDI.pm
bloba692da146b80d7feb42af8b6bb6b815861c01424
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 CloseBasket 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 = $invoice_message->edi_acct()->plugin();
215 my $edi_plugin;
216 if ( $plugin ) {
217 $edi_plugin = Koha::Plugins::Handler->run(
219 class => $plugin,
220 method => 'edifact',
221 params => {
222 invoice_message => $invoice_message,
223 transmission => $invoice_message->raw_msg,
229 my $edi = $edi_plugin ||
230 Koha::Edifact->new( { transmission => $invoice_message->raw_msg, } );
232 my $messages = $edi->message_array();
234 if ( @{$messages} ) {
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;
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(
250 san => $vendor_ean,
252 )->single;
254 if ( !$vendor_acct ) {
255 carp
256 "Cannot find vendor with ean $vendor_ean for invoice $invoicenumber in $invoice_message->filename";
257 next;
259 $invoice_message->edi_acct( $vendor_acct->id );
260 $logger->trace("Adding invoice:$invoicenumber");
261 my $new_invoice = $schema->resultset('Aqinvoice')->create(
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,
272 my $invoiceid = $new_invoice->invoiceid;
273 $logger->trace("Added as invoiceno :$invoiceid");
274 my $lines = $msg->lineitems();
276 foreach my $line ( @{$lines} ) {
277 my $ordernumber = $line->ordernumber;
278 $logger->trace( "Receipting order:$ordernumber Qty: ",
279 $line->quantity );
281 my $order = $schema->resultset('Aqorder')->find($ordernumber);
283 # ModReceiveOrder does not validate that $ordernumber exists validate here
284 if ($order) {
286 # check suggestions
287 my $s = $schema->resultset('Suggestion')->search(
289 biblionumber => $order->biblionumber->biblionumber,
291 )->single;
292 if ($s) {
293 ModSuggestion(
295 suggestionid => $s->suggestionid,
296 STATUS => 'AVAILABLE',
301 my $price = _get_invoiced_price($line);
303 if ( $order->quantity > $line->quantity ) {
304 my $ordered = $order->quantity;
306 # part receipt
307 $order->orderstatus('partial');
308 $order->quantity( $ordered - $line->quantity );
309 $order->update;
310 my $received_order = $order->copy(
312 ordernumber => undef,
313 quantity => $line->quantity,
314 quantityreceived => $line->quantity,
315 orderstatus => 'complete',
316 unitprice => $price,
317 invoiceid => $invoiceid,
318 datereceived => $msg_date,
321 transfer_items( $schema, $line, $order,
322 $received_order );
323 receipt_items( $schema, $line,
324 $received_order->ordernumber );
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 );
336 else {
337 $logger->error(
338 "No order found for $ordernumber Invoice:$invoicenumber"
340 next;
348 $invoice_message->status('received');
349 $invoice_message->update; # status and basketno link
350 return;
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
362 return $price;
365 sub receipt_items {
366 my ( $schema, $inv_line, $ordernumber ) = @_;
367 my $logger = Log::Log4perl->get_logger();
368 my $quantity = $inv_line->quantity;
370 # itemnumber is not a foreign key ??? makes this a bit cumbersome
371 my @item_links = $schema->resultset('AqordersItem')->search(
373 ordernumber => $ordernumber,
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;
385 my $b = $item->homebranch->branchcode;
386 if ( !exists $branch_map{$b} ) {
387 $branch_map{$b} = [];
389 push @{ $branch_map{$b} }, $item;
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 my $order = Koha::Acquisition::Orders->find($ordernumber);
401 $biblionumber = $order->biblionumber;
402 my $frameworkcode = GetFrameworkCode($biblionumber);
403 ($itemfield) = GetMarcFromKohaField( 'items.itemnumber',
404 $frameworkcode );
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(
417 barcode => $barcode,
420 if ( $rs->count > 0 ) {
421 $logger->warn("Barcode $barcode is a duplicate");
423 else {
425 $logger->trace("Adding barcode $barcode");
426 $item->barcode($barcode);
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 );
439 C4::Items::ModItemFromMarc( $item_marc, $biblionumber, $item->itemnumber );
442 $item->update;
444 else {
445 $logger->warn("Unmatched item at branch:$branch");
447 ++$gir_occurrence;
449 return;
453 sub transfer_items {
454 my ( $schema, $inv_line, $order_from, $order_to ) = @_;
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;
465 else {
466 $mapped_by_branch{$branch}++;
468 ++$gocc;
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");
475 my @item_links = $schema->resultset('AqordersItem')->search(
477 ordernumber => $order_from->ordernumber,
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 )
487 $ilink->ordernumber( $order_to->ordernumber );
488 $ilink->update;
489 --$quantity;
490 --$mapped_by_branch{$i_branch};
491 $logger->warn("Transferred item $item");
493 else {
494 $logger->warn("Skipped item $item");
496 if ( $quantity < 1 ) {
497 last;
501 return;
504 sub process_quote {
505 my $quote = shift;
507 $quote->status('processing');
508 $quote->update;
510 my $edi = Koha::Edifact->new( { transmission => $quote->raw_msg, } );
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
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(
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,
542 else {
543 $quote->basketno($basketno);
545 $logger->trace("Created basket :$basketno");
546 my $items = $msg->lineitems();
547 my $refnum = $msg->message_refno;
549 for my $item ( @{$items} ) {
550 if ( !quote_item( $item, $quote, $basketno ) ) {
551 ++$process_errors;
556 my $status = 'received';
557 if ($process_errors) {
558 $status = 'error';
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(
566 vendor_id => $quote->vendor_id,
568 )->single;
569 if ( $v->auto_orders ) {
570 for my $b (@added_baskets) {
571 create_edi_order(
573 ean => $messages->[0]->buyer_ean,
574 basketno => $b,
577 CloseBasket($b);
581 return;
584 sub quote_item {
585 my ( $item, $quote, $basketno ) = @_;
587 my $schema = Koha::Database->new()->schema();
588 my $logger = Log::Log4perl->get_logger();
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;
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}");
606 else {
607 $logger->trace("Match found: $bib->{biblionumber}");
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");
621 $order_quantity = 1; # attempts to create an orderline for each gir
623 my $vendor = Koha::Acquisition::Booksellers->find( $quote->vendor_id );
625 # database definitions should set some of these defaults but dont
626 my $order_hash = {
627 biblionumber => $bib->{biblionumber},
628 entrydate => dt_from_string()->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(),
645 # suppliers references
646 if ( $item->reference() ) {
647 $order_hash->{suppliers_reference_number} = $item->reference;
648 $order_hash->{suppliers_reference_qualifier} = 'QLI';
650 elsif ( $item->orderline_reference_number() ) {
651 $order_hash->{suppliers_reference_number} =
652 $item->orderline_reference_number;
653 $order_hash->{suppliers_reference_qualifier} = 'SLI';
655 if ( $item->item_number_id ) { # suppliers ean
656 $order_hash->{line_item_id} = $item->item_number_id;
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{: };
667 $txt .= $si;
668 ++$occ;
670 $order_hash->{order_vendornote} = $txt;
673 if ( $item->internal_notes() ) {
674 if ( $order_hash->{order_internalnote} ) { # more than ''
675 $order_hash->{order_internalnote} .= q{ };
677 $order_hash->{order_internalnote} .= $item->internal_notes;
680 my $budget = _get_budget( $schema, $item->girfield('fund_allocation') );
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++;
689 else {
690 carp 'Skipping line with no budget info';
691 $logger->trace('orderline skipped for invalid budget');
692 return;
696 my %ordernumber;
697 my %budgets;
698 my $item_hash;
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");
706 # should be done by database settings
707 $first_order->parent_ordernumber( $first_order->ordernumber() );
708 $first_order->update();
710 # add to $budgets to prevent duplicate orderlines
711 $budgets{ $budget->budget_id } = '1';
713 # record ordernumber against budget
714 $ordernumber{ $budget->budget_id } = $o;
716 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
717 $item_hash = _create_item_from_quote( $item, $quote );
719 my $created = 0;
720 while ( $created < $order_quantity ) {
721 $item_hash->{biblionumber} = $bib->{biblionumber};
722 my $item = Koha::Item->new( $item_hash );
723 my $itemnumber = $item->itemnumber;
724 $logger->trace("Added item:$itemnumber");
725 $schema->resultset('AqordersItem')->create(
727 ordernumber => $first_order->ordernumber,
728 itemnumber => $itemnumber,
731 ++$created;
736 if ( $order_quantity == 1 && $item->quantity > 1 ) {
737 my $occurrence = 1; # occ zero already added
738 while ( $occurrence < $item->quantity ) {
740 # check budget code
741 $budget = _get_budget( $schema,
742 $item->girfield( 'fund_allocation', $occurrence ) );
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;
754 # add orderline for NEW budget in $budgets
755 if ( !exists $budgets{ $budget->budget_id } ) {
757 # $order_hash->{quantity} = 1; by default above
758 # we should handle both 1:1 GIR & 1:n GIR (with LQT values) here
760 $order_hash->{budget_id} = $budget->budget_id;
762 my $new_order =
763 $schema->resultset('Aqorder')->create($order_hash);
764 my $o = $new_order->ordernumber();
765 $logger->trace("Order created :$o");
767 # should be done by database settings
768 $new_order->parent_ordernumber( $new_order->ordernumber() );
769 $new_order->update();
771 # add to $budgets to prevent duplicate orderlines
772 $budgets{ $budget->budget_id } = '1';
774 # record ordernumber against budget
775 $ordernumber{ $budget->budget_id } = $o;
777 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
778 if ( !defined $item_hash ) {
779 $item_hash = _create_item_from_quote( $item, $quote );
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 ),
794 if ( $new_item->{itype} ) {
795 $item_hash->{itype} = $new_item->{itype};
797 if ( $new_item->{location} ) {
798 $item_hash->{location} = $new_item->{location};
800 if ( $new_item->{itemcallnumber} ) {
801 $item_hash->{itemcallnumber} =
802 $new_item->{itemcallnumber};
804 if ( $new_item->{holdingbranch} ) {
805 $item_hash->{holdingbranch} =
806 $new_item->{holdingbranch};
808 if ( $new_item->{homebranch} ) {
809 $item_hash->{homebranch} = $new_item->{homebranch};
812 $item_hash->{biblionumber} = $bib->{biblionumber};
813 my $item = Koha::Item->new( $item_hash );
814 my $itemnumber = $item->itemnumber;
815 $logger->trace("New item $itemnumber added");
816 $schema->resultset('AqordersItem')->create(
818 ordernumber => $new_order->ordernumber,
819 itemnumber => $itemnumber,
823 my $lrp =
824 $item->girfield( 'library_rotation_plan', $occurrence );
825 if ($lrp) {
826 my $rota =
827 Koha::StockRotationRotas->find( { title => $lrp },
828 { key => 'stockrotationrotas_title' } );
829 if ($rota) {
830 $rota->add_item($itemnumber);
831 $logger->trace("Item added to rota $rota->id");
833 else {
834 $logger->error(
835 "No rota found matching $lrp in orderline");
840 ++$occurrence;
843 # increment quantity in orderline for EXISTING budget in $budgets
844 else {
845 my $row = $schema->resultset('Aqorder')->find(
847 ordernumber => $ordernumber{ $budget->budget_id }
850 if ($row) {
851 my $qty = $row->quantity;
852 $qty++;
853 $row->update(
855 quantity => $qty,
860 # Do not use the basket level value as it is always NULL
861 # See calling subs call to AddBasket
862 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
863 my $new_item = {
864 notforloan => -1,
865 cn_sort => q{},
866 cn_source => 'ddc',
867 price => $item->price,
868 replacementprice => $item->price,
869 itype =>
870 $item->girfield( 'stock_category', $occurrence ),
871 location =>
872 $item->girfield( 'collection_code', $occurrence ),
873 itemcallnumber =>
874 $item->girfield( 'shelfmark', $occurrence )
875 || $item->girfield( 'classification', $occurrence )
876 || $item_hash->{itemcallnumber},
877 holdingbranch =>
878 $item->girfield( 'branch', $occurrence ),
879 homebranch => $item->girfield( 'branch', $occurrence ),
881 $new_item->{biblionumber} = $bib->{biblionumber};
882 my $item = Koha::Item->new( $new_item );
883 my $itemnumber = $item->itemnumber;
884 $logger->trace("New item $itemnumber added");
885 $schema->resultset('AqordersItem')->create(
887 ordernumber => $ordernumber{ $budget->budget_id },
888 itemnumber => $itemnumber,
892 my $lrp =
893 $item->girfield( 'library_rotation_plan', $occurrence );
894 if ($lrp) {
895 my $rota =
896 Koha::StockRotationRotas->find( { title => $lrp },
897 { key => 'stockrotationrotas_title' } );
898 if ($rota) {
899 $rota->add_item($itemnumber);
900 $logger->trace("Item added to rota $rota->id");
902 else {
903 $logger->error(
904 "No rota found matching $lrp in orderline");
909 ++$occurrence;
913 return 1;
917 sub get_edifact_ean {
919 my $dbh = C4::Context->dbh;
921 my $eans = $dbh->selectcol_arrayref('select ean from edifact_ean');
923 return $eans->[0];
926 # We should not need to have a routine to do this here
927 sub _discounted_price {
928 my ( $discount, $price ) = @_;
929 return $price - ( ( $discount * $price ) / 100 );
932 sub _check_for_existing_bib {
933 my $isbn = shift;
935 my $search_isbn = $isbn;
936 $search_isbn =~ s/^\s*/%/xms;
937 $search_isbn =~ s/\s*$/%/xms;
938 my $dbh = C4::Context->dbh;
939 my $sth = $dbh->prepare(
940 'select biblionumber, biblioitemnumber from biblioitems where isbn like ?',
942 my $tuple_arr =
943 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
944 if ( @{$tuple_arr} ) {
945 return $tuple_arr->[0];
947 elsif ( length($isbn) == 13 && $isbn !~ /^97[89]/ ) {
948 my $tarr = $dbh->selectall_arrayref(
949 'select biblionumber, biblioitemnumber from biblioitems where ean = ?',
950 { Slice => {} },
951 $isbn
953 if ( @{$tarr} ) {
954 return $tarr->[0];
957 else {
958 undef $search_isbn;
959 $isbn =~ s/\-//xmsg;
960 if ( $isbn =~ m/(\d{13})/xms ) {
961 my $b_isbn = Business::ISBN->new($1);
962 if ( $b_isbn && $b_isbn->is_valid && $b_isbn->as_isbn10 ) {
963 $search_isbn = $b_isbn->as_isbn10->as_string( [] );
967 elsif ( $isbn =~ m/(\d{9}[xX]|\d{10})/xms ) {
968 my $b_isbn = Business::ISBN->new($1);
969 if ( $b_isbn && $b_isbn->is_valid ) {
970 $search_isbn = $b_isbn->as_isbn13->as_string( [] );
974 if ($search_isbn) {
975 $search_isbn = "%$search_isbn%";
976 $tuple_arr =
977 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
978 if ( @{$tuple_arr} ) {
979 return $tuple_arr->[0];
983 return;
986 # returns a budget obj or undef
987 # fact we need this shows what a mess Acq API is
988 sub _get_budget {
989 my ( $schema, $budget_code ) = @_;
990 my $period_rs = $schema->resultset('Aqbudgetperiod')->search(
992 budget_period_active => 1,
996 # db does not ensure budget code is unque
997 return $schema->resultset('Aqbudget')->single(
999 budget_code => $budget_code,
1000 budget_period_id =>
1001 { -in => $period_rs->get_column('budget_period_id')->as_query },
1006 # try to get title level classification from incoming quote
1007 sub title_level_class {
1008 my ($item) = @_;
1009 my $class = q{};
1010 my $default_scheme = C4::Context->preference('DefaultClassificationSource');
1011 if ( $default_scheme eq 'ddc' ) {
1012 $class = $item->dewey_class();
1014 elsif ( $default_scheme eq 'lcc' ) {
1015 $class = $item->lc_class();
1017 if ( !$class ) {
1018 $class =
1019 $item->girfield('shelfmark')
1020 || $item->girfield('classification')
1021 || q{};
1023 return $class;
1026 sub _create_bib_from_quote {
1028 #TBD we should flag this for updating from an external source
1029 #As biblio (&biblioitems) has no candidates flag in order
1030 my ( $item, $quote ) = @_;
1031 my $itemid = $item->item_number_id;
1032 my $defalt_classification_source =
1033 C4::Context->preference('DefaultClassificationSource');
1034 my $bib_hash = {
1035 'biblioitems.cn_source' => $defalt_classification_source,
1036 'items.cn_source' => $defalt_classification_source,
1037 'items.notforloan' => -1,
1038 'items.cn_sort' => q{},
1040 $bib_hash->{'biblio.seriestitle'} = $item->series;
1042 $bib_hash->{'biblioitems.publishercode'} = $item->publisher;
1043 $bib_hash->{'biblioitems.publicationyear'} =
1044 $bib_hash->{'biblio.copyrightdate'} = $item->publication_date;
1046 $bib_hash->{'biblio.title'} = $item->title;
1047 $bib_hash->{'biblio.author'} = $item->author;
1048 $bib_hash->{'biblioitems.isbn'} = $item->item_number_id;
1049 $bib_hash->{'biblioitems.itemtype'} = $item->girfield('stock_category');
1051 # If we have a 13 digit id we are assuming its an ean
1052 # (it may also be an isbn or issn)
1053 if ( $itemid =~ /^\d{13}$/ ) {
1054 $bib_hash->{'biblioitems.ean'} = $itemid;
1055 if ( $itemid =~ /^977/ ) {
1056 $bib_hash->{'biblioitems.issn'} = $itemid;
1059 for my $key ( keys %{$bib_hash} ) {
1060 if ( !defined $bib_hash->{$key} ) {
1061 delete $bib_hash->{$key};
1064 return TransformKohaToMarc($bib_hash);
1068 sub _create_item_from_quote {
1069 my ( $item, $quote ) = @_;
1070 my $defalt_classification_source =
1071 C4::Context->preference('DefaultClassificationSource');
1072 my $item_hash = {
1073 cn_source => $defalt_classification_source,
1074 notforloan => -1,
1075 cn_sort => q{},
1077 $item_hash->{booksellerid} = $quote->vendor_id;
1078 $item_hash->{price} = $item_hash->{replacementprice} = $item->price;
1079 $item_hash->{itype} = $item->girfield('stock_category');
1080 $item_hash->{location} = $item->girfield('collection_code');
1082 my $note = {};
1084 $item_hash->{itemcallnumber} =
1085 $item->girfield('shelfmark')
1086 || $item->girfield('classification')
1087 || title_level_class($item);
1089 my $branch = $item->girfield('branch');
1090 $item_hash->{holdingbranch} = $item_hash->{homebranch} = $branch;
1091 return $item_hash;
1095 __END__
1097 =head1 NAME
1099 Koha::EDI
1101 =head1 SYNOPSIS
1103 Module exporting subroutines used in EDI processing for Koha
1105 =head1 DESCRIPTION
1107 Subroutines called by batch processing to handle Edifact
1108 messages of various types and related utilities
1110 =head1 BUGS
1112 These routines should really be methods of some object.
1113 get_edifact_ean is a stopgap which should be replaced
1115 =head1 SUBROUTINES
1117 =head2 process_quote
1119 process_quote(quote_message);
1121 passed a message object for a quote, parses it creating an order basket
1122 and orderlines in the database
1123 updates the message's status to received in the database and adds the
1124 link to basket
1126 =head2 process_invoice
1128 process_invoice(invoice_message)
1130 passed a message object for an invoice, add the contained invoices
1131 and update the orderlines referred to in the invoice
1132 As an Edifact invoice is in effect a despatch note this receipts the
1133 appropriate quantities in the orders
1135 no meaningful return value
1137 =head2 process_ordrsp
1139 process_ordrsp(ordrsp_message)
1141 passed a message object for a supplier response, process the contents
1142 If an orderline is cancelled cancel the corresponding orderline in koha
1143 otherwise record the supplier message against it
1145 no meaningful return value
1147 =head2 create_edi_order
1149 create_edi_order( { parameter_hashref } )
1151 parameters must include basketno and ean
1153 branchcode can optionally be passed
1155 returns 1 on success undef otherwise
1157 if the parameter noingest is set the formatted order is returned
1158 and not saved in the database. This functionality is intended for debugging only
1160 =head2 receipt_items
1162 receipt_items( schema_obj, invoice_line, ordernumber)
1164 receipts the items recorded on this invoice line
1166 no meaningful return
1168 =head2 transfer_items
1170 transfer_items(schema, invoice_line, originating_order, receiving_order)
1172 Transfer the items covered by this invoice line from their original
1173 order to another order recording the partial fulfillment of the original
1174 order
1176 no meaningful return
1178 =head2 get_edifact_ean
1180 $ean = get_edifact_ean();
1182 routine to return the ean.
1184 =head2 quote_item
1186 quote_item(lineitem, quote_message);
1188 Called by process_quote to handle an individual lineitem
1189 Generate the biblios and items if required and orderline linking to them
1191 Returns 1 on success undef on error
1193 Most usual cause of error is a line with no or incorrect budget codes
1194 which woild cause order creation to abort
1195 If other correct lines exist these are processed and the erroneous line os logged
1197 =head2 title_level_class
1199 classmark = title_level_class(edi_item)
1201 Trys to return a title level classmark from a quote message line
1202 Will return a dewey or lcc classmark if one exists according to the
1203 value in DefaultClassificationSource syspref
1205 If unable to returns the shelfmark or classification from the GIR segment
1207 If all else fails returns empty string
1209 =head2 _create_bib_from_quote
1211 marc_record_obj = _create_bib_from_quote(lineitem, quote)
1213 Returns a MARC::Record object based on the info in the quote's lineitem
1215 =head2 _create_item_from_quote
1217 item_hashref = _create_item_from_quote( lineitem, quote)
1219 returns a hashref representing the item fields specified in the quote
1221 =head2 _get_invoiced_price
1223 _get_invoiced_price(line_object)
1225 Returns the net price or an equivalent calculated from line cost / qty
1227 =head2 _discounted_price
1229 ecost = _discounted_price(discount, item_price)
1231 utility subroutine to return a price calculated from the
1232 vendors discount and quoted price
1234 =head2 _check_for_existing_bib
1236 (biblionumber, biblioitemnumber) = _check_for_existing_bib(isbn_or_ean)
1238 passed an isbn or ean attempts to locate a match bib
1239 On success returns biblionumber and biblioitemnumber
1240 On failure returns undefined/an empty list
1242 =head2 _get_budget
1244 b = _get_budget(schema_obj, budget_code)
1246 Returns the Aqbudget object for the active budget given the passed budget_code
1247 or undefined if one does not exist
1249 =head1 AUTHOR
1251 Colin Campbell <colin.campbell@ptfs-europe.com>
1254 =head1 COPYRIGHT
1256 Copyright 2014,2015 PTFS-Europe Ltd
1257 This program is free software, You may redistribute it under
1258 under the terms of the GNU General Public License
1261 =cut