Bug 2093: (follow-up) Add OPAC dashboard for logged-in users
[koha.git] / Koha / EDI.pm
blobbad68d19951b31807200e7fcc30d17d3f15f03da
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::Items qw(AddItem);
33 use C4::Biblio qw( AddBiblio TransformKohaToMarc GetMarcBiblio );
34 use Koha::Edifact::Order;
35 use Koha::Edifact;
36 use Log::Log4perl;
37 use Text::Unidecode;
38 use Koha::Plugins::Handler;
39 use Koha::Acquisition::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 my $dbh = C4::Context->dbh;
84 my $arr_ref = $dbh->selectcol_arrayref(
85 'select id from edifact_messages where basketno = ? and message_type = \'QUOTE\'',
86 {}, $basketno
88 my $response = @{$arr_ref} ? 1 : 0;
90 my $edifact_order_params = {
91 orderlines => \@orderlines,
92 vendor => $vendor,
93 ean => $ean_obj,
94 is_response => $response,
97 my $edifact;
98 if ( $vendor->plugin ) {
99 $edifact = Koha::Plugins::Handler->run(
101 class => $vendor->plugin,
102 method => 'edifact_order',
103 params => {
104 params => $edifact_order_params,
109 else {
110 $edifact = Koha::Edifact::Order->new($edifact_order_params);
113 return unless $edifact;
115 my $order_file = $edifact->encode();
117 # ingest result
118 if ($order_file) {
119 my $m = unidecode($order_file); # remove diacritics and non-latin chars
120 if ($noingest) { # allows scripts to produce test files
121 return $m;
123 my $order = {
124 message_type => 'ORDERS',
125 raw_msg => $m,
126 vendor_id => $vendor->vendor_id,
127 status => 'Pending',
128 basketno => $basketno,
129 filename => $edifact->filename(),
130 transfer_date => $edifact->msg_date_string(),
131 edi_acct => $vendor->id,
134 $schema->resultset('EdifactMessage')->create($order);
135 return 1;
138 return;
141 sub process_ordrsp {
142 my $response_message = shift;
143 $response_message->status('processing');
144 $response_message->update;
145 my $schema = Koha::Database->new()->schema();
146 my $logger = Log::Log4perl->get_logger();
147 my $vendor_acct;
148 my $edi =
149 Koha::Edifact->new( { transmission => $response_message->raw_msg, } );
150 my $messages = $edi->message_array();
152 if ( @{$messages} ) {
153 foreach my $msg ( @{$messages} ) {
154 my $lines = $msg->lineitems();
155 foreach my $line ( @{$lines} ) {
156 my $ordernumber = $line->ordernumber();
158 # action cancelled:change_requested:no_action:accepted:not_found:recorded
159 my $action = $line->action_notification();
160 if ( $action eq 'cancelled' ) {
161 my $reason = $line->coded_orderline_text();
162 ModOrder(
164 ordernumber => $ordernumber,
165 cancellationreason => $reason,
166 orderstatus => 'cancelled',
167 datecancellationprinted => DateTime->now()->ymd(),
171 else { # record order as due with possible further info
173 my $report = $line->coded_orderline_text();
174 my $date_avail = $line->availability_date();
175 $report ||= q{};
176 if ($date_avail) {
177 $report .= " Available: $date_avail";
179 ModOrder(
181 ordernumber => $ordernumber,
182 suppliers_report => $report,
190 $response_message->status('received');
191 $response_message->update;
192 return;
195 sub process_invoice {
196 my $invoice_message = shift;
197 $invoice_message->status('processing');
198 $invoice_message->update;
199 my $schema = Koha::Database->new()->schema();
200 my $logger = Log::Log4perl->get_logger();
201 my $vendor_acct;
203 my $plugin = $invoice_message->edi_acct()->plugin();
204 my $edi_plugin;
205 if ( $plugin ) {
206 $edi_plugin = Koha::Plugins::Handler->run(
208 class => $plugin,
209 method => 'edifact',
210 params => {
211 invoice_message => $invoice_message,
212 transmission => $invoice_message->raw_msg,
218 my $edi = $edi_plugin ||
219 Koha::Edifact->new( { transmission => $invoice_message->raw_msg, } );
221 my $messages = $edi->message_array();
223 if ( @{$messages} ) {
225 # BGM contains an invoice number
226 foreach my $msg ( @{$messages} ) {
227 my $invoicenumber = $msg->docmsg_number();
228 my $shipmentcharge = $msg->shipment_charge();
229 my $msg_date = $msg->message_date;
230 my $tax_date = $msg->tax_point_date;
231 if ( !defined $tax_date || $tax_date !~ m/^\d{8}/xms ) {
232 $tax_date = $msg_date;
235 my $vendor_ean = $msg->supplier_ean;
236 if ( !defined $vendor_acct || $vendor_ean ne $vendor_acct->san ) {
237 $vendor_acct = $schema->resultset('VendorEdiAccount')->search(
239 san => $vendor_ean,
241 )->single;
243 if ( !$vendor_acct ) {
244 carp
245 "Cannot find vendor with ean $vendor_ean for invoice $invoicenumber in $invoice_message->filename";
246 next;
248 $invoice_message->edi_acct( $vendor_acct->id );
249 $logger->trace("Adding invoice:$invoicenumber");
250 my $new_invoice = $schema->resultset('Aqinvoice')->create(
252 invoicenumber => $invoicenumber,
253 booksellerid => $invoice_message->vendor_id,
254 shipmentdate => $msg_date,
255 billingdate => $tax_date,
256 shipmentcost => $shipmentcharge,
257 shipmentcost_budgetid => $vendor_acct->shipment_budget,
258 message_id => $invoice_message->id,
261 my $invoiceid = $new_invoice->invoiceid;
262 $logger->trace("Added as invoiceno :$invoiceid");
263 my $lines = $msg->lineitems();
265 foreach my $line ( @{$lines} ) {
266 my $ordernumber = $line->ordernumber;
267 $logger->trace( "Receipting order:$ordernumber Qty: ",
268 $line->quantity );
270 my $order = $schema->resultset('Aqorder')->find($ordernumber);
272 # ModReceiveOrder does not validate that $ordernumber exists validate here
273 if ($order) {
275 # check suggestions
276 my $s = $schema->resultset('Suggestion')->search(
278 biblionumber => $order->biblionumber->biblionumber,
280 )->single;
281 if ($s) {
282 ModSuggestion(
284 suggestionid => $s->suggestionid,
285 STATUS => 'AVAILABLE',
290 my $price = _get_invoiced_price($line);
292 if ( $order->quantity > $line->quantity ) {
293 my $ordered = $order->quantity;
295 # part receipt
296 $order->orderstatus('partial');
297 $order->quantity( $ordered - $line->quantity );
298 $order->update;
299 my $received_order = $order->copy(
301 ordernumber => undef,
302 quantity => $line->quantity,
303 quantityreceived => $line->quantity,
304 orderstatus => 'complete',
305 unitprice => $price,
306 invoiceid => $invoiceid,
307 datereceived => $msg_date,
310 transfer_items( $schema, $line, $order,
311 $received_order );
312 receipt_items( $schema, $line,
313 $received_order->ordernumber );
315 else { # simple receipt all copies on order
316 $order->quantityreceived( $line->quantity );
317 $order->datereceived($msg_date);
318 $order->invoiceid($invoiceid);
319 $order->unitprice($price);
320 $order->orderstatus('complete');
321 $order->update;
322 receipt_items( $schema, $line, $ordernumber );
325 else {
326 $logger->error(
327 "No order found for $ordernumber Invoice:$invoicenumber"
329 next;
337 $invoice_message->status('received');
338 $invoice_message->update; # status and basketno link
339 return;
342 sub _get_invoiced_price {
343 my $line = shift;
344 my $price = $line->price_net;
345 if ( !defined $price ) { # no net price so generate it from lineitem amount
346 $price = $line->amt_lineitem;
347 if ( $price and $line->quantity > 1 ) {
348 $price /= $line->quantity; # div line cost by qty
351 return $price;
354 sub receipt_items {
355 my ( $schema, $inv_line, $ordernumber ) = @_;
356 my $logger = Log::Log4perl->get_logger();
357 my $quantity = $inv_line->quantity;
359 # itemnumber is not a foreign key ??? makes this a bit cumbersome
360 my @item_links = $schema->resultset('AqordersItem')->search(
362 ordernumber => $ordernumber,
365 my %branch_map;
366 foreach my $ilink (@item_links) {
367 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
368 if ( !$item ) {
369 my $i = $ilink->itemnumber;
370 $logger->warn(
371 "Cannot find aqorder item for $i :Order:$ordernumber");
372 next;
374 my $b = $item->homebranch->branchcode;
375 if ( !exists $branch_map{$b} ) {
376 $branch_map{$b} = [];
378 push @{ $branch_map{$b} }, $item;
380 my $gir_occurrence = 0;
381 while ( $gir_occurrence < $quantity ) {
382 my $branch = $inv_line->girfield( 'branch', $gir_occurrence );
383 my $item = shift @{ $branch_map{$branch} };
384 if ($item) {
385 my $barcode = $inv_line->girfield( 'barcode', $gir_occurrence );
386 if ( $barcode && !$item->barcode ) {
387 my $rs = $schema->resultset('Item')->search(
389 barcode => $barcode,
392 if ( $rs->count > 0 ) {
393 $logger->warn("Barcode $barcode is a duplicate");
395 else {
397 $logger->trace("Adding barcode $barcode");
398 $item->barcode($barcode);
402 $item->update;
404 else {
405 $logger->warn("Unmatched item at branch:$branch");
407 ++$gir_occurrence;
409 return;
413 sub transfer_items {
414 my ( $schema, $inv_line, $order_from, $order_to ) = @_;
416 # Transfer x items from the orig order to a completed partial order
417 my $quantity = $inv_line->quantity;
418 my $gocc = 0;
419 my %mapped_by_branch;
420 while ( $gocc < $quantity ) {
421 my $branch = $inv_line->girfield( 'branch', $gocc );
422 if ( !exists $mapped_by_branch{$branch} ) {
423 $mapped_by_branch{$branch} = 1;
425 else {
426 $mapped_by_branch{$branch}++;
428 ++$gocc;
430 my $logger = Log::Log4perl->get_logger();
431 my $o1 = $order_from->ordernumber;
432 my $o2 = $order_to->ordernumber;
433 $logger->warn("transferring $quantity copies from order $o1 to order $o2");
435 my @item_links = $schema->resultset('AqordersItem')->search(
437 ordernumber => $order_from->ordernumber,
440 foreach my $ilink (@item_links) {
441 my $ino = $ilink->itemnumber;
442 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
443 my $i_branch = $item->homebranch;
444 if ( exists $mapped_by_branch{$i_branch}
445 && $mapped_by_branch{$i_branch} > 0 )
447 $ilink->ordernumber( $order_to->ordernumber );
448 $ilink->update;
449 --$quantity;
450 --$mapped_by_branch{$i_branch};
451 $logger->warn("Transferred item $item");
453 else {
454 $logger->warn("Skipped item $item");
456 if ( $quantity < 1 ) {
457 last;
461 return;
464 sub process_quote {
465 my $quote = shift;
467 $quote->status('processing');
468 $quote->update;
470 my $edi = Koha::Edifact->new( { transmission => $quote->raw_msg, } );
472 my $messages = $edi->message_array();
473 my $process_errors = 0;
474 my $logger = Log::Log4perl->get_logger();
475 my $schema = Koha::Database->new()->schema();
476 my $message_count = 0;
477 my @added_baskets; # if auto & multiple baskets need to order all
479 if ( @{$messages} && $quote->vendor_id ) {
480 foreach my $msg ( @{$messages} ) {
481 ++$message_count;
482 my $basketno =
483 NewBasket( $quote->vendor_id, 0, $quote->filename, q{},
484 q{} . q{} );
485 push @added_baskets, $basketno;
486 if ( $message_count > 1 ) {
487 my $m_filename = $quote->filename;
488 $m_filename .= "_$message_count";
489 $schema->resultset('EdifactMessage')->create(
491 message_type => $quote->message_type,
492 transfer_date => $quote->transfer_date,
493 vendor_id => $quote->vendor_id,
494 edi_acct => $quote->edi_acct,
495 status => 'recmsg',
496 basketno => $basketno,
497 raw_msg => q{},
498 filename => $m_filename,
502 else {
503 $quote->basketno($basketno);
505 $logger->trace("Created basket :$basketno");
506 my $items = $msg->lineitems();
507 my $refnum = $msg->message_refno;
509 for my $item ( @{$items} ) {
510 if ( !quote_item( $item, $quote, $basketno ) ) {
511 ++$process_errors;
516 my $status = 'received';
517 if ($process_errors) {
518 $status = 'error';
521 $quote->status($status);
522 $quote->update; # status and basketno link
523 # Do we automatically generate orders for this vendor
524 my $v = $schema->resultset('VendorEdiAccount')->search(
526 vendor_id => $quote->vendor_id,
528 )->single;
529 if ( $v->auto_orders ) {
530 for my $b (@added_baskets) {
531 create_edi_order(
534 basketno => $b,
537 CloseBasket($b);
541 return;
544 sub quote_item {
545 my ( $item, $quote, $basketno ) = @_;
547 my $schema = Koha::Database->new()->schema();
549 # create biblio record
550 my $logger = Log::Log4perl->get_logger();
551 if ( !$basketno ) {
552 $logger->error('Skipping order creation no basketno');
553 return;
555 $logger->trace( 'Checking db for matches with ', $item->item_number_id() );
556 my $bib = _check_for_existing_bib( $item->item_number_id() );
557 if ( !defined $bib ) {
558 $bib = {};
559 my $bib_record = _create_bib_from_quote( $item, $quote );
560 ( $bib->{biblionumber}, $bib->{biblioitemnumber} ) =
561 AddBiblio( $bib_record, q{} );
562 $logger->trace("New biblio added $bib->{biblionumber}");
564 else {
565 $logger->trace("Match found: $bib->{biblionumber}");
568 # Create an orderline
569 my $order_note = $item->{orderline_free_text};
570 $order_note ||= q{};
571 my $order_quantity = $item->quantity();
572 my $gir_count = $item->number_of_girs();
573 $order_quantity ||= 1; # quantity not necessarily present
574 if ( $gir_count > 1 ) {
575 if ( $gir_count != $order_quantity ) {
576 $logger->error(
577 "Order for $order_quantity items, $gir_count segments present");
579 $order_quantity = 1; # attempts to create an orderline for each gir
581 my $vendor = Koha::Acquisition::Booksellers->find( $quote->vendor_id );
583 # database definitions should set some of these defaults but dont
584 my $order_hash = {
585 biblionumber => $bib->{biblionumber},
586 entrydate => DateTime->now( time_zone => 'local' )->ymd(),
587 basketno => $basketno,
588 listprice => $item->price,
589 quantity => $order_quantity,
590 quantityreceived => 0,
591 order_vendornote => q{},
592 order_internalnote => $order_note,
593 rrp => $item->price,
594 ecost => _discounted_price( $quote->vendor->discount, $item->price ),
595 uncertainprice => 0,
596 sort1 => q{},
597 sort2 => q{},
598 currency => $vendor->listprice(),
601 # suppliers references
602 if ( $item->reference() ) {
603 $order_hash->{suppliers_reference_number} = $item->reference;
604 $order_hash->{suppliers_reference_qualifier} = 'QLI';
606 elsif ( $item->orderline_reference_number() ) {
607 $order_hash->{suppliers_reference_number} =
608 $item->orderline_reference_number;
609 $order_hash->{suppliers_reference_qualifier} = 'SLI';
611 if ( $item->item_number_id ) { # suppliers ean
612 $order_hash->{line_item_id} = $item->item_number_id;
615 if ( $item->girfield('servicing_instruction') ) {
616 my $occ = 0;
617 my $txt = q{};
618 my $si;
619 while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
620 if ($occ) {
621 $txt .= q{: };
623 $txt .= $si;
624 ++$occ;
626 $order_hash->{order_vendornote} = $txt;
629 if ( $item->internal_notes() ) {
630 if ( $order_hash->{order_internalnote} ) { # more than ''
631 $order_hash->{order_internalnote} .= q{ };
633 $order_hash->{order_internalnote} .= $item->internal_notes;
636 my $budget = _get_budget( $schema, $item->girfield('fund_allocation') );
638 my $skip = '0';
639 if ( !$budget ) {
640 if ( $item->quantity > 1 ) {
641 carp 'Skipping line with no budget info';
642 $logger->trace('girfield skipped for invalid budget');
643 $skip++;
645 else {
646 carp 'Skipping line with no budget info';
647 $logger->trace('orderline skipped for invalid budget');
648 return;
652 my %ordernumber;
653 my %budgets;
654 my $item_hash;
656 if ( !$skip ) {
657 $order_hash->{budget_id} = $budget->budget_id;
658 my $first_order = $schema->resultset('Aqorder')->create($order_hash);
659 my $o = $first_order->ordernumber();
660 $logger->trace("Order created :$o");
662 # should be done by database settings
663 $first_order->parent_ordernumber( $first_order->ordernumber() );
664 $first_order->update();
666 # add to $budgets to prevent duplicate orderlines
667 $budgets{ $budget->budget_id } = '1';
669 # record ordernumber against budget
670 $ordernumber{ $budget->budget_id } = $o;
672 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
673 $item_hash = _create_item_from_quote( $item, $quote );
675 my $created = 0;
676 while ( $created < $order_quantity ) {
677 my $itemnumber;
678 ( $bib->{biblionumber}, $bib->{biblioitemnumber}, $itemnumber )
679 = AddItem( $item_hash, $bib->{biblionumber} );
680 $logger->trace("Added item:$itemnumber");
681 $schema->resultset('AqordersItem')->create(
683 ordernumber => $first_order->ordernumber,
684 itemnumber => $itemnumber,
687 ++$created;
692 if ( $order_quantity == 1 && $item->quantity > 1 ) {
693 my $occurrence = 1; # occ zero already added
694 while ( $occurrence < $item->quantity ) {
696 # check budget code
697 $budget = _get_budget( $schema,
698 $item->girfield( 'fund_allocation', $occurrence ) );
700 if ( !$budget ) {
701 my $bad_budget =
702 $item->girfield( 'fund_allocation', $occurrence );
703 carp 'Skipping line with no budget info';
704 $logger->trace(
705 "girfield skipped for invalid budget:$bad_budget");
706 ++$occurrence; ## lets look at the next one not this one again
707 next;
710 # add orderline for NEW budget in $budgets
711 if ( !exists $budgets{ $budget->budget_id } ) {
713 # $order_hash->{quantity} = 1; by default above
714 # we should handle both 1:1 GIR & 1:n GIR (with LQT values) here
716 $order_hash->{budget_id} = $budget->budget_id;
718 my $new_order =
719 $schema->resultset('Aqorder')->create($order_hash);
720 my $o = $new_order->ordernumber();
721 $logger->trace("Order created :$o");
723 # should be done by database settings
724 $new_order->parent_ordernumber( $new_order->ordernumber() );
725 $new_order->update();
727 # add to $budgets to prevent duplicate orderlines
728 $budgets{ $budget->budget_id } = '1';
730 # record ordernumber against budget
731 $ordernumber{ $budget->budget_id } = $o;
733 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
734 if ( !defined $item_hash ) {
735 $item_hash = _create_item_from_quote( $item, $quote );
737 my $new_item = {
738 itype =>
739 $item->girfield( 'stock_category', $occurrence ),
740 location =>
741 $item->girfield( 'collection_code', $occurrence ),
742 itemcallnumber =>
743 $item->girfield( 'shelfmark', $occurrence )
744 || $item->girfield( 'classification', $occurrence )
745 || title_level_class($item),
746 holdingbranch =>
747 $item->girfield( 'branch', $occurrence ),
748 homebranch => $item->girfield( 'branch', $occurrence ),
750 if ( $new_item->{itype} ) {
751 $item_hash->{itype} = $new_item->{itype};
753 if ( $new_item->{location} ) {
754 $item_hash->{location} = $new_item->{location};
756 if ( $new_item->{itemcallnumber} ) {
757 $item_hash->{itemcallnumber} =
758 $new_item->{itemcallnumber};
760 if ( $new_item->{holdingbranch} ) {
761 $item_hash->{holdingbranch} =
762 $new_item->{holdingbranch};
764 if ( $new_item->{homebranch} ) {
765 $item_hash->{homebranch} = $new_item->{homebranch};
768 my $itemnumber;
769 ( undef, undef, $itemnumber ) =
770 AddItem( $item_hash, $bib->{biblionumber} );
771 $logger->trace("New item $itemnumber added");
772 $schema->resultset('AqordersItem')->create(
774 ordernumber => $new_order->ordernumber,
775 itemnumber => $itemnumber,
780 ++$occurrence;
783 # increment quantity in orderline for EXISTING budget in $budgets
784 else {
785 my $row = $schema->resultset('Aqorder')->find(
787 ordernumber => $ordernumber{ $budget->budget_id }
790 if ($row) {
791 my $qty = $row->quantity;
792 $qty++;
793 $row->update(
795 quantity => $qty,
800 my $basket = Koha::Acquisition::Basket->find( $basketno );
802 if ( $basket->effective_create_item eq 'ordering' ) {
803 my $new_item = {
804 notforloan => -1,
805 cn_sort => q{},
806 cn_source => 'ddc',
807 price => $item->price,
808 replacementprice => $item->price,
809 itype =>
810 $item->girfield( 'stock_category', $occurrence ),
811 location =>
812 $item->girfield( 'collection_code', $occurrence ),
813 itemcallnumber =>
814 $item->girfield( 'shelfmark', $occurrence )
815 || $item->girfield( 'classification', $occurrence )
816 || $item_hash->{itemcallnumber},
817 holdingbranch =>
818 $item->girfield( 'branch', $occurrence ),
819 homebranch => $item->girfield( 'branch', $occurrence ),
821 my $itemnumber;
822 ( undef, undef, $itemnumber ) =
823 AddItem( $new_item, $bib->{biblionumber} );
824 $logger->trace("New item $itemnumber added");
825 $schema->resultset('AqordersItem')->create(
827 ordernumber => $ordernumber{ $budget->budget_id },
828 itemnumber => $itemnumber,
833 ++$occurrence;
837 return 1;
841 sub get_edifact_ean {
843 my $dbh = C4::Context->dbh;
845 my $eans = $dbh->selectcol_arrayref('select ean from edifact_ean');
847 return $eans->[0];
850 # We should not need to have a routine to do this here
851 sub _discounted_price {
852 my ( $discount, $price ) = @_;
853 return $price - ( ( $discount * $price ) / 100 );
856 sub _check_for_existing_bib {
857 my $isbn = shift;
859 my $search_isbn = $isbn;
860 $search_isbn =~ s/^\s*/%/xms;
861 $search_isbn =~ s/\s*$/%/xms;
862 my $dbh = C4::Context->dbh;
863 my $sth = $dbh->prepare(
864 'select biblionumber, biblioitemnumber from biblioitems where isbn like ?',
866 my $tuple_arr =
867 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
868 if ( @{$tuple_arr} ) {
869 return $tuple_arr->[0];
871 elsif ( length($isbn) == 13 && $isbn !~ /^97[89]/ ) {
872 my $tarr = $dbh->selectall_arrayref(
873 'select biblionumber, biblioitemnumber from biblioitems where ean = ?',
874 { Slice => {} },
875 $isbn
877 if ( @{$tarr} ) {
878 return $tarr->[0];
881 else {
882 undef $search_isbn;
883 $isbn =~ s/\-//xmsg;
884 if ( $isbn =~ m/(\d{13})/xms ) {
885 my $b_isbn = Business::ISBN->new($1);
886 if ( $b_isbn && $b_isbn->is_valid ) {
887 $search_isbn = $b_isbn->as_isbn10->as_string( [] );
891 elsif ( $isbn =~ m/(\d{9}[xX]|\d{10})/xms ) {
892 my $b_isbn = Business::ISBN->new($1);
893 if ( $b_isbn && $b_isbn->is_valid ) {
894 $search_isbn = $b_isbn->as_isbn13->as_string( [] );
898 if ($search_isbn) {
899 $search_isbn = "%$search_isbn%";
900 $tuple_arr =
901 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
902 if ( @{$tuple_arr} ) {
903 return $tuple_arr->[0];
907 return;
910 # returns a budget obj or undef
911 # fact we need this shows what a mess Acq API is
912 sub _get_budget {
913 my ( $schema, $budget_code ) = @_;
914 my $period_rs = $schema->resultset('Aqbudgetperiod')->search(
916 budget_period_active => 1,
920 # db does not ensure budget code is unque
921 return $schema->resultset('Aqbudget')->single(
923 budget_code => $budget_code,
924 budget_period_id =>
925 { -in => $period_rs->get_column('budget_period_id')->as_query },
930 # try to get title level classification from incoming quote
931 sub title_level_class {
932 my ($item) = @_;
933 my $class = q{};
934 my $default_scheme = C4::Context->preference('DefaultClassificationSource');
935 if ( $default_scheme eq 'ddc' ) {
936 $class = $item->dewey_class();
938 elsif ( $default_scheme eq 'lcc' ) {
939 $class = $item->lc_class();
941 if ( !$class ) {
942 $class =
943 $item->girfield('shelfmark')
944 || $item->girfield('classification')
945 || q{};
947 return $class;
950 sub _create_bib_from_quote {
952 #TBD we should flag this for updating from an external source
953 #As biblio (&biblioitems) has no candidates flag in order
954 my ( $item, $quote ) = @_;
955 my $itemid = $item->item_number_id;
956 my $defalt_classification_source =
957 C4::Context->preference('DefaultClassificationSource');
958 my $bib_hash = {
959 'biblioitems.cn_source' => $defalt_classification_source,
960 'items.cn_source' => $defalt_classification_source,
961 'items.notforloan' => -1,
962 'items.cn_sort' => q{},
964 $bib_hash->{'biblio.seriestitle'} = $item->series;
966 $bib_hash->{'biblioitems.publishercode'} = $item->publisher;
967 $bib_hash->{'biblioitems.publicationyear'} =
968 $bib_hash->{'biblio.copyrightdate'} = $item->publication_date;
970 $bib_hash->{'biblio.title'} = $item->title;
971 $bib_hash->{'biblio.author'} = $item->author;
972 $bib_hash->{'biblioitems.isbn'} = $item->item_number_id;
973 $bib_hash->{'biblioitems.itemtype'} = $item->girfield('stock_category');
975 # If we have a 13 digit id we are assuming its an ean
976 # (it may also be an isbn or issn)
977 if ( $itemid =~ /^\d{13}$/ ) {
978 $bib_hash->{'biblioitems.ean'} = $itemid;
979 if ( $itemid =~ /^977/ ) {
980 $bib_hash->{'biblioitems.issn'} = $itemid;
983 for my $key ( keys %{$bib_hash} ) {
984 if ( !defined $bib_hash->{$key} ) {
985 delete $bib_hash->{$key};
988 return TransformKohaToMarc($bib_hash);
992 sub _create_item_from_quote {
993 my ( $item, $quote ) = @_;
994 my $defalt_classification_source =
995 C4::Context->preference('DefaultClassificationSource');
996 my $item_hash = {
997 cn_source => $defalt_classification_source,
998 notforloan => -1,
999 cn_sort => q{},
1001 $item_hash->{booksellerid} = $quote->vendor_id;
1002 $item_hash->{price} = $item_hash->{replacementprice} = $item->price;
1003 $item_hash->{itype} = $item->girfield('stock_category');
1004 $item_hash->{location} = $item->girfield('collection_code');
1006 my $note = {};
1008 $item_hash->{itemcallnumber} =
1009 $item->girfield('shelfmark')
1010 || $item->girfield('classification')
1011 || title_level_class($item);
1013 my $branch = $item->girfield('branch');
1014 $item_hash->{holdingbranch} = $item_hash->{homebranch} = $branch;
1015 return $item_hash;
1019 __END__
1021 =head1 NAME
1023 Koha::EDI
1025 =head1 SYNOPSIS
1027 Module exporting subroutines used in EDI processing for Koha
1029 =head1 DESCRIPTION
1031 Subroutines called by batch processing to handle Edifact
1032 messages of various types and related utilities
1034 =head1 BUGS
1036 These routines should really be methods of some object.
1037 get_edifact_ean is a stopgap which should be replaced
1039 =head1 SUBROUTINES
1041 =head2 process_quote
1043 process_quote(quote_message);
1045 passed a message object for a quote, parses it creating an order basket
1046 and orderlines in the database
1047 updates the message's status to received in the database and adds the
1048 link to basket
1050 =head2 process_invoice
1052 process_invoice(invoice_message)
1054 passed a message object for an invoice, add the contained invoices
1055 and update the orderlines referred to in the invoice
1056 As an Edifact invoice is in effect a despatch note this receipts the
1057 appropriate quantities in the orders
1059 no meaningful return value
1061 =head2 process_ordrsp
1063 process_ordrsp(ordrsp_message)
1065 passed a message object for a supplier response, process the contents
1066 If an orderline is cancelled cancel the corresponding orderline in koha
1067 otherwise record the supplier message against it
1069 no meaningful return value
1071 =head2 create_edi_order
1073 create_edi_order( { parameter_hashref } )
1075 parameters must include basketno and ean
1077 branchcode can optionally be passed
1079 returns 1 on success undef otherwise
1081 if the parameter noingest is set the formatted order is returned
1082 and not saved in the database. This functionality is intended for debugging only
1084 =head2 receipt_items
1086 receipt_items( schema_obj, invoice_line, ordernumber)
1088 receipts the items recorded on this invoice line
1090 no meaningful return
1092 =head2 transfer_items
1094 transfer_items(schema, invoice_line, originating_order, receiving_order)
1096 Transfer the items covered by this invoice line from their original
1097 order to another order recording the partial fulfillment of the original
1098 order
1100 no meaningful return
1102 =head2 get_edifact_ean
1104 $ean = get_edifact_ean();
1106 routine to return the ean.
1108 =head2 quote_item
1110 quote_item(lineitem, quote_message);
1112 Called by process_quote to handle an individual lineitem
1113 Generate the biblios and items if required and orderline linking to them
1115 Returns 1 on success undef on error
1117 Most usual cause of error is a line with no or incorrect budget codes
1118 which woild cause order creation to abort
1119 If other correct lines exist these are processed and the erroneous line os logged
1121 =head2 title_level_class
1123 classmark = title_level_class(edi_item)
1125 Trys to return a title level classmark from a quote message line
1126 Will return a dewey or lcc classmark if one exists according to the
1127 value in DefaultClassificationSource syspref
1129 If unable to returns the shelfmark or classification from the GIR segment
1131 If all else fails returns empty string
1133 =head2 _create_bib_from_quote
1135 marc_record_obj = _create_bib_from_quote(lineitem, quote)
1137 Returns a MARC::Record object based on the info in the quote's lineitem
1139 =head2 _create_item_from_quote
1141 item_hashref = _create_item_from_quote( lineitem, quote)
1143 returns a hashref representing the item fields specified in the quote
1145 =head2 _get_invoiced_price
1147 _get_invoiced_price(line_object)
1149 Returns the net price or an equivalent calculated from line cost / qty
1151 =head2 _discounted_price
1153 ecost = _discounted_price(discount, item_price)
1155 utility subroutine to return a price calculated from the
1156 vendors discount and quoted price
1158 =head2 _check_for_existing_bib
1160 (biblionumber, biblioitemnumber) = _check_for_existing_bib(isbn_or_ean)
1162 passed an isbn or ean attempts to locate a match bib
1163 On success returns biblionumber and biblioitemnumber
1164 On failure returns undefined/an empty list
1166 =head2 _get_budget
1168 b = _get_budget(schema_obj, budget_code)
1170 Returns the Aqbudget object for the active budget given the passed budget_code
1171 or undefined if one does not exist
1173 =head1 AUTHOR
1175 Colin Campbell <colin.campbell@ptfs-europe.com>
1178 =head1 COPYRIGHT
1180 Copyright 2014,2015 PTFS-Europe Ltd
1181 This program is free software, You may redistribute it under
1182 under the terms of the GNU General Public License
1185 =cut