Bug 13618: Do not use html filters with KohaSpan
[koha.git] / Koha / EDI.pm
blob4840fe1f5ab042b94b2ac280ff00c6d9f05bca92
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::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 my $dbh = C4::Context->dbh;
85 my $arr_ref = $dbh->selectcol_arrayref(
86 'select id from edifact_messages where basketno = ? and message_type = \'QUOTE\'',
87 {}, $basketno
89 my $response = @{$arr_ref} ? 1 : 0;
91 my $edifact_order_params = {
92 orderlines => \@orderlines,
93 vendor => $vendor,
94 ean => $ean_obj,
95 is_response => $response,
98 my $edifact;
99 if ( $vendor->plugin ) {
100 $edifact = Koha::Plugins::Handler->run(
102 class => $vendor->plugin,
103 method => 'edifact_order',
104 params => {
105 params => $edifact_order_params,
110 else {
111 $edifact = Koha::Edifact::Order->new($edifact_order_params);
114 return unless $edifact;
116 my $order_file = $edifact->encode();
118 # ingest result
119 if ($order_file) {
120 my $m = unidecode($order_file); # remove diacritics and non-latin chars
121 if ($noingest) { # allows scripts to produce test files
122 return $m;
124 my $order = {
125 message_type => 'ORDERS',
126 raw_msg => $m,
127 vendor_id => $vendor->vendor_id,
128 status => 'Pending',
129 basketno => $basketno,
130 filename => $edifact->filename(),
131 transfer_date => $edifact->msg_date_string(),
132 edi_acct => $vendor->id,
135 $schema->resultset('EdifactMessage')->create($order);
136 return 1;
139 return;
142 sub process_ordrsp {
143 my $response_message = shift;
144 $response_message->status('processing');
145 $response_message->update;
146 my $schema = Koha::Database->new()->schema();
147 my $logger = Log::Log4perl->get_logger();
148 my $vendor_acct;
149 my $edi =
150 Koha::Edifact->new( { transmission => $response_message->raw_msg, } );
151 my $messages = $edi->message_array();
153 if ( @{$messages} ) {
154 foreach my $msg ( @{$messages} ) {
155 my $lines = $msg->lineitems();
156 foreach my $line ( @{$lines} ) {
157 my $ordernumber = $line->ordernumber();
159 # action cancelled:change_requested:no_action:accepted:not_found:recorded
160 my $action = $line->action_notification();
161 if ( $action eq 'cancelled' ) {
162 my $reason = $line->coded_orderline_text();
163 ModOrder(
165 ordernumber => $ordernumber,
166 cancellationreason => $reason,
167 orderstatus => 'cancelled',
168 datecancellationprinted => DateTime->now()->ymd(),
172 else { # record order as due with possible further info
174 my $report = $line->coded_orderline_text();
175 my $date_avail = $line->availability_date();
176 $report ||= q{};
177 if ($date_avail) {
178 $report .= " Available: $date_avail";
180 ModOrder(
182 ordernumber => $ordernumber,
183 suppliers_report => $report,
191 $response_message->status('received');
192 $response_message->update;
193 return;
196 sub process_invoice {
197 my $invoice_message = shift;
198 $invoice_message->status('processing');
199 $invoice_message->update;
200 my $schema = Koha::Database->new()->schema();
201 my $logger = Log::Log4perl->get_logger();
202 my $vendor_acct;
204 my $plugin = $invoice_message->edi_acct()->plugin();
205 my $edi_plugin;
206 if ( $plugin ) {
207 $edi_plugin = Koha::Plugins::Handler->run(
209 class => $plugin,
210 method => 'edifact',
211 params => {
212 invoice_message => $invoice_message,
213 transmission => $invoice_message->raw_msg,
219 my $edi = $edi_plugin ||
220 Koha::Edifact->new( { transmission => $invoice_message->raw_msg, } );
222 my $messages = $edi->message_array();
224 if ( @{$messages} ) {
226 # BGM contains an invoice number
227 foreach my $msg ( @{$messages} ) {
228 my $invoicenumber = $msg->docmsg_number();
229 my $shipmentcharge = $msg->shipment_charge();
230 my $msg_date = $msg->message_date;
231 my $tax_date = $msg->tax_point_date;
232 if ( !defined $tax_date || $tax_date !~ m/^\d{8}/xms ) {
233 $tax_date = $msg_date;
236 my $vendor_ean = $msg->supplier_ean;
237 if ( !defined $vendor_acct || $vendor_ean ne $vendor_acct->san ) {
238 $vendor_acct = $schema->resultset('VendorEdiAccount')->search(
240 san => $vendor_ean,
242 )->single;
244 if ( !$vendor_acct ) {
245 carp
246 "Cannot find vendor with ean $vendor_ean for invoice $invoicenumber in $invoice_message->filename";
247 next;
249 $invoice_message->edi_acct( $vendor_acct->id );
250 $logger->trace("Adding invoice:$invoicenumber");
251 my $new_invoice = $schema->resultset('Aqinvoice')->create(
253 invoicenumber => $invoicenumber,
254 booksellerid => $invoice_message->vendor_id,
255 shipmentdate => $msg_date,
256 billingdate => $tax_date,
257 shipmentcost => $shipmentcharge,
258 shipmentcost_budgetid => $vendor_acct->shipment_budget,
259 message_id => $invoice_message->id,
262 my $invoiceid = $new_invoice->invoiceid;
263 $logger->trace("Added as invoiceno :$invoiceid");
264 my $lines = $msg->lineitems();
266 foreach my $line ( @{$lines} ) {
267 my $ordernumber = $line->ordernumber;
268 $logger->trace( "Receipting order:$ordernumber Qty: ",
269 $line->quantity );
271 my $order = $schema->resultset('Aqorder')->find($ordernumber);
273 # ModReceiveOrder does not validate that $ordernumber exists validate here
274 if ($order) {
276 # check suggestions
277 my $s = $schema->resultset('Suggestion')->search(
279 biblionumber => $order->biblionumber->biblionumber,
281 )->single;
282 if ($s) {
283 ModSuggestion(
285 suggestionid => $s->suggestionid,
286 STATUS => 'AVAILABLE',
291 my $price = _get_invoiced_price($line);
293 if ( $order->quantity > $line->quantity ) {
294 my $ordered = $order->quantity;
296 # part receipt
297 $order->orderstatus('partial');
298 $order->quantity( $ordered - $line->quantity );
299 $order->update;
300 my $received_order = $order->copy(
302 ordernumber => undef,
303 quantity => $line->quantity,
304 quantityreceived => $line->quantity,
305 orderstatus => 'complete',
306 unitprice => $price,
307 invoiceid => $invoiceid,
308 datereceived => $msg_date,
311 transfer_items( $schema, $line, $order,
312 $received_order );
313 receipt_items( $schema, $line,
314 $received_order->ordernumber );
316 else { # simple receipt all copies on order
317 $order->quantityreceived( $line->quantity );
318 $order->datereceived($msg_date);
319 $order->invoiceid($invoiceid);
320 $order->unitprice($price);
321 $order->orderstatus('complete');
322 $order->update;
323 receipt_items( $schema, $line, $ordernumber );
326 else {
327 $logger->error(
328 "No order found for $ordernumber Invoice:$invoicenumber"
330 next;
338 $invoice_message->status('received');
339 $invoice_message->update; # status and basketno link
340 return;
343 sub _get_invoiced_price {
344 my $line = shift;
345 my $price = $line->price_net;
346 if ( !defined $price ) { # no net price so generate it from lineitem amount
347 $price = $line->amt_lineitem;
348 if ( $price and $line->quantity > 1 ) {
349 $price /= $line->quantity; # div line cost by qty
352 return $price;
355 sub receipt_items {
356 my ( $schema, $inv_line, $ordernumber ) = @_;
357 my $logger = Log::Log4perl->get_logger();
358 my $quantity = $inv_line->quantity;
360 # itemnumber is not a foreign key ??? makes this a bit cumbersome
361 my @item_links = $schema->resultset('AqordersItem')->search(
363 ordernumber => $ordernumber,
366 my %branch_map;
367 foreach my $ilink (@item_links) {
368 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
369 if ( !$item ) {
370 my $i = $ilink->itemnumber;
371 $logger->warn(
372 "Cannot find aqorder item for $i :Order:$ordernumber");
373 next;
375 my $b = $item->homebranch->branchcode;
376 if ( !exists $branch_map{$b} ) {
377 $branch_map{$b} = [];
379 push @{ $branch_map{$b} }, $item;
381 my $gir_occurrence = 0;
382 while ( $gir_occurrence < $quantity ) {
383 my $branch = $inv_line->girfield( 'branch', $gir_occurrence );
384 my $item = shift @{ $branch_map{$branch} };
385 if ($item) {
386 my $barcode = $inv_line->girfield( 'barcode', $gir_occurrence );
387 if ( $barcode && !$item->barcode ) {
388 my $rs = $schema->resultset('Item')->search(
390 barcode => $barcode,
393 if ( $rs->count > 0 ) {
394 $logger->warn("Barcode $barcode is a duplicate");
396 else {
398 $logger->trace("Adding barcode $barcode");
399 $item->barcode($barcode);
403 $item->update;
405 else {
406 $logger->warn("Unmatched item at branch:$branch");
408 ++$gir_occurrence;
410 return;
414 sub transfer_items {
415 my ( $schema, $inv_line, $order_from, $order_to ) = @_;
417 # Transfer x items from the orig order to a completed partial order
418 my $quantity = $inv_line->quantity;
419 my $gocc = 0;
420 my %mapped_by_branch;
421 while ( $gocc < $quantity ) {
422 my $branch = $inv_line->girfield( 'branch', $gocc );
423 if ( !exists $mapped_by_branch{$branch} ) {
424 $mapped_by_branch{$branch} = 1;
426 else {
427 $mapped_by_branch{$branch}++;
429 ++$gocc;
431 my $logger = Log::Log4perl->get_logger();
432 my $o1 = $order_from->ordernumber;
433 my $o2 = $order_to->ordernumber;
434 $logger->warn("transferring $quantity copies from order $o1 to order $o2");
436 my @item_links = $schema->resultset('AqordersItem')->search(
438 ordernumber => $order_from->ordernumber,
441 foreach my $ilink (@item_links) {
442 my $ino = $ilink->itemnumber;
443 my $item = $schema->resultset('Item')->find( $ilink->itemnumber );
444 my $i_branch = $item->homebranch;
445 if ( exists $mapped_by_branch{$i_branch}
446 && $mapped_by_branch{$i_branch} > 0 )
448 $ilink->ordernumber( $order_to->ordernumber );
449 $ilink->update;
450 --$quantity;
451 --$mapped_by_branch{$i_branch};
452 $logger->warn("Transferred item $item");
454 else {
455 $logger->warn("Skipped item $item");
457 if ( $quantity < 1 ) {
458 last;
462 return;
465 sub process_quote {
466 my $quote = shift;
468 $quote->status('processing');
469 $quote->update;
471 my $edi = Koha::Edifact->new( { transmission => $quote->raw_msg, } );
473 my $messages = $edi->message_array();
474 my $process_errors = 0;
475 my $logger = Log::Log4perl->get_logger();
476 my $schema = Koha::Database->new()->schema();
477 my $message_count = 0;
478 my @added_baskets; # if auto & multiple baskets need to order all
480 if ( @{$messages} && $quote->vendor_id ) {
481 foreach my $msg ( @{$messages} ) {
482 ++$message_count;
483 my $basketno =
484 NewBasket( $quote->vendor_id, 0, $quote->filename, q{},
485 q{} . q{} );
486 push @added_baskets, $basketno;
487 if ( $message_count > 1 ) {
488 my $m_filename = $quote->filename;
489 $m_filename .= "_$message_count";
490 $schema->resultset('EdifactMessage')->create(
492 message_type => $quote->message_type,
493 transfer_date => $quote->transfer_date,
494 vendor_id => $quote->vendor_id,
495 edi_acct => $quote->edi_acct,
496 status => 'recmsg',
497 basketno => $basketno,
498 raw_msg => q{},
499 filename => $m_filename,
503 else {
504 $quote->basketno($basketno);
506 $logger->trace("Created basket :$basketno");
507 my $items = $msg->lineitems();
508 my $refnum = $msg->message_refno;
510 for my $item ( @{$items} ) {
511 if ( !quote_item( $item, $quote, $basketno ) ) {
512 ++$process_errors;
517 my $status = 'received';
518 if ($process_errors) {
519 $status = 'error';
522 $quote->status($status);
523 $quote->update; # status and basketno link
524 # Do we automatically generate orders for this vendor
525 my $v = $schema->resultset('VendorEdiAccount')->search(
527 vendor_id => $quote->vendor_id,
529 )->single;
530 if ( $v->auto_orders ) {
531 for my $b (@added_baskets) {
532 create_edi_order(
535 basketno => $b,
538 CloseBasket($b);
542 return;
545 sub quote_item {
546 my ( $item, $quote, $basketno ) = @_;
548 my $schema = Koha::Database->new()->schema();
549 my $logger = Log::Log4perl->get_logger();
551 my $basket = Koha::Acquisition::Baskets->find( $basketno );
552 unless ( $basket ) {
553 $logger->error('Skipping order creation no valid basketno');
554 return;
556 $logger->trace( 'Checking db for matches with ', $item->item_number_id() );
557 my $bib = _check_for_existing_bib( $item->item_number_id() );
558 if ( !defined $bib ) {
559 $bib = {};
560 my $bib_record = _create_bib_from_quote( $item, $quote );
561 ( $bib->{biblionumber}, $bib->{biblioitemnumber} ) =
562 AddBiblio( $bib_record, q{} );
563 $logger->trace("New biblio added $bib->{biblionumber}");
565 else {
566 $logger->trace("Match found: $bib->{biblionumber}");
569 # Create an orderline
570 my $order_note = $item->{orderline_free_text};
571 $order_note ||= q{};
572 my $order_quantity = $item->quantity();
573 my $gir_count = $item->number_of_girs();
574 $order_quantity ||= 1; # quantity not necessarily present
575 if ( $gir_count > 1 ) {
576 if ( $gir_count != $order_quantity ) {
577 $logger->error(
578 "Order for $order_quantity items, $gir_count segments present");
580 $order_quantity = 1; # attempts to create an orderline for each gir
582 my $vendor = Koha::Acquisition::Booksellers->find( $quote->vendor_id );
584 # database definitions should set some of these defaults but dont
585 my $order_hash = {
586 biblionumber => $bib->{biblionumber},
587 entrydate => DateTime->now( time_zone => 'local' )->ymd(),
588 basketno => $basketno,
589 listprice => $item->price,
590 quantity => $order_quantity,
591 quantityreceived => 0,
592 order_vendornote => q{},
593 order_internalnote => $order_note,
594 replacementprice => $item->price,
595 rrp_tax_included => $item->price,
596 rrp_tax_excluded => $item->price,
597 ecost => _discounted_price( $quote->vendor->discount, $item->price ),
598 uncertainprice => 0,
599 sort1 => q{},
600 sort2 => q{},
601 currency => $vendor->listprice(),
604 # suppliers references
605 if ( $item->reference() ) {
606 $order_hash->{suppliers_reference_number} = $item->reference;
607 $order_hash->{suppliers_reference_qualifier} = 'QLI';
609 elsif ( $item->orderline_reference_number() ) {
610 $order_hash->{suppliers_reference_number} =
611 $item->orderline_reference_number;
612 $order_hash->{suppliers_reference_qualifier} = 'SLI';
614 if ( $item->item_number_id ) { # suppliers ean
615 $order_hash->{line_item_id} = $item->item_number_id;
618 if ( $item->girfield('servicing_instruction') ) {
619 my $occ = 0;
620 my $txt = q{};
621 my $si;
622 while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
623 if ($occ) {
624 $txt .= q{: };
626 $txt .= $si;
627 ++$occ;
629 $order_hash->{order_vendornote} = $txt;
632 if ( $item->internal_notes() ) {
633 if ( $order_hash->{order_internalnote} ) { # more than ''
634 $order_hash->{order_internalnote} .= q{ };
636 $order_hash->{order_internalnote} .= $item->internal_notes;
639 my $budget = _get_budget( $schema, $item->girfield('fund_allocation') );
641 my $skip = '0';
642 if ( !$budget ) {
643 if ( $item->quantity > 1 ) {
644 carp 'Skipping line with no budget info';
645 $logger->trace('girfield skipped for invalid budget');
646 $skip++;
648 else {
649 carp 'Skipping line with no budget info';
650 $logger->trace('orderline skipped for invalid budget');
651 return;
655 my %ordernumber;
656 my %budgets;
657 my $item_hash;
659 if ( !$skip ) {
660 $order_hash->{budget_id} = $budget->budget_id;
661 my $first_order = $schema->resultset('Aqorder')->create($order_hash);
662 my $o = $first_order->ordernumber();
663 $logger->trace("Order created :$o");
665 # should be done by database settings
666 $first_order->parent_ordernumber( $first_order->ordernumber() );
667 $first_order->update();
669 # add to $budgets to prevent duplicate orderlines
670 $budgets{ $budget->budget_id } = '1';
672 # record ordernumber against budget
673 $ordernumber{ $budget->budget_id } = $o;
675 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
676 $item_hash = _create_item_from_quote( $item, $quote );
678 my $created = 0;
679 while ( $created < $order_quantity ) {
680 my $itemnumber;
681 ( $bib->{biblionumber}, $bib->{biblioitemnumber}, $itemnumber )
682 = AddItem( $item_hash, $bib->{biblionumber} );
683 $logger->trace("Added item:$itemnumber");
684 $schema->resultset('AqordersItem')->create(
686 ordernumber => $first_order->ordernumber,
687 itemnumber => $itemnumber,
690 ++$created;
695 if ( $order_quantity == 1 && $item->quantity > 1 ) {
696 my $occurrence = 1; # occ zero already added
697 while ( $occurrence < $item->quantity ) {
699 # check budget code
700 $budget = _get_budget( $schema,
701 $item->girfield( 'fund_allocation', $occurrence ) );
703 if ( !$budget ) {
704 my $bad_budget =
705 $item->girfield( 'fund_allocation', $occurrence );
706 carp 'Skipping line with no budget info';
707 $logger->trace(
708 "girfield skipped for invalid budget:$bad_budget");
709 ++$occurrence; ## lets look at the next one not this one again
710 next;
713 # add orderline for NEW budget in $budgets
714 if ( !exists $budgets{ $budget->budget_id } ) {
716 # $order_hash->{quantity} = 1; by default above
717 # we should handle both 1:1 GIR & 1:n GIR (with LQT values) here
719 $order_hash->{budget_id} = $budget->budget_id;
721 my $new_order =
722 $schema->resultset('Aqorder')->create($order_hash);
723 my $o = $new_order->ordernumber();
724 $logger->trace("Order created :$o");
726 # should be done by database settings
727 $new_order->parent_ordernumber( $new_order->ordernumber() );
728 $new_order->update();
730 # add to $budgets to prevent duplicate orderlines
731 $budgets{ $budget->budget_id } = '1';
733 # record ordernumber against budget
734 $ordernumber{ $budget->budget_id } = $o;
736 if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
737 if ( !defined $item_hash ) {
738 $item_hash = _create_item_from_quote( $item, $quote );
740 my $new_item = {
741 itype =>
742 $item->girfield( 'stock_category', $occurrence ),
743 location =>
744 $item->girfield( 'collection_code', $occurrence ),
745 itemcallnumber =>
746 $item->girfield( 'shelfmark', $occurrence )
747 || $item->girfield( 'classification', $occurrence )
748 || title_level_class($item),
749 holdingbranch =>
750 $item->girfield( 'branch', $occurrence ),
751 homebranch => $item->girfield( 'branch', $occurrence ),
753 if ( $new_item->{itype} ) {
754 $item_hash->{itype} = $new_item->{itype};
756 if ( $new_item->{location} ) {
757 $item_hash->{location} = $new_item->{location};
759 if ( $new_item->{itemcallnumber} ) {
760 $item_hash->{itemcallnumber} =
761 $new_item->{itemcallnumber};
763 if ( $new_item->{holdingbranch} ) {
764 $item_hash->{holdingbranch} =
765 $new_item->{holdingbranch};
767 if ( $new_item->{homebranch} ) {
768 $item_hash->{homebranch} = $new_item->{homebranch};
771 my $itemnumber;
772 ( undef, undef, $itemnumber ) =
773 AddItem( $item_hash, $bib->{biblionumber} );
774 $logger->trace("New item $itemnumber added");
775 $schema->resultset('AqordersItem')->create(
777 ordernumber => $new_order->ordernumber,
778 itemnumber => $itemnumber,
783 ++$occurrence;
786 # increment quantity in orderline for EXISTING budget in $budgets
787 else {
788 my $row = $schema->resultset('Aqorder')->find(
790 ordernumber => $ordernumber{ $budget->budget_id }
793 if ($row) {
794 my $qty = $row->quantity;
795 $qty++;
796 $row->update(
798 quantity => $qty,
803 if ( $basket->effective_create_item eq 'ordering' ) {
804 my $new_item = {
805 notforloan => -1,
806 cn_sort => q{},
807 cn_source => 'ddc',
808 price => $item->price,
809 replacementprice => $item->price,
810 itype =>
811 $item->girfield( 'stock_category', $occurrence ),
812 location =>
813 $item->girfield( 'collection_code', $occurrence ),
814 itemcallnumber =>
815 $item->girfield( 'shelfmark', $occurrence )
816 || $item->girfield( 'classification', $occurrence )
817 || $item_hash->{itemcallnumber},
818 holdingbranch =>
819 $item->girfield( 'branch', $occurrence ),
820 homebranch => $item->girfield( 'branch', $occurrence ),
822 my $itemnumber;
823 ( undef, undef, $itemnumber ) =
824 AddItem( $new_item, $bib->{biblionumber} );
825 $logger->trace("New item $itemnumber added");
826 $schema->resultset('AqordersItem')->create(
828 ordernumber => $ordernumber{ $budget->budget_id },
829 itemnumber => $itemnumber,
834 ++$occurrence;
838 return 1;
842 sub get_edifact_ean {
844 my $dbh = C4::Context->dbh;
846 my $eans = $dbh->selectcol_arrayref('select ean from edifact_ean');
848 return $eans->[0];
851 # We should not need to have a routine to do this here
852 sub _discounted_price {
853 my ( $discount, $price ) = @_;
854 return $price - ( ( $discount * $price ) / 100 );
857 sub _check_for_existing_bib {
858 my $isbn = shift;
860 my $search_isbn = $isbn;
861 $search_isbn =~ s/^\s*/%/xms;
862 $search_isbn =~ s/\s*$/%/xms;
863 my $dbh = C4::Context->dbh;
864 my $sth = $dbh->prepare(
865 'select biblionumber, biblioitemnumber from biblioitems where isbn like ?',
867 my $tuple_arr =
868 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
869 if ( @{$tuple_arr} ) {
870 return $tuple_arr->[0];
872 elsif ( length($isbn) == 13 && $isbn !~ /^97[89]/ ) {
873 my $tarr = $dbh->selectall_arrayref(
874 'select biblionumber, biblioitemnumber from biblioitems where ean = ?',
875 { Slice => {} },
876 $isbn
878 if ( @{$tarr} ) {
879 return $tarr->[0];
882 else {
883 undef $search_isbn;
884 $isbn =~ s/\-//xmsg;
885 if ( $isbn =~ m/(\d{13})/xms ) {
886 my $b_isbn = Business::ISBN->new($1);
887 if ( $b_isbn && $b_isbn->is_valid ) {
888 $search_isbn = $b_isbn->as_isbn10->as_string( [] );
892 elsif ( $isbn =~ m/(\d{9}[xX]|\d{10})/xms ) {
893 my $b_isbn = Business::ISBN->new($1);
894 if ( $b_isbn && $b_isbn->is_valid ) {
895 $search_isbn = $b_isbn->as_isbn13->as_string( [] );
899 if ($search_isbn) {
900 $search_isbn = "%$search_isbn%";
901 $tuple_arr =
902 $dbh->selectall_arrayref( $sth, { Slice => {} }, $search_isbn );
903 if ( @{$tuple_arr} ) {
904 return $tuple_arr->[0];
908 return;
911 # returns a budget obj or undef
912 # fact we need this shows what a mess Acq API is
913 sub _get_budget {
914 my ( $schema, $budget_code ) = @_;
915 my $period_rs = $schema->resultset('Aqbudgetperiod')->search(
917 budget_period_active => 1,
921 # db does not ensure budget code is unque
922 return $schema->resultset('Aqbudget')->single(
924 budget_code => $budget_code,
925 budget_period_id =>
926 { -in => $period_rs->get_column('budget_period_id')->as_query },
931 # try to get title level classification from incoming quote
932 sub title_level_class {
933 my ($item) = @_;
934 my $class = q{};
935 my $default_scheme = C4::Context->preference('DefaultClassificationSource');
936 if ( $default_scheme eq 'ddc' ) {
937 $class = $item->dewey_class();
939 elsif ( $default_scheme eq 'lcc' ) {
940 $class = $item->lc_class();
942 if ( !$class ) {
943 $class =
944 $item->girfield('shelfmark')
945 || $item->girfield('classification')
946 || q{};
948 return $class;
951 sub _create_bib_from_quote {
953 #TBD we should flag this for updating from an external source
954 #As biblio (&biblioitems) has no candidates flag in order
955 my ( $item, $quote ) = @_;
956 my $itemid = $item->item_number_id;
957 my $defalt_classification_source =
958 C4::Context->preference('DefaultClassificationSource');
959 my $bib_hash = {
960 'biblioitems.cn_source' => $defalt_classification_source,
961 'items.cn_source' => $defalt_classification_source,
962 'items.notforloan' => -1,
963 'items.cn_sort' => q{},
965 $bib_hash->{'biblio.seriestitle'} = $item->series;
967 $bib_hash->{'biblioitems.publishercode'} = $item->publisher;
968 $bib_hash->{'biblioitems.publicationyear'} =
969 $bib_hash->{'biblio.copyrightdate'} = $item->publication_date;
971 $bib_hash->{'biblio.title'} = $item->title;
972 $bib_hash->{'biblio.author'} = $item->author;
973 $bib_hash->{'biblioitems.isbn'} = $item->item_number_id;
974 $bib_hash->{'biblioitems.itemtype'} = $item->girfield('stock_category');
976 # If we have a 13 digit id we are assuming its an ean
977 # (it may also be an isbn or issn)
978 if ( $itemid =~ /^\d{13}$/ ) {
979 $bib_hash->{'biblioitems.ean'} = $itemid;
980 if ( $itemid =~ /^977/ ) {
981 $bib_hash->{'biblioitems.issn'} = $itemid;
984 for my $key ( keys %{$bib_hash} ) {
985 if ( !defined $bib_hash->{$key} ) {
986 delete $bib_hash->{$key};
989 return TransformKohaToMarc($bib_hash);
993 sub _create_item_from_quote {
994 my ( $item, $quote ) = @_;
995 my $defalt_classification_source =
996 C4::Context->preference('DefaultClassificationSource');
997 my $item_hash = {
998 cn_source => $defalt_classification_source,
999 notforloan => -1,
1000 cn_sort => q{},
1002 $item_hash->{booksellerid} = $quote->vendor_id;
1003 $item_hash->{price} = $item_hash->{replacementprice} = $item->price;
1004 $item_hash->{itype} = $item->girfield('stock_category');
1005 $item_hash->{location} = $item->girfield('collection_code');
1007 my $note = {};
1009 $item_hash->{itemcallnumber} =
1010 $item->girfield('shelfmark')
1011 || $item->girfield('classification')
1012 || title_level_class($item);
1014 my $branch = $item->girfield('branch');
1015 $item_hash->{holdingbranch} = $item_hash->{homebranch} = $branch;
1016 return $item_hash;
1020 __END__
1022 =head1 NAME
1024 Koha::EDI
1026 =head1 SYNOPSIS
1028 Module exporting subroutines used in EDI processing for Koha
1030 =head1 DESCRIPTION
1032 Subroutines called by batch processing to handle Edifact
1033 messages of various types and related utilities
1035 =head1 BUGS
1037 These routines should really be methods of some object.
1038 get_edifact_ean is a stopgap which should be replaced
1040 =head1 SUBROUTINES
1042 =head2 process_quote
1044 process_quote(quote_message);
1046 passed a message object for a quote, parses it creating an order basket
1047 and orderlines in the database
1048 updates the message's status to received in the database and adds the
1049 link to basket
1051 =head2 process_invoice
1053 process_invoice(invoice_message)
1055 passed a message object for an invoice, add the contained invoices
1056 and update the orderlines referred to in the invoice
1057 As an Edifact invoice is in effect a despatch note this receipts the
1058 appropriate quantities in the orders
1060 no meaningful return value
1062 =head2 process_ordrsp
1064 process_ordrsp(ordrsp_message)
1066 passed a message object for a supplier response, process the contents
1067 If an orderline is cancelled cancel the corresponding orderline in koha
1068 otherwise record the supplier message against it
1070 no meaningful return value
1072 =head2 create_edi_order
1074 create_edi_order( { parameter_hashref } )
1076 parameters must include basketno and ean
1078 branchcode can optionally be passed
1080 returns 1 on success undef otherwise
1082 if the parameter noingest is set the formatted order is returned
1083 and not saved in the database. This functionality is intended for debugging only
1085 =head2 receipt_items
1087 receipt_items( schema_obj, invoice_line, ordernumber)
1089 receipts the items recorded on this invoice line
1091 no meaningful return
1093 =head2 transfer_items
1095 transfer_items(schema, invoice_line, originating_order, receiving_order)
1097 Transfer the items covered by this invoice line from their original
1098 order to another order recording the partial fulfillment of the original
1099 order
1101 no meaningful return
1103 =head2 get_edifact_ean
1105 $ean = get_edifact_ean();
1107 routine to return the ean.
1109 =head2 quote_item
1111 quote_item(lineitem, quote_message);
1113 Called by process_quote to handle an individual lineitem
1114 Generate the biblios and items if required and orderline linking to them
1116 Returns 1 on success undef on error
1118 Most usual cause of error is a line with no or incorrect budget codes
1119 which woild cause order creation to abort
1120 If other correct lines exist these are processed and the erroneous line os logged
1122 =head2 title_level_class
1124 classmark = title_level_class(edi_item)
1126 Trys to return a title level classmark from a quote message line
1127 Will return a dewey or lcc classmark if one exists according to the
1128 value in DefaultClassificationSource syspref
1130 If unable to returns the shelfmark or classification from the GIR segment
1132 If all else fails returns empty string
1134 =head2 _create_bib_from_quote
1136 marc_record_obj = _create_bib_from_quote(lineitem, quote)
1138 Returns a MARC::Record object based on the info in the quote's lineitem
1140 =head2 _create_item_from_quote
1142 item_hashref = _create_item_from_quote( lineitem, quote)
1144 returns a hashref representing the item fields specified in the quote
1146 =head2 _get_invoiced_price
1148 _get_invoiced_price(line_object)
1150 Returns the net price or an equivalent calculated from line cost / qty
1152 =head2 _discounted_price
1154 ecost = _discounted_price(discount, item_price)
1156 utility subroutine to return a price calculated from the
1157 vendors discount and quoted price
1159 =head2 _check_for_existing_bib
1161 (biblionumber, biblioitemnumber) = _check_for_existing_bib(isbn_or_ean)
1163 passed an isbn or ean attempts to locate a match bib
1164 On success returns biblionumber and biblioitemnumber
1165 On failure returns undefined/an empty list
1167 =head2 _get_budget
1169 b = _get_budget(schema_obj, budget_code)
1171 Returns the Aqbudget object for the active budget given the passed budget_code
1172 or undefined if one does not exist
1174 =head1 AUTHOR
1176 Colin Campbell <colin.campbell@ptfs-europe.com>
1179 =head1 COPYRIGHT
1181 Copyright 2014,2015 PTFS-Europe Ltd
1182 This program is free software, You may redistribute it under
1183 under the terms of the GNU General Public License
1186 =cut