1 package Koha::Edifact::Order;
7 # Copyright 2014,2015 PTFS-Europe Ltd
9 # This file is part of Koha.
11 # Koha is free software; you can redistribute it and/or modify it
12 # under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
16 # Koha is distributed in the hope that it will be useful, but
17 # WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with Koha; if not, see <http://www.gnu.org/licenses>.
30 use C4::Budgets qw( GetBudget );
32 use Koha::Acquisition::Orders;
34 Readonly::Scalar my $seg_terminator => q{'};
35 Readonly::Scalar my $separator => q{+};
36 Readonly::Scalar my $component_separator => q{:};
37 Readonly::Scalar my $release_character => q{?};
39 Readonly::Scalar my $NINES_12 => 999_999_999_999;
40 Readonly::Scalar my $NINES_14 => 99_999_999_999_999;
41 Readonly::Scalar my $CHUNKSIZE => 35;
44 my ( $class, $parameter_hashref ) = @_;
47 if ( ref $parameter_hashref ) {
48 $self->{orderlines} = $parameter_hashref->{orderlines};
49 $self->{recipient} = $parameter_hashref->{vendor};
50 $self->{sender} = $parameter_hashref->{ean};
51 $self->{is_response} = $parameter_hashref->{is_response};
54 $self->{basket} = $self->{orderlines}->[0]->basketno;
55 $self->{message_date} = dt_from_string();
58 # validate that its worth proceeding
59 if ( !$self->{orderlines} ) {
60 carp 'No orderlines passed to create order';
63 if ( !$self->{recipient} ) {
64 carp 'No vendor passed to order creation: basket = '
65 . $self->{basket}->basketno;
68 if ( !$self->{sender} ) {
69 carp 'No sender ean passed to order creation: basket = '
70 . $self->{basket}->basketno;
74 # do this once per object not once per orderline
75 my $database = Koha::Database->new();
76 $self->{schema} = $database->schema;
84 if ( !$self->{orderlines} ) {
87 my $filename = 'ordr' . $self->{basket}->basketno;
95 $self->{interchange_control_reference} = int rand($NINES_14);
96 $self->{message_count} = 0;
98 # $self->{segs}; # Message segments
100 $self->{transmission} = q{};
102 $self->{transmission} .= $self->initial_service_segments();
104 $self->{transmission} .= $self->user_data_message_segments();
106 $self->{transmission} .= $self->trailing_service_segments();
107 return $self->{transmission};
110 sub msg_date_string {
112 return $self->{message_date}->ymd();
115 sub initial_service_segments {
118 #UNA service string advice - specifies standard separators
119 my $segs = _const('service_string_advice');
121 #UNB interchange header
122 $segs .= $self->interchange_header();
124 #UNG functional group header NOT USED
128 sub interchange_header {
133 'UNB+UNOC:3'; # controlling agency character set syntax version number
135 $hdr .= _interchange_sr_identifier( $self->{sender}->ean,
136 $self->{sender}->id_code_qualifier ); # interchange sender
137 $hdr .= _interchange_sr_identifier( $self->{recipient}->san,
138 $self->{recipient}->id_code_qualifier ); # interchange Recipient
142 # DateTime of preparation
143 $hdr .= $self->{message_date}->format_cldr('yyMMdd:HHmm');
145 $hdr .= $self->interchange_control_reference();
148 # Recipents reference password not usually used in edifact
149 $hdr .= q{+ORDERS}; # application reference
151 #Edifact does not usually include the following
152 # $hdr .= $separator; # Processing priority not usually used in edifact
153 # $hdr .= $separator; # Acknowledgewment request : not usually used in edifact
154 # $hdr .= q{+EANCOM} # Communications agreement id
155 # $hdr .= q{+1} # Test indicator
157 $hdr .= $seg_terminator;
161 sub user_data_message_segments {
164 #UNH message_header :: seg count begins here
165 $self->message_header();
167 $self->order_msg_header();
170 foreach my $ol ( @{ $self->{orderlines} } ) {
172 $self->order_line( $line_number, $ol );
175 $self->message_trailer();
177 my $data_segment_string = join q{}, @{ $self->{segs} };
178 return $data_segment_string;
181 sub message_trailer {
184 # terminate the message
185 $self->add_seg("UNS+S$seg_terminator");
188 # Could be (code 1) total value of QTY segments
189 # or ( code = 2 ) number of lineitems
190 my $num_orderlines = @{ $self->{orderlines} };
191 $self->add_seg("CNT+2:$num_orderlines$seg_terminator");
193 # UNT Message Trailer
194 my $segments_in_message =
195 1 + @{ $self->{segs} }; # count incl UNH & UNT (!!this one)
196 my $reference = $self->message_reference('current');
197 $self->add_seg("UNT+$segments_in_message+$reference$seg_terminator");
201 sub trailing_service_segments {
205 #UNE functional group trailer NOT USED
206 #UNZ interchange trailer
207 $trailer .= $self->interchange_trailer();
212 sub interchange_control_reference {
214 if ( $self->{interchange_control_reference} ) {
215 return sprintf '%014d', $self->{interchange_control_reference};
218 carp 'calling for ref of unencoded order';
219 return 'NONE ASSIGNED';
223 sub message_reference {
224 my ( $self, $function ) = @_;
225 if ( $function eq 'new' || !$self->{message_reference_no} ) {
227 # unique 14 char mesage ref
228 $self->{message_reference_no} = sprintf 'ME%012d', int rand($NINES_12);
230 return $self->{message_reference_no};
236 $self->{segs} = []; # initialize the message
237 $self->{message_count}++; # In practice alwaya 1
239 my $hdr = q{UNH+} . $self->message_reference('new');
240 $hdr .= _const('message_identifier');
241 $self->add_seg($hdr);
245 sub interchange_trailer {
248 my $t = "UNZ+$self->{message_count}+";
249 $t .= $self->interchange_control_reference;
250 $t .= $seg_terminator;
254 sub order_msg_header {
258 # UNH see message_header
261 beginning_of_message(
262 $self->{basket}->basketno,
263 $self->{recipient}->san,
268 push @header, message_date_segment( $self->{message_date} );
270 # NAD-RFF buyer supplier ids
274 $self->{sender}->ean,
275 $self->{sender}->id_code_qualifier
280 $self->{recipient}->san,
281 $self->{recipient}->id_code_qualifier
284 # repeat for for other relevant parties
287 # ISO 4217 code to show default currency prices are quoted in
289 # TBD currency handling
291 $self->add_seg(@header);
295 sub beginning_of_message {
296 my $basketno = shift;
297 my $supplier_san = shift;
298 my $response = shift;
299 my $document_message_no = sprintf '%011d', $basketno;
301 # Peters & Bolinda use the BIC recommendation to use 22V a code not in Edifact
302 # If the order is in response to a quote
304 '5013546025065' => 'Peters',
305 '9377779308820' => 'Bolinda',
308 # my $message_function = 9; # original 7 = retransmission
309 # message_code values
312 # 228 sample order :: order for approval / inspection copies
313 # 22C continuation order for volumes in a set etc.
314 # my $message_code = '220';
315 if ( exists $bic_sans{$supplier_san} && $response ) {
316 return "BGM+22V+$document_message_no+9$seg_terminator";
319 return "BGM+220+$document_message_no+9$seg_terminator";
322 sub name_and_address {
323 my ( $party, $id_code, $id_agency ) = @_;
324 my %qualifier_code = (
326 DELIVERY => 'DP', # delivery location if != buyer
327 INVOICEE => 'IV', # if different from buyer
330 if ( !exists $qualifier_code{$party} ) {
331 carp "No qualifier code for $party";
334 if ( $id_agency eq '14' ) {
335 $id_agency = '9'; # ean coded differently in this seg
338 return "NAD+$qualifier_code{$party}+${id_code}::$id_agency$seg_terminator";
342 my ( $self, $linenumber, $orderline ) = @_;
344 my $basket = Koha::Acquisition::Orders->find( $orderline->ordernumber )->basket;
346 my $schema = $self->{schema};
347 if ( !$orderline->biblionumber )
348 { # cannot generate an orderline without a bib record
351 my $biblionumber = $orderline->biblionumber->biblionumber;
352 my @biblioitems = $schema->resultset('Biblioitem')
353 ->search( { biblionumber => $biblionumber, } );
354 my $biblioitem = $biblioitems[0]; # makes the assumption there is 1 only
355 # or else all have same details
357 my $id_string = $orderline->line_item_id;
359 # LIN line-number in msg :: if we had a 13 digit ean we could add
360 $self->add_seg( lin_segment( $linenumber, $id_string ) );
362 # PIA isbn or other id
364 foreach my $id ( $biblioitem->ean, $biblioitem->issn, $biblioitem->isbn ) {
365 if ( $id && $id ne $id_string ) {
366 push @identifiers, $id;
369 $self->add_seg( additional_product_id( join( ' ', @identifiers ) ) );
372 $self->add_seg( item_description( $orderline->biblionumber, $biblioitem ) );
375 my $qty = join q{}, 'QTY+21:', $orderline->quantity, $seg_terminator;
376 $self->add_seg($qty);
378 # DTM Optional date constraints on delivery
379 # we dont currently support this in koha
380 # GIR copy-related data
382 if ( $basket->effective_create_items eq 'ordering' ) {
383 my @linked_itemnumbers = $orderline->aqorders_items;
385 foreach my $item (@linked_itemnumbers) {
386 my $i_obj = $schema->resultset('Item')->find( $item->itemnumber );
387 if ( defined $i_obj ) {
394 itemtype => $biblioitem->itemtype,
395 shelfmark => $biblioitem->cn_class,
397 my $branch = $orderline->basketno->deliveryplace;
399 $item_hash->{branch} = $branch;
401 for ( 1 .. $orderline->quantity ) {
402 push @items, $item_hash;
405 my $budget = GetBudget( $orderline->budget_id );
406 my $ol_fields = { budget_code => $budget->{budget_code}, };
407 if ( $orderline->order_vendornote ) {
408 $ol_fields->{servicing_instruction} = $orderline->order_vendornote;
414 ol_fields => $ol_fields,
420 # TBD what if #items exceeds quantity
422 # FTX free text for current orderline TBD
423 # dont really have a special instructions field to encode here
425 # PRI-CUX-DTM unit price on which order is placed : optional
426 # Coutts read this as 0.00 if not present
427 if ( $orderline->listprice ) {
428 my $price = sprintf 'PRI+AAE:%.2f:CA', $orderline->listprice;
429 $price .= $seg_terminator;
430 $self->add_seg($price);
433 # RFF unique orderline reference no
434 my $rff = join q{}, 'RFF+LI:', $orderline->ordernumber, $seg_terminator;
435 $self->add_seg($rff);
437 # RFF : suppliers unique quotation reference number
438 if ( $orderline->suppliers_reference_number ) {
439 $rff = join q{}, 'RFF+', $orderline->suppliers_reference_qualifier,
440 ':', $orderline->suppliers_reference_number, $seg_terminator;
441 $self->add_seg($rff);
444 # LOC-QTY multiple delivery locations
445 #TBD to specify extra delivery locs
446 # NAD order line name and address
447 #TBD Optionally indicate a name & address or order originator
448 # TDT method of delivey ol-specific
449 # TBD requests a special delivery option
454 sub item_description {
455 my ( $bib, $biblioitem ) = @_;
457 author => $bib->author,
458 title => $bib->title,
459 publisher => $biblioitem->publishercode,
460 year => $biblioitem->publicationyear,
468 # 100 Edition statement
469 # 109 Publisher :: publisher
471 # 170 Date of publication :: year
472 # 220 Binding :: binding
480 for my $field (qw(author title publisher year binding )) {
481 if ( $bib_desc->{$field} ) {
482 my $data = encode_text( $bib_desc->{$field} );
483 push @itm, imd_segment( $code{$field}, $data );
491 my ( $code, $data ) = @_;
493 my $seg_prefix = "IMD+L+$code+:::";
497 while ( my $x = substr $data, 0, $CHUNKSIZE, q{} ) {
498 if ( length $x == $CHUNKSIZE ) {
499 if ( $x =~ s/([?]{1,2})$// ) {
500 $data = "$1$data"; # dont breakup ?' ?? etc
507 foreach my $c (@chunks) {
509 push @segs, "$seg_prefix$c";
512 $segs[-1] .= ":$c$seg_terminator";
516 if ( @segs && $segs[-1] !~ m/[^?]$seg_terminator$/o ) {
517 $segs[-1] .= $seg_terminator;
525 my $basket = $params->{basket};
526 my $orderfields = $params->{ol_fields};
527 my @onorderitems = @{ $params->{items} };
529 my $budget_code = $orderfields->{budget_code};
532 foreach my $item (@onorderitems) {
533 my $seg = sprintf 'GIR+%03d', $sequence_no;
534 $seg .= add_gir_identity_number( 'LFN', $budget_code );
535 if ( $basket->effective_create_items eq 'ordering' ) {
537 add_gir_identity_number( 'LLO', $item->homebranch->branchcode );
538 $seg .= add_gir_identity_number( 'LST', $item->itype );
539 $seg .= add_gir_identity_number( 'LSQ', $item->location );
540 $seg .= add_gir_identity_number( 'LSM', $item->itemcallnumber );
542 # itemcallnumber -> shelfmark
545 if ( $item->{branch} ) {
546 $seg .= add_gir_identity_number( 'LLO', $item->{branch} );
548 $seg .= add_gir_identity_number( 'LST', $item->{itemtype} );
549 $seg .= add_gir_identity_number( 'LSM', $item->{shelfmark} );
551 if ( $orderfields->{servicing_instruction} ) {
552 $seg .= add_gir_identity_number( 'LVT',
553 $orderfields->{servicing_instruction} );
556 push @segments, $seg;
561 sub add_gir_identity_number {
562 my ( $number_qualifier, $number ) = @_;
564 return "+${number}:${number_qualifier}";
570 my ( $self, @s ) = @_;
571 foreach my $segment (@s) {
572 if ( $segment !~ m/$seg_terminator$/o ) {
573 $segment .= $seg_terminator;
576 push @{ $self->{segs} }, @s;
581 my ( $line_number, $item_number_id ) = @_;
583 if ($item_number_id) {
584 $item_number_id = "++${item_number_id}:EN";
587 $item_number_id = q||;
590 return "LIN+$line_number$item_number_id$seg_terminator";
593 sub additional_product_id {
594 my $isbn_field = shift;
595 my ( $product_id, $product_code );
596 if ( $isbn_field =~ m/(\d{13})/ ) {
598 $product_code = 'EN';
600 elsif ( $isbn_field =~ m/(\d{9}[Xx\d])/ ) {
602 $product_code = 'IB';
605 # TBD we could have a manufacturers no issn etc
606 if ( !$product_id ) {
610 # function id set to 5 states this is the main product id
611 return "PIA+5+$product_id:$product_code$seg_terminator";
614 sub message_date_segment {
617 # qualifier:message_date:format_code
619 my $message_date = $dt->ymd(q{}); # no sep in edifact format
621 return "DTM+137:$message_date:102$seg_terminator";
627 service_string_advice => q{UNA:+.? '},
628 message_identifier => q{+ORDERS:D:96A:UN:EAN008'},
630 return ( $S{$key} ) ? $S{$key} : q{};
633 sub _interchange_sr_identifier {
634 my ( $identification, $qualifier ) = @_;
636 if ( !$identification ) {
637 $identification = 'RANDOM';
639 carp 'undefined identifier';
642 # 14 EAN International
643 # 31B US SAN (preferred)
644 # also 91 assigned by supplier
645 # also 92 assigned by buyer
646 if ( $qualifier !~ m/^(?:14|31B|91|92)/xms ) {
650 return "+$identification:$qualifier";
656 $string =~ s/[?]/??/g;
659 $string =~ s/[+]/?+/g;
673 Format an Edifact Order message from a Koha basket
677 Generates an Edifact format Order message for a Koha basket.
678 Normally the only methods used directly by the caller would be
679 new to set up the message, encode to return the formatted message
680 and filename to obtain a name under which to store the message
684 Should integrate into Koha::Edifact namespace
685 Can caller interface be made cleaner?
686 Make handling of GIR segments more customizable
692 my $edi_order = Edifact::Order->new(
693 orderlines => \@orderlines,
694 vendor => $vendor_edi_account,
698 instantiate the Edifact::Order object, all parameters are Schema::Resultset objects
699 Called in Koha::Edifact create_edi_order
703 my $filename = $edi_order->filename()
705 returns a filename for the edi order. The filename embeds a reference to the
706 basket the message was created to encode
710 my $edifact_message = $edi_order->encode();
712 Encodes the basket as a valid edifact message ready for transmission
714 =head2 initial_service_segments
716 Creates the service segments which begin the message
718 =head2 interchange_header
720 Return an interchange header encoding sender and recipient
721 ids message date and standards
723 =head2 user_data_message_segments
725 Include message data within the encoded message
727 =head2 message_trailer
729 Terminate message data including control data on number
730 of messages and segments included
732 =head2 trailing_service_segments
734 Include the service segments occurring at the end of the message
736 =head2 interchange_control_reference
738 Returns the unique interchange control reference as a 14 digit number
740 =head2 message_reference
742 On generates and subsequently returns the unique message
743 reference number as a 12 digit number preceded by ME, to generate a new number
744 pass the string 'new'.
745 In practice we encode 1 message per transmission so there is only one message
746 referenced. were we to encode multiple messages a new reference would be
749 =head2 message_header
751 Commences a new message
753 =head2 interchange_trailer
755 returns the UNZ segment which ends the tranmission encoding the
756 message count and control reference for the interchange
758 =head2 order_msg_header
760 Formats the message header segments
762 =head2 beginning_of_message
764 Returns the BGM segment which includes the Koha basket number
766 =head2 name_and_address
768 Parameters: Function ( BUYER, DELIVERY, INVOICE, SUPPLIER)
772 Returns a NAD segment containg the id and agency for for the Function
773 value. Handles the fact that NAD segments encode the value for 'EAN' differently
778 Creates the message segments wncoding an order line
780 =head2 item_description
782 Encodes the biblio item fields Author, title, publisher, date of publication
787 Formats an IMD segment, handles the chunking of data into the 35 character
788 lengths required and the creation of repeat segments
792 Add item level information
794 =head2 add_gir_identity_number
796 Handle the formatting of a GIR element
797 return empty string if no data
801 Adds a parssed array of segments to the objects segment list
802 ensures all segments are properly terminated by '
806 Adds a LIN segment consisting of the line number and the ean number
807 if the passed isbn is valid
809 =head2 additional_product_id
811 Add a PIA segment for an additional product id
813 =head2 message_date_segment
815 Passed a DateTime object returns a correctly formatted DTM segment
819 Stores and returns constant strings for service_string_advice
820 and message_identifier
821 TBD replace with class variables
823 =head2 _interchange_sr_identifier
825 Format sender and receipient identifiers for use in the interchange header
829 Encode textual data into the standard character set ( iso 8859-1 )
830 and quote any Edifact metacharacters
832 =head2 msg_date_string
834 Convenient routine which returns message date as a Y-m-d string
835 useful if the caller wants to log date of creation
839 Colin Campbell <colin.campbell@ptfs-europe.com>
844 Copyright 2014,2015,2016 PTFS-Europe Ltd
845 This program is free software, You may redistribute it under
846 under the terms of the GNU General Public License