Bug 22098: Update stocknumberAV cataloguing plugin to use objects
[koha.git] / Koha / EDI.pm
blob5bf0197184ecf360f5a8d438138c933481e8ad74
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 C4::Acquisition qw( NewBasket CloseBasket ModOrder);
31 use C4::Suggestions qw( ModSuggestion );
32 use C4::Biblio qw( AddBiblio TransformKohaToMarc GetMarcBiblio GetFrameworkCode GetMarcFromKohaField );
33 use Koha::Edifact::Order;
34 use Koha::Edifact;
35 use Log::Log4perl;
36 use Text::Unidecode;
37 use Koha::Plugins::Handler;
38 use Koha::Acquisition::Baskets;
39 use Koha::Acquisition::Booksellers;
41 our $VERSION = 1.1;
42 our @EXPORT_OK =
43 qw( process_quote process_invoice process_ordrsp create_edi_order get_edifact_ean );
45 sub create_edi_order {
46 my $parameters = shift;
47 my $basketno = $parameters->{basketno};
48 my $ean = $parameters->{ean};
49 my $branchcode = $parameters->{branchcode};
50 my $noingest = $parameters->{noingest};
51 if ( !$basketno || !$ean ) {
52 carp 'create_edi_order called with no basketno or ean';
53 return;
56 my $schema = Koha::Database->new()->schema();
58 my @orderlines = $schema->resultset('Aqorder')->search(
60 basketno => $basketno,
61 orderstatus => 'new',
63 )->all;
65 if ( !@orderlines ) {
66 carp "No orderlines for basket $basketno";
67 return;
70 my $vendor = $schema->resultset('VendorEdiAccount')->search(
72 vendor_id => $orderlines[0]->basketno->booksellerid->id,
74 )->single;
76 my $ean_search_keys = { ean => $ean, };
77 if ($branchcode) {
78 $ean_search_keys->{branchcode} = $branchcode;
80 my $ean_obj =
81 $schema->resultset('EdifactEan')->search($ean_search_keys)->single;
83 # If no branch specific each can be found, look for a default ean
84 unless ($ean_obj) {
85 $ean_obj = $schema->resultset('EdifactEan')->search(
87 ean => $ean,
88 branchcode => undef,
90 )->single;
93 my $dbh = C4::Context->dbh;
94 my $arr_ref = $dbh->selectcol_arrayref(
95 'select id from edifact_messages where basketno = ? and message_type = \'QUOTE\'',
96 {}, $basketno
98 my $response = @{$arr_ref} ? 1 : 0;
100 my $edifact_order_params = {
101 orderlines => \@orderlines,
102 vendor => $vendor,
103 ean => $ean_obj,
104 is_response => $response,
107 my $edifact;
108 if ( $vendor->plugin ) {
109 $edifact = Koha::Plugins::Handler->run(
111 class => $vendor->plugin,
112 method => 'edifact_order',
113 params => {
114 params => $edifact_order_params,
119 else {
120 $edifact = Koha::Edifact::Order->new($edifact_order_params);
123 return unless $edifact;
125 my $order_file = $edifact->encode();
127 # ingest result
128 if ($order_file) {
129 my $m = unidecode($order_file); # remove diacritics and non-latin chars
130 if ($noingest) { # allows scripts to produce test files
131 return $m;
133 my $order = {
134 message_type => 'ORDERS',
135 raw_msg => $m,
136 vendor_id => $vendor->vendor_id,
137 status => 'Pending',
138 basketno => $basketno,
139 filename => $edifact->filename(),
140 transfer_date => $edifact->msg_date_string(),
141 edi_acct => $vendor->id,
144 $schema->resultset('EdifactMessage')->create($order);
145 return 1;
148 return;
151 sub process_ordrsp {
152 my $response_message = shift;
153 $response_message->status('processing');
154 $response_message->update;
155 my $schema = Koha::Database->new()->schema();
156 my $logger = Log::Log4perl->get_logger();
157 my $vendor_acct;
158 my $edi =
159 Koha::Edifact->new( { transmission => $response_message->raw_msg, } );
160 my $messages = $edi->message_array();
162 if ( @{$messages} ) {
163 foreach my $msg ( @{$messages} ) {
164 my $lines = $msg->lineitems();
165 foreach my $line ( @{$lines} ) {
166 my $ordernumber = $line->ordernumber();
168 # action cancelled:change_requested:no_action:accepted:not_found:recorded
169 my $action = $line->action_notification();
170 if ( $action eq 'cancelled' ) {
171 my $reason = $line->coded_orderline_text();
172 ModOrder(
174 ordernumber => $ordernumber,
175 cancellationreason => $reason,
176 orderstatus => 'cancelled',
177 datecancellationprinted => DateTime->now()->ymd(),
181 else { # record order as due with possible further info
183 my $report = $line->coded_orderline_text();
184 my $date_avail = $line->availability_date();
185 $report ||= q{};
186 if ($date_avail) {
187 $report .= " Available: $date_avail";
189 ModOrder(
191 ordernumber => $ordernumber,
192 suppliers_report => $report,
200 $response_message->status('received');
201 $response_message->update;
202 return;
205 sub process_invoice {
206 my $invoice_message = shift;
207 $invoice_message->status('processing');
208 $invoice_message->update;
209 my $schema = Koha::Database->new()->schema();
210 my $logger = Log::Log4perl->get_logger();
211 my $vendor_acct;
213 my $plugin = $invoice_message->edi_acct()->plugin();
214 my $edi_plugin;
215 if ( $plugin ) {
216 $edi_plugin = Koha::Plugins::Handler->run(
218 class => $plugin,
219 method => 'edifact',
220 params => {
221 invoice_message => $invoice_message,
222 transmission => $invoice_message->raw_msg,
228 my $edi = $edi_plugin ||
229 Koha::Edifact->new( { transmission => $invoice_message->raw_msg, } );
231 my $messages = $edi->message_array();
233 if ( @{$messages} ) {
235 # BGM contains an invoice number
236 foreach my $msg ( @{$messages} ) {
237 my $invoicenumber = $msg->docmsg_number();
238 my $shipmentcharge = $msg->shipment_charge();
239 my $msg_date = $msg->message_date;
240 my $tax_date = $msg->tax_point_date;
241 if ( !defined $tax_date || $tax_date !~ m/^\d{8}/xms ) {
242 $tax_date = $msg_date;
245 my $vendor_ean = $msg->supplier_ean;
246 if ( !defined $vendor_acct || $vendor_ean ne $vendor_acct->san ) {
247 $vendor_acct = $schema->resultset('VendorEdiAccount')->search(
249 san => $vendor_ean,
251 )->single;
253 if ( !$vendor_acct ) {
254 carp
255 "Cannot find vendor with ean $vendor_ean for invoice $invoicenumber in $invoice_message->filename";
256 next;
258 $invoice_message->edi_acct( $vendor_acct->id );
259 $logger->trace("Adding invoice:$invoicenumber");
260 my $new_invoice = $schema->resultset('Aqinvoice')->create(
262 invoicenumber => $invoicenumber,
263 booksellerid => $invoice_message->vendor_id,
264 shipmentdate => $msg_date,
265 billingdate => $tax_date,
266 shipmentcost => $shipmentcharge,
267 shipmentcost_budgetid => $vendor_acct->shipment_budget,
268 message_id => $invoice_message->id,
271 my $invoiceid = $new_invoice->invoiceid;
272 $logger->trace("Added as invoiceno :$invoiceid");
273 my $lines = $msg->lineitems();
275 foreach my $line ( @{$lines} ) {
276 my $ordernumber = $line->ordernumber;
277 $logger->trace( "Receipting order:$ordernumber Qty: ",
278 $line->quantity );
280 my $order = $schema->resultset('Aqorder')->find($ordernumber);
282 # ModReceiveOrder does not validate that $ordernumber exists validate here
283 if ($order) {
285 # check suggestions
286 my $s = $schema->resultset('Suggestion')->search(
288 biblionumber => $order->biblionumber->biblionumber,
290 )->single;
291 if ($s) {
292 ModSuggestion(
294 suggestionid => $s->suggestionid,
295 STATUS => 'AVAILABLE',
300 my $price = _get_invoiced_price($line);
302 if ( $order->quantity > $line->quantity ) {
303 my $ordered = $order->quantity;
305 # part receipt
306 $order->orderstatus('partial');
307 $order->quantity( $ordered - $line->quantity );
308 $order->update;
309 my $received_order = $order->copy(
311 ordernumber => undef,
312 quantity => $line->quantity,
313 quantityreceived => $line->quantity,
314 orderstatus => 'complete',
315 unitprice => $price,
316 invoiceid => $invoiceid,
317 datereceived => $msg_date,
320 transfer_items( $schema, $line, $order,
321 $received_order );
322 receipt_items( $schema, $line,
323 $received_order->ordernumber );
325 else { # simple receipt all copies on order
326 $order->quantityreceived( $line->quantity );
327 $order->datereceived($msg_date);
328 $order->invoiceid($invoiceid);
329 $order->unitprice($price);
330 $order->orderstatus('complete');
331 $order->update;
332 receipt_items( $schema, $line, $ordernumber );
335 else {
336 $logger->error(
337 "No order found for $ordernumber Invoice:$invoicenumber"
339 next;
347 $invoice_message->status('received');
348 $invoice_message->update; # status and basketno link
349 return;
352 sub _get_invoiced_price {
353 my $line = shift;
354 my $price = $line->price_net;
355 if ( !defined $price ) { # no net price so generate it from lineitem amount
356 $price = $line->amt_lineitem;
357 if ( $price and $line->quantity > 1 ) {
358 $price /= $line->quantity; # div line cost by qty
361 return $price;
364 sub receipt_items {
365 my ( $schema, $inv_line, $ordernumber ) = @_;
366 my $logger = Log::Log4perl->get_logger();
367 my $quantity = $inv_line->quantity;
369 # itemnumber is not a foreign key ??? makes this a bit cumbersome
370 my @item_links = $schema->resultset('AqordersItem')->search(
372 ordernumber => $ordernumber,
375 my %branch_map;
376 foreach my $ilink (@item_links) {
377 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
378 if ( !$item ) {
379 my $i = $ilink->itemnumber;
380 $logger->warn(
381 "Cannot find aqorder item for $i :Order:$ordernumber");
382 next;
384 my $b = $item->homebranch->branchcode;
385 if ( !exists $branch_map{$b} ) {
386 $branch_map{$b} = [];
388 push @{ $branch_map{$b} }, $item;
391 # Handling for 'AcqItemSetSubfieldsWhenReceived'
392 my @affects;
393 my $biblionumber;
394 my $itemfield;
395 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
396 @affects = split q{\|},
397 C4::Context->preference("AcqItemSetSubfieldsWhenReceived");
398 if (@affects) {
399 my $order = Koha::Acquisition::Orders->find($ordernumber);
400 $biblionumber = $order->biblionumber;
401 my $frameworkcode = GetFrameworkCode($biblionumber);
402 ($itemfield) = GetMarcFromKohaField( 'items.itemnumber',
403 $frameworkcode );
407 my $gir_occurrence = 0;
408 while ( $gir_occurrence < $quantity ) {
409 my $branch = $inv_line->girfield( 'branch', $gir_occurrence );
410 my $item = shift @{ $branch_map{$branch} };
411 if ($item) {
412 my $barcode = $inv_line->girfield( 'barcode', $gir_occurrence );
413 if ( $barcode && !$item->barcode ) {
414 my $rs = $schema->resultset('Item')->search(
416 barcode => $barcode,
419 if ( $rs->count > 0 ) {
420 $logger->warn("Barcode $barcode is a duplicate");
422 else {
424 $logger->trace("Adding barcode $barcode");
425 $item->barcode($barcode);
429 # Handling for 'AcqItemSetSubfieldsWhenReceived'
430 if (@affects) {
431 my $item_marc = C4::Items::GetMarcItem( $biblionumber, $item->itemnumber );
432 for my $affect (@affects) {
433 my ( $sf, $v ) = split q{=}, $affect, 2;
434 foreach ( $item_marc->field($itemfield) ) {
435 $_->update( $sf => $v );
438 C4::Items::ModItemFromMarc( $item_marc, $biblionumber, $item->itemnumber );
441 $item->update;
443 else {
444 $logger->warn("Unmatched item at branch:$branch");
446 ++$gir_occurrence;
448 return;
452 sub transfer_items {
453 my ( $schema, $inv_line, $order_from, $order_to ) = @_;
455 # Transfer x items from the orig order to a completed partial order
456 my $quantity = $inv_line->quantity;
457 my $gocc = 0;
458 my %mapped_by_branch;
459 while ( $gocc < $quantity ) {
460 my $branch = $inv_line->girfield( 'branch', $gocc );
461 if ( !exists $mapped_by_branch{$branch} ) {
462 $mapped_by_branch{$branch} = 1;
464 else {
465 $mapped_by_branch{$branch}++;
467 ++$gocc;
469 my $logger = Log::Log4perl->get_logger();
470 my $o1 = $order_from->ordernumber;
471 my $o2 = $order_to->ordernumber;
472 $logger->warn("transferring $quantity copies from order $o1 to order $o2");
474 my @item_links = $schema->resultset('AqordersItem')->search(
476 ordernumber => $order_from->ordernumber,
479 foreach my $ilink (@item_links) {
480 my $ino = $ilink->itemnumber;
481 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
482 my $i_branch = $item->homebranch;
483 if ( exists $mapped_by_branch{$i_branch}
484 && $mapped_by_branch{$i_branch} > 0 )
486 $ilink->ordernumber( $order_to->ordernumber );
487 $ilink->update;
488 --$quantity;
489 --$mapped_by_branch{$i_branch};
490 $logger->warn("Transferred item $item");
492 else {
493 $logger->warn("Skipped item $item");
495 if ( $quantity < 1 ) {
496 last;
500 return;
503 sub process_quote {
504 my $quote = shift;
506 $quote->status('processing');
507 $quote->update;
509 my $edi = Koha::Edifact->new( { transmission => $quote->raw_msg, } );
511 my $messages = $edi->message_array();
512 my $process_errors = 0;
513 my $logger = Log::Log4perl->get_logger();
514 my $schema = Koha::Database->new()->schema();
515 my $message_count = 0;
516 my @added_baskets; # if auto & multiple baskets need to order all
518 if ( @{$messages} && $quote->vendor_id ) {
519 foreach my $msg ( @{$messages} ) {
520 ++$message_count;
521 my $basketno =
522 NewBasket( $quote->vendor_id, 0, $quote->filename, q{},
523 q{} . q{} );
524 push @added_baskets, $basketno;
525 if ( $message_count > 1 ) {
526 my $m_filename = $quote->filename;
527 $m_filename .= "_$message_count";
528 $schema->resultset('EdifactMessage')->create(
530 message_type => $quote->message_type,
531 transfer_date => $quote->transfer_date,
532 vendor_id => $quote->vendor_id,
533 edi_acct => $quote->edi_acct,
534 status => 'recmsg',
535 basketno => $basketno,
536 raw_msg => q{},
537 filename => $m_filename,
541 else {
542 $quote->basketno($basketno);
544 $logger->trace("Created basket :$basketno");
545 my $items = $msg->lineitems();
546 my $refnum = $msg->message_refno;
548 for my $item ( @{$items} ) {
549 if ( !quote_item( $item, $quote, $basketno ) ) {
550 ++$process_errors;
555 my $status = 'received';
556 if ($process_errors) {
557 $status = 'error';
560 $quote->status($status);
561 $quote->update; # status and basketno link
562 # Do we automatically generate orders for this vendor
563 my $v = $schema->resultset('VendorEdiAccount')->search(
565 vendor_id => $quote->vendor_id,
567 )->single;
568 if ( $v->auto_orders ) {
569 for my $b (@added_baskets) {
570 create_edi_order(
572 ean => $messages->[0]->buyer_ean,
573 basketno => $b,
576 CloseBasket($b);
580 return;
583 sub quote_item {
584 my ( $item, $quote, $basketno ) = @_;
586 my $schema = Koha::Database->new()->schema();
587 my $logger = Log::Log4perl->get_logger();
589 # $basketno is the return from AddBasket in the calling routine
590 # So this call should not fail unless that has
591 my $basket = Koha::Acquisition::Baskets->find( $basketno );
592 unless ( $basket ) {
593 $logger->error('Skipping order creation no valid basketno');
594 return;
596 $logger->trace( 'Checking db for matches with ', $item->item_number_id() );
597 my $bib = _check_for_existing_bib( $item->item_number_id() );
598 if ( !defined $bib ) {
599 $bib = {};
600 my $bib_record = _create_bib_from_quote( $item, $quote );
601 ( $bib->{biblionumber}, $bib->{biblioitemnumber} ) =
602 AddBiblio( $bib_record, q{} );
603 $logger->trace("New biblio added $bib->{biblionumber}");
605 else {
606 $logger->trace("Match found: $bib->{biblionumber}");
609 # Create an orderline
610 my $order_note = $item->{orderline_free_text};
611 $order_note ||= q{};
612 my $order_quantity = $item->quantity();
613 my $gir_count = $item->number_of_girs();
614 $order_quantity ||= 1; # quantity not necessarily present
615 if ( $gir_count > 1 ) {
616 if ( $gir_count != $order_quantity ) {
617 $logger->error(
618 "Order for $order_quantity items, $gir_count segments present");
620 $order_quantity = 1; # attempts to create an orderline for each gir
622 my $vendor = Koha::Acquisition::Booksellers->find( $quote->vendor_id );
624 # database definitions should set some of these defaults but dont
625 my $order_hash = {
626 biblionumber => $bib->{biblionumber},
627 entrydate => DateTime->now( time_zone => 'local' )->ymd(),
628 basketno => $basketno,
629 listprice => $item->price,
630 quantity => $order_quantity,
631 quantityreceived => 0,
632 order_vendornote => q{},
633 order_internalnote => $order_note,
634 replacementprice => $item->price,
635 rrp_tax_included => $item->price,
636 rrp_tax_excluded => $item->price,
637 ecost => _discounted_price( $quote->vendor->discount, $item->price ),
638 uncertainprice => 0,
639 sort1 => q{},
640 sort2 => q{},
641 currency => $vendor->listprice(),
644 # suppliers references
645 if ( $item->reference() ) {
646 $order_hash->{suppliers_reference_number} = $item->reference;
647 $order_hash->{suppliers_reference_qualifier} = 'QLI';
649 elsif ( $item->orderline_reference_number() ) {
650 $order_hash->{suppliers_reference_number} =
651 $item->orderline_reference_number;
652 $order_hash->{suppliers_reference_qualifier} = 'SLI';
654 if ( $item->item_number_id ) { # suppliers ean
655 $order_hash->{line_item_id} = $item->item_number_id;
658 if ( $item->girfield('servicing_instruction') ) {
659 my $occ = 0;
660 my $txt = q{};
661 my $si;
662 while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
663 if ($occ) {
664 $txt .= q{: };
666 $txt .= $si;
667 ++$occ;
669 $order_hash->{order_vendornote} = $txt;
672 if ( $item->internal_notes() ) {
673 if ( $order_hash->{order_internalnote} ) { # more than ''
674 $order_hash->{order_internalnote} .= q{ };
676 $order_hash->{order_internalnote} .= $item->internal_notes;
679 my $budget = _get_budget( $schema, $item->girfield('fund_allocation') );
681 my $skip = '0';
682 if ( !$budget ) {
683 if ( $item->quantity > 1 ) {
684 carp 'Skipping line with no budget info';
685 $logger->trace('girfield skipped for invalid budget');
686 $skip++;
688 else {
689 carp 'Skipping line with no budget info';
690 $logger->trace('orderline skipped for invalid budget');
691 return;
695 my %ordernumber;
696 my %budgets;
697 my $item_hash;
699 if ( !$skip ) {
700 $order_hash->{budget_id} = $budget->budget_id;
701 my $first_order = $schema->resultset('Aqorder')->create($order_hash);
702 my $o = $first_order->ordernumber();
703 $logger->trace("Order created :$o");
705 # should be done by database settings
706 $first_order->parent_ordernumber( $first_order->ordernumber() );
707 $first_order->update();
709 # add to $budgets to prevent duplicate orderlines
710 $budgets{ $budget->budget_id } = '1';
712 # record ordernumber against budget
713 $ordernumber{ $budget->budget_id } = $o;
715 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
716 $item_hash = _create_item_from_quote( $item, $quote );
718 my $created = 0;
719 while ( $created < $order_quantity ) {
720 $item_hash->{biblionumber} = $bib->{biblionumber};
721 my $item = Koha::Item->new( $item_hash );
722 my $itemnumber = $item->itemnumber;
723 $logger->trace("Added item:$itemnumber");
724 $schema->resultset('AqordersItem')->create(
726 ordernumber => $first_order->ordernumber,
727 itemnumber => $itemnumber,
730 ++$created;
735 if ( $order_quantity == 1 && $item->quantity > 1 ) {
736 my $occurrence = 1; # occ zero already added
737 while ( $occurrence < $item->quantity ) {
739 # check budget code
740 $budget = _get_budget( $schema,
741 $item->girfield( 'fund_allocation', $occurrence ) );
743 if ( !$budget ) {
744 my $bad_budget =
745 $item->girfield( 'fund_allocation', $occurrence );
746 carp 'Skipping line with no budget info';
747 $logger->trace(
748 "girfield skipped for invalid budget:$bad_budget");
749 ++$occurrence; ## lets look at the next one not this one again
750 next;
753 # add orderline for NEW budget in $budgets
754 if ( !exists $budgets{ $budget->budget_id } ) {
756 # $order_hash->{quantity} = 1; by default above
757 # we should handle both 1:1 GIR & 1:n GIR (with LQT values) here
759 $order_hash->{budget_id} = $budget->budget_id;
761 my $new_order =
762 $schema->resultset('Aqorder')->create($order_hash);
763 my $o = $new_order->ordernumber();
764 $logger->trace("Order created :$o");
766 # should be done by database settings
767 $new_order->parent_ordernumber( $new_order->ordernumber() );
768 $new_order->update();
770 # add to $budgets to prevent duplicate orderlines
771 $budgets{ $budget->budget_id } = '1';
773 # record ordernumber against budget
774 $ordernumber{ $budget->budget_id } = $o;
776 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
777 if ( !defined $item_hash ) {
778 $item_hash = _create_item_from_quote( $item, $quote );
780 my $new_item = {
781 itype =>
782 $item->girfield( 'stock_category', $occurrence ),
783 location =>
784 $item->girfield( 'collection_code', $occurrence ),
785 itemcallnumber =>
786 $item->girfield( 'shelfmark', $occurrence )
787 || $item->girfield( 'classification', $occurrence )
788 || title_level_class($item),
789 holdingbranch =>
790 $item->girfield( 'branch', $occurrence ),
791 homebranch => $item->girfield( 'branch', $occurrence ),
793 if ( $new_item->{itype} ) {
794 $item_hash->{itype} = $new_item->{itype};
796 if ( $new_item->{location} ) {
797 $item_hash->{location} = $new_item->{location};
799 if ( $new_item->{itemcallnumber} ) {
800 $item_hash->{itemcallnumber} =
801 $new_item->{itemcallnumber};
803 if ( $new_item->{holdingbranch} ) {
804 $item_hash->{holdingbranch} =
805 $new_item->{holdingbranch};
807 if ( $new_item->{homebranch} ) {
808 $item_hash->{homebranch} = $new_item->{homebranch};
811 $item_hash->{biblionumber} = $bib->{biblionumber};
812 my $item = Koha::Item->new( $item_hash );
813 my $itemnumber = $item->itemnumber;
814 $logger->trace("New item $itemnumber added");
815 $schema->resultset('AqordersItem')->create(
817 ordernumber => $new_order->ordernumber,
818 itemnumber => $itemnumber,
822 my $lrp =
823 $item->girfield( 'library_rotation_plan', $occurrence );
824 if ($lrp) {
825 my $rota =
826 Koha::StockRotationRotas->find( { title => $lrp },
827 { key => 'stockrotationrotas_title' } );
828 if ($rota) {
829 $rota->add_item($itemnumber);
830 $logger->trace("Item added to rota $rota->id");
832 else {
833 $logger->error(
834 "No rota found matching $lrp in orderline");
839 ++$occurrence;
842 # increment quantity in orderline for EXISTING budget in $budgets
843 else {
844 my $row = $schema->resultset('Aqorder')->find(
846 ordernumber => $ordernumber{ $budget->budget_id }
849 if ($row) {
850 my $qty = $row->quantity;
851 $qty++;
852 $row->update(
854 quantity => $qty,
859 # Do not use the basket level value as it is always NULL
860 # See calling subs call to AddBasket
861 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
862 my $new_item = {
863 notforloan => -1,
864 cn_sort => q{},
865 cn_source => 'ddc',
866 price => $item->price,
867 replacementprice => $item->price,
868 itype =>
869 $item->girfield( 'stock_category', $occurrence ),
870 location =>
871 $item->girfield( 'collection_code', $occurrence ),
872 itemcallnumber =>
873 $item->girfield( 'shelfmark', $occurrence )
874 || $item->girfield( 'classification', $occurrence )
875 || $item_hash->{itemcallnumber},
876 holdingbranch =>
877 $item->girfield( 'branch', $occurrence ),
878 homebranch => $item->girfield( 'branch', $occurrence ),
880 $new_item->{biblionumber} = $bib->{biblionumber};
881 my $item = Koha::Item->new( $new_item );
882 my $itemnumber = $item->itemnumber;
883 $logger->trace("New item $itemnumber added");
884 $schema->resultset('AqordersItem')->create(
886 ordernumber => $ordernumber{ $budget->budget_id },
887 itemnumber => $itemnumber,
891 my $lrp =
892 $item->girfield( 'library_rotation_plan', $occurrence );
893 if ($lrp) {
894 my $rota =
895 Koha::StockRotationRotas->find( { title => $lrp },
896 { key => 'stockrotationrotas_title' } );
897 if ($rota) {
898 $rota->add_item($itemnumber);
899 $logger->trace("Item added to rota $rota->id");
901 else {
902 $logger->error(
903 "No rota found matching $lrp in orderline");
908 ++$occurrence;
912 return 1;
916 sub get_edifact_ean {
918 my $dbh = C4::Context->dbh;
920 my $eans = $dbh->selectcol_arrayref('select ean from edifact_ean');
922 return $eans->[0];
925 # We should not need to have a routine to do this here
926 sub _discounted_price {
927 my ( $discount, $price ) = @_;
928 return $price - ( ( $discount * $price ) / 100 );
931 sub _check_for_existing_bib {
932 my $isbn = shift;
934 my $search_isbn = $isbn;
935 $search_isbn =~ s/^\s*/%/xms;
936 $search_isbn =~ s/\s*$/%/xms;
937 my $dbh = C4::Context->dbh;
938 my $sth = $dbh->prepare(
939 'select biblionumber, biblioitemnumber from biblioitems where isbn like ?',
941 my $tuple_arr =
942 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
943 if ( @{$tuple_arr} ) {
944 return $tuple_arr->[0];
946 elsif ( length($isbn) == 13 && $isbn !~ /^97[89]/ ) {
947 my $tarr = $dbh->selectall_arrayref(
948 'select biblionumber, biblioitemnumber from biblioitems where ean = ?',
949 { Slice => {} },
950 $isbn
952 if ( @{$tarr} ) {
953 return $tarr->[0];
956 else {
957 undef $search_isbn;
958 $isbn =~ s/\-//xmsg;
959 if ( $isbn =~ m/(\d{13})/xms ) {
960 my $b_isbn = Business::ISBN->new($1);
961 if ( $b_isbn && $b_isbn->is_valid && $b_isbn->as_isbn10 ) {
962 $search_isbn = $b_isbn->as_isbn10->as_string( [] );
966 elsif ( $isbn =~ m/(\d{9}[xX]|\d{10})/xms ) {
967 my $b_isbn = Business::ISBN->new($1);
968 if ( $b_isbn && $b_isbn->is_valid ) {
969 $search_isbn = $b_isbn->as_isbn13->as_string( [] );
973 if ($search_isbn) {
974 $search_isbn = "%$search_isbn%";
975 $tuple_arr =
976 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
977 if ( @{$tuple_arr} ) {
978 return $tuple_arr->[0];
982 return;
985 # returns a budget obj or undef
986 # fact we need this shows what a mess Acq API is
987 sub _get_budget {
988 my ( $schema, $budget_code ) = @_;
989 my $period_rs = $schema->resultset('Aqbudgetperiod')->search(
991 budget_period_active => 1,
995 # db does not ensure budget code is unque
996 return $schema->resultset('Aqbudget')->single(
998 budget_code => $budget_code,
999 budget_period_id =>
1000 { -in => $period_rs->get_column('budget_period_id')->as_query },
1005 # try to get title level classification from incoming quote
1006 sub title_level_class {
1007 my ($item) = @_;
1008 my $class = q{};
1009 my $default_scheme = C4::Context->preference('DefaultClassificationSource');
1010 if ( $default_scheme eq 'ddc' ) {
1011 $class = $item->dewey_class();
1013 elsif ( $default_scheme eq 'lcc' ) {
1014 $class = $item->lc_class();
1016 if ( !$class ) {
1017 $class =
1018 $item->girfield('shelfmark')
1019 || $item->girfield('classification')
1020 || q{};
1022 return $class;
1025 sub _create_bib_from_quote {
1027 #TBD we should flag this for updating from an external source
1028 #As biblio (&biblioitems) has no candidates flag in order
1029 my ( $item, $quote ) = @_;
1030 my $itemid = $item->item_number_id;
1031 my $defalt_classification_source =
1032 C4::Context->preference('DefaultClassificationSource');
1033 my $bib_hash = {
1034 'biblioitems.cn_source' => $defalt_classification_source,
1035 'items.cn_source' => $defalt_classification_source,
1036 'items.notforloan' => -1,
1037 'items.cn_sort' => q{},
1039 $bib_hash->{'biblio.seriestitle'} = $item->series;
1041 $bib_hash->{'biblioitems.publishercode'} = $item->publisher;
1042 $bib_hash->{'biblioitems.publicationyear'} =
1043 $bib_hash->{'biblio.copyrightdate'} = $item->publication_date;
1045 $bib_hash->{'biblio.title'} = $item->title;
1046 $bib_hash->{'biblio.author'} = $item->author;
1047 $bib_hash->{'biblioitems.isbn'} = $item->item_number_id;
1048 $bib_hash->{'biblioitems.itemtype'} = $item->girfield('stock_category');
1050 # If we have a 13 digit id we are assuming its an ean
1051 # (it may also be an isbn or issn)
1052 if ( $itemid =~ /^\d{13}$/ ) {
1053 $bib_hash->{'biblioitems.ean'} = $itemid;
1054 if ( $itemid =~ /^977/ ) {
1055 $bib_hash->{'biblioitems.issn'} = $itemid;
1058 for my $key ( keys %{$bib_hash} ) {
1059 if ( !defined $bib_hash->{$key} ) {
1060 delete $bib_hash->{$key};
1063 return TransformKohaToMarc($bib_hash);
1067 sub _create_item_from_quote {
1068 my ( $item, $quote ) = @_;
1069 my $defalt_classification_source =
1070 C4::Context->preference('DefaultClassificationSource');
1071 my $item_hash = {
1072 cn_source => $defalt_classification_source,
1073 notforloan => -1,
1074 cn_sort => q{},
1076 $item_hash->{booksellerid} = $quote->vendor_id;
1077 $item_hash->{price} = $item_hash->{replacementprice} = $item->price;
1078 $item_hash->{itype} = $item->girfield('stock_category');
1079 $item_hash->{location} = $item->girfield('collection_code');
1081 my $note = {};
1083 $item_hash->{itemcallnumber} =
1084 $item->girfield('shelfmark')
1085 || $item->girfield('classification')
1086 || title_level_class($item);
1088 my $branch = $item->girfield('branch');
1089 $item_hash->{holdingbranch} = $item_hash->{homebranch} = $branch;
1090 return $item_hash;
1094 __END__
1096 =head1 NAME
1098 Koha::EDI
1100 =head1 SYNOPSIS
1102 Module exporting subroutines used in EDI processing for Koha
1104 =head1 DESCRIPTION
1106 Subroutines called by batch processing to handle Edifact
1107 messages of various types and related utilities
1109 =head1 BUGS
1111 These routines should really be methods of some object.
1112 get_edifact_ean is a stopgap which should be replaced
1114 =head1 SUBROUTINES
1116 =head2 process_quote
1118 process_quote(quote_message);
1120 passed a message object for a quote, parses it creating an order basket
1121 and orderlines in the database
1122 updates the message's status to received in the database and adds the
1123 link to basket
1125 =head2 process_invoice
1127 process_invoice(invoice_message)
1129 passed a message object for an invoice, add the contained invoices
1130 and update the orderlines referred to in the invoice
1131 As an Edifact invoice is in effect a despatch note this receipts the
1132 appropriate quantities in the orders
1134 no meaningful return value
1136 =head2 process_ordrsp
1138 process_ordrsp(ordrsp_message)
1140 passed a message object for a supplier response, process the contents
1141 If an orderline is cancelled cancel the corresponding orderline in koha
1142 otherwise record the supplier message against it
1144 no meaningful return value
1146 =head2 create_edi_order
1148 create_edi_order( { parameter_hashref } )
1150 parameters must include basketno and ean
1152 branchcode can optionally be passed
1154 returns 1 on success undef otherwise
1156 if the parameter noingest is set the formatted order is returned
1157 and not saved in the database. This functionality is intended for debugging only
1159 =head2 receipt_items
1161 receipt_items( schema_obj, invoice_line, ordernumber)
1163 receipts the items recorded on this invoice line
1165 no meaningful return
1167 =head2 transfer_items
1169 transfer_items(schema, invoice_line, originating_order, receiving_order)
1171 Transfer the items covered by this invoice line from their original
1172 order to another order recording the partial fulfillment of the original
1173 order
1175 no meaningful return
1177 =head2 get_edifact_ean
1179 $ean = get_edifact_ean();
1181 routine to return the ean.
1183 =head2 quote_item
1185 quote_item(lineitem, quote_message);
1187 Called by process_quote to handle an individual lineitem
1188 Generate the biblios and items if required and orderline linking to them
1190 Returns 1 on success undef on error
1192 Most usual cause of error is a line with no or incorrect budget codes
1193 which woild cause order creation to abort
1194 If other correct lines exist these are processed and the erroneous line os logged
1196 =head2 title_level_class
1198 classmark = title_level_class(edi_item)
1200 Trys to return a title level classmark from a quote message line
1201 Will return a dewey or lcc classmark if one exists according to the
1202 value in DefaultClassificationSource syspref
1204 If unable to returns the shelfmark or classification from the GIR segment
1206 If all else fails returns empty string
1208 =head2 _create_bib_from_quote
1210 marc_record_obj = _create_bib_from_quote(lineitem, quote)
1212 Returns a MARC::Record object based on the info in the quote's lineitem
1214 =head2 _create_item_from_quote
1216 item_hashref = _create_item_from_quote( lineitem, quote)
1218 returns a hashref representing the item fields specified in the quote
1220 =head2 _get_invoiced_price
1222 _get_invoiced_price(line_object)
1224 Returns the net price or an equivalent calculated from line cost / qty
1226 =head2 _discounted_price
1228 ecost = _discounted_price(discount, item_price)
1230 utility subroutine to return a price calculated from the
1231 vendors discount and quoted price
1233 =head2 _check_for_existing_bib
1235 (biblionumber, biblioitemnumber) = _check_for_existing_bib(isbn_or_ean)
1237 passed an isbn or ean attempts to locate a match bib
1238 On success returns biblionumber and biblioitemnumber
1239 On failure returns undefined/an empty list
1241 =head2 _get_budget
1243 b = _get_budget(schema_obj, budget_code)
1245 Returns the Aqbudget object for the active budget given the passed budget_code
1246 or undefined if one does not exist
1248 =head1 AUTHOR
1250 Colin Campbell <colin.campbell@ptfs-europe.com>
1253 =head1 COPYRIGHT
1255 Copyright 2014,2015 PTFS-Europe Ltd
1256 This program is free software, You may redistribute it under
1257 under the terms of the GNU General Public License
1260 =cut