Bug 7736: Support Ordering via Edifact EDI messages
[koha.git] / Koha / Edifact / Line.pm
blobed59f5008d298af483a2b2389ae8882013c89c18
1 package Koha::Edifact::Line;
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 utf8;
24 use MARC::Record;
25 use MARC::Field;
26 use Carp;
28 sub new {
29 my ( $class, $data_array_ref ) = @_;
30 my $self = _parse_lines($data_array_ref);
32 bless $self, $class;
33 return $self;
36 # helper routine used by constructor
37 # creates the hashref used as a data structure by the Line object
39 sub _parse_lines {
40 my $aref = shift;
42 my $lin = shift @{$aref};
44 my $id = $lin->elem( 2, 0 ); # may be undef in ordrsp
45 my $action = $lin->elem( 1, 0 );
46 my $d = {
47 line_item_number => $lin->elem(0),
48 action_notification => $action,
49 item_number_id => $id,
50 additional_product_ids => [],
52 my @item_description;
54 foreach my $s ( @{$aref} ) {
55 if ( $s->tag eq 'PIA' ) {
56 push @{ $d->{additional_product_ids} },
58 function_code => $s->elem(0),
59 item_number => $s->elem( 1, 0 ),
60 number_type => $s->elem( 1, 1 ),
63 elsif ( $s->tag eq 'IMD' ) {
64 push @item_description, $s;
66 elsif ( $s->tag eq 'QTY' ) {
67 $d->{quantity} = $s->elem( 0, 1 );
69 elsif ( $s->tag eq 'DTM' ) {
70 if ( $s->elem( 0, 0 ) eq '44' ) {
71 $d->{availability_date} = $s->elem( 0, 1 );
74 elsif ( $s->tag eq 'GIR' ) {
76 # we may get a Gir for each copy if QTY > 1
77 if ( !$d->{GIR} ) {
78 $d->{GIR} = [];
79 push @{ $d->{GIR} }, extract_gir($s);
81 else {
82 my $gir = extract_gir($s);
83 if ( $gir->{copy} ) { # may have to merge
84 foreach my $g ( @{ $d->{GIR} } ) {
85 if ( $gir->{copy} eq $g->{copy} ) {
86 foreach my $field ( keys %{$gir} ) {
87 if ( !exists $g->{$field} ) {
88 $g->{$field} = $gir->{$field};
91 undef $gir;
92 last;
95 if ( defined $gir ) {
96 push @{ $d->{GIR} }, $gir;
101 elsif ( $s->tag eq 'FTX' ) {
103 my $type = $s->elem(0);
104 my $ctype = 'coded_free_text';
105 if ( $type eq 'LNO' ) { # Ingrams Oasis Internal Notes field
106 $type = 'internal_notes';
107 $ctype = 'coded_internal_note';
109 elsif ( $type eq 'LIN' ) {
110 $type = 'orderline_free_text';
111 $ctype = 'coded_orderline_text';
113 elsif ( $type eq 'SUB' ) {
114 $type = 'coded_substitute_text';
116 else {
117 $type = 'free_text';
120 my $coded_text = $s->elem(2);
121 if ( ref $coded_text eq 'ARRAY' && $coded_text->[0] ) {
122 $d->{$ctype}->{table} = $coded_text->[1];
123 $d->{$ctype}->{code} = $coded_text->[0];
126 my $ftx = $s->elem(3);
127 if ( ref $ftx eq 'ARRAY' ) { # it comes in 70 character components
128 $ftx = join q{ }, @{$ftx};
130 if ( exists $d->{$type} ) { # we can only catenate repeats
131 $d->{$type} .= q{ };
132 $d->{$type} .= $ftx;
134 else {
135 $d->{$type} = $ftx;
138 elsif ( $s->tag eq 'MOA' ) {
140 $d->{monetary_amount} = $s->elem( 0, 1 );
142 elsif ( $s->tag eq 'PRI' ) {
144 $d->{price} = $s->elem( 0, 1 );
146 elsif ( $s->tag eq 'RFF' ) {
147 my $qualifier = $s->elem( 0, 0 );
148 if ( $qualifier eq 'QLI' ) { # Suppliers unique quotation reference
149 $d->{reference} = $s->elem( 0, 1 );
151 elsif ( $qualifier eq 'LI' ) { # Buyer's unique orderline number
152 $d->{ordernumber} = $s->elem( 0, 1 );
154 elsif ( $qualifier eq 'SLI' )
155 { # Suppliers unique order line reference number
156 $d->{orderline_reference_number} = $s->elem( 0, 1 );
160 $d->{item_description} = _format_item_description(@item_description);
161 $d->{segs} = $aref;
163 return $d;
166 sub _format_item_description {
167 my @imd = @_;
168 my $bibrec = {};
170 # IMD : +Type code 'L' + characteristic code 3 char + Description in comp 3 & 4
171 foreach my $imd (@imd) {
172 my $type_code = $imd->elem(0);
173 my $ccode = $imd->elem(1);
174 my $desc = $imd->elem( 2, 3 );
175 if ( $imd->elem( 2, 4 ) ) {
176 $desc .= $imd->elem( 2, 4 );
178 if ( $type_code ne 'L' ) {
179 carp
180 "Only handles text item descriptions at present: code=$type_code";
181 next;
183 if ( exists $bibrec->{$ccode} ) {
184 $bibrec->{$ccode} .= q{ };
185 $bibrec->{$ccode} .= $desc;
187 else {
188 $bibrec->{$ccode} = $desc;
191 return $bibrec;
194 sub marc_record {
195 my $self = shift;
196 my $b = $self->{item_description};
198 my $bib = MARC::Record->new();
200 my @spec;
201 my @fields;
202 if ( exists $b->{'010'} ) {
203 @spec = qw( 100 a 011 c 012 b 013 d 014 e );
204 push @fields, new_field( $b, [ 100, 1, q{ } ], @spec );
206 if ( exists $b->{'020'} ) {
207 @spec = qw( 020 a 021 c 022 b 023 d 024 e );
208 push @fields, new_field( $b, [ 700, 1, q{ } ], @spec );
211 # corp conf
212 if ( exists $b->{'030'} ) {
213 push @fields, $self->corpcon(1);
215 if ( exists $b->{'040'} ) {
216 push @fields, $self->corpcon(7);
218 if ( exists $b->{'050'} ) {
219 @spec = qw( '050' a '060' b '065' c );
220 push @fields, new_field( $b, [ 245, 1, 0 ], @spec );
222 if ( exists $b->{100} ) {
223 @spec = qw( 100 a 101 b);
224 push @fields, new_field( $b, [ 250, q{ }, q{ } ], @spec );
226 @spec = qw( 110 a 120 b 170 c );
227 my $f = new_field( $b, [ 260, q{ }, q{ } ], @spec );
228 if ($f) {
229 push @fields, $f;
231 @spec = qw( 180 a 181 b 182 c 183 e);
232 $f = new_field( $b, [ 300, q{ }, q{ } ], @spec );
233 if ($f) {
234 push @fields, $f;
236 if ( exists $b->{190} ) {
237 @spec = qw( 190 a);
238 push @fields, new_field( $b, [ 490, q{ }, q{ } ], @spec );
241 if ( exists $b->{200} ) {
242 @spec = qw( 200 a);
243 push @fields, new_field( $b, [ 490, q{ }, q{ } ], @spec );
245 if ( exists $b->{210} ) {
246 @spec = qw( 210 a);
247 push @fields, new_field( $b, [ 490, q{ }, q{ } ], @spec );
249 if ( exists $b->{300} ) {
250 @spec = qw( 300 a);
251 push @fields, new_field( $b, [ 500, q{ }, q{ } ], @spec );
253 if ( exists $b->{310} ) {
254 @spec = qw( 310 a);
255 push @fields, new_field( $b, [ 520, q{ }, q{ } ], @spec );
257 if ( exists $b->{320} ) {
258 @spec = qw( 320 a);
259 push @fields, new_field( $b, [ 521, q{ }, q{ } ], @spec );
261 if ( exists $b->{260} ) {
262 @spec = qw( 260 a);
263 push @fields, new_field( $b, [ 600, q{ }, q{ } ], @spec );
265 if ( exists $b->{270} ) {
266 @spec = qw( 270 a);
267 push @fields, new_field( $b, [ 650, q{ }, q{ } ], @spec );
269 if ( exists $b->{280} ) {
270 @spec = qw( 280 a);
271 push @fields, new_field( $b, [ 655, q{ }, q{ } ], @spec );
274 # class
275 if ( exists $b->{230} ) {
276 @spec = qw( 230 a);
277 push @fields, new_field( $b, [ '082', q{ }, q{ } ], @spec );
279 if ( exists $b->{240} ) {
280 @spec = qw( 240 a);
281 push @fields, new_field( $b, [ '084', q{ }, q{ } ], @spec );
283 $bib->insert_fields_ordered(@fields);
285 return $bib;
288 sub corpcon {
289 my ( $self, $level ) = @_;
290 my $test_these = {
291 1 => [ '033', '032', '034' ],
292 7 => [ '043', '042', '044' ],
294 my $conf = 0;
295 foreach my $t ( @{ $test_these->{$level} } ) {
296 if ( exists $self->{item_description}->{$t} ) {
297 $conf = 1;
300 my $tag;
301 my @spec;
302 my ( $i1, $i2 ) = ( q{ }, q{ } );
303 if ($conf) {
304 $tag = ( $level * 100 ) + 11;
305 if ( $level == 1 ) {
306 @spec = qw( 030 a 031 e 032 n 033 c 034 d);
308 else {
309 @spec = qw( 040 a 041 e 042 n 043 c 044 d);
312 else {
313 $tag = ( $level * 100 ) + 10;
314 if ( $level == 1 ) {
315 @spec = qw( 030 a 031 b);
317 else {
318 @spec = qw( 040 a 041 b);
321 return new_field( $self->{item_description}, [ $tag, $i1, $i2 ], @spec );
324 sub new_field {
325 my ( $b, $tag_ind, @sfd_elem ) = @_;
326 my @sfd;
327 while (@sfd_elem) {
328 my $e = shift @sfd_elem;
329 my $c = shift @sfd_elem;
330 if ( exists $b->{$e} ) {
331 push @sfd, $c, $b->{$e};
334 if (@sfd) {
335 my $field = MARC::Field->new( @{$tag_ind}, @sfd );
336 return $field;
338 return;
341 # Accessor methods to line data
343 sub item_number_id {
344 my $self = shift;
345 return $self->{item_number_id};
348 sub line_item_number {
349 my $self = shift;
350 return $self->{line_item_number};
353 sub additional_product_ids {
354 my $self = shift;
355 return $self->{additional_product_ids};
358 sub action_notification {
359 my $self = shift;
360 my $a = $self->{action_notification};
361 if ($a) {
362 $a = _translate_action($a); # return the associated text string
364 return $a;
367 sub item_description {
368 my $self = shift;
369 return $self->{item_description};
372 sub monetary_amount {
373 my $self = shift;
374 return $self->{monetary_amount};
377 sub quantity {
378 my $self = shift;
379 return $self->{quantity};
382 sub price {
383 my $self = shift;
384 return $self->{price};
387 sub reference {
388 my $self = shift;
389 return $self->{reference};
392 sub orderline_reference_number {
393 my $self = shift;
394 return $self->{orderline_reference_number};
397 sub ordernumber {
398 my $self = shift;
399 return $self->{ordernumber};
402 sub free_text {
403 my $self = shift;
404 return $self->{free_text};
407 sub coded_free_text {
408 my $self = shift;
409 return $self->{coded_free_text}->{code};
412 sub internal_notes {
413 my $self = shift;
414 return $self->{internal_notes};
417 sub coded_internal_note {
418 my $self = shift;
419 return $self->{coded_internal_note}->{code};
422 sub orderline_free_text {
423 my $self = shift;
424 return $self->{orderline_free_text};
427 sub coded_orderline_text {
428 my $self = shift;
429 my $code = $self->{coded_orderline_text}->{code};
430 my $table = $self->{coded_orderline_text}->{table};
431 my $txt;
432 if ( $table eq '8B' || $table eq '7B' ) {
433 $txt = translate_8B($code);
435 elsif ( $table eq '12B' ) {
436 $txt = translate_12B($code);
438 if ( !$txt || $txt eq 'no match' ) {
439 $txt = $code;
441 return $txt;
444 sub substitute_free_text {
445 my $self = shift;
446 return $self->{substitute_free_text};
449 sub coded_substitute_text {
450 my $self = shift;
451 return $self->{coded_substitute_text}->{code};
454 # This will take a standard code as returned
455 # by (orderline|substitue)-free_text (FTX seg LIN)
456 # and expand it useing EditEUR code list 8B
457 sub translate_8B {
458 my ($code) = @_;
460 # list 7B is a subset of this
461 my %code_list_8B = (
462 AB => 'Publication abandoned',
463 AD => 'Apply direct',
464 AU => 'Publisher address unknown',
465 CS => 'Status uncertain',
466 FQ => 'Only available abroad',
467 HK => 'Paperback OP: Hardback available',
468 IB => 'In stock',
469 IP => 'In print and in stock at publisher',
470 MD => 'Manufactured on demand',
471 NK => 'Item not known',
472 NN => 'We do not supply this item',
473 NP => 'Not yet published',
474 NQ => 'Not stocked',
475 NS => 'Not sold separately',
476 OB => 'Temporarily out of stock',
477 OF => 'This format out of print: other format available',
478 OP => 'Out of print',
479 OR => 'Out pf print; New Edition coming',
480 PK => 'Hardback out of print: paperback available',
481 PN => 'Publisher no longer in business',
482 RE => 'Awaiting reissue',
483 RF => 'refer to other publisher or distributor',
484 RM => 'Remaindered',
485 RP => 'Reprinting',
486 RR => 'Rights restricted: cannot supply in this market',
487 SD => 'Sold',
488 SN => 'Our supplier cannot trace',
489 SO => 'Pack or set not available: single items only',
490 ST => 'Stocktaking: temporarily unavailable',
491 TO => 'Only to order',
492 TU => 'Temporarily unavailable',
493 UB => 'Item unobtainable from our suppliers',
494 UC => 'Unavailable@ reprint under consideration',
497 if ( exists $code_list_8B{$code} ) {
498 return $code_list_8B{$code};
500 else {
501 return 'no match';
505 sub translate_12B {
506 my ($code) = @_;
508 my %code_list_12B = (
509 100 => 'Order line accepted',
510 101 => 'Price query: orderline will be held awaiting customer response',
511 102 =>
512 'Discount query: order line will be held awaiting customer response',
513 103 => 'Minimum order value not reached: order line will be held',
514 104 =>
515 'Firm order required: order line will be held awaiting customer response',
516 110 => 'Order line accepted, substitute product will be supplied',
517 200 => 'Order line not accepted',
518 201 => 'Price query: order line not accepted',
519 202 => 'Discount query: order line not accepted',
520 203 => 'Minimum order value not reached: order line not accepted',
521 205 => 'Order line not accepted: quoted promotion is invalid',
522 206 => 'Order line not accepted: quoted promotion has ended',
523 207 =>
524 'Order line not accepted: customer ineligible for quoted promotion',
525 210 => 'Order line not accepted: substitute product is offered',
526 220 => 'Oustanding order line cancelled: reason unspecified',
527 221 => 'Oustanding order line cancelled: past order expiry date',
528 222 => 'Oustanding order line cancelled by customer request',
529 223 => 'Oustanding order line cancelled: unable to supply',
530 300 => 'Order line passed to new supplier',
531 301 => 'Order line passed to secondhand department',
532 400 => 'Backordered - awaiting supply',
533 401 => 'On order from our supplier',
534 402 => 'On order from abroad',
535 403 => 'Backordered, waiting to reach minimum order value',
536 404 => 'Despatched from our supplier, awaiting delivery',
537 405 => 'Our supplier sent wrong item(s), re-ordered',
538 406 => 'Our supplier sent short, re-ordered',
539 407 => 'Our supplier sent damaged item(s), re-ordered',
540 408 => 'Our supplier sent imperfect item(s), re-ordered',
541 409 => 'Our supplier cannot trace order, re-ordered',
542 410 => 'Ordered item(s) being processed by bookseller',
543 411 =>
544 'Ordered item(s) being processed by bookseller, awaiting customer action',
545 412 => 'Order line held awaiting customer instruction',
546 500 => 'Order line on hold - contact customer service',
547 800 => 'Order line already despatched',
548 900 => 'Cannot trace order line',
549 901 => 'Order line held: note title change',
550 902 => 'Order line held: note availability date delay',
551 903 => 'Order line held: note price change',
552 999 => 'Temporary hold: order action not yet determined',
555 if ( exists $code_list_12B{$code} ) {
556 return $code_list_12B{$code};
558 else {
559 return 'no match';
563 # item_desription_fields accessors
565 sub title {
566 my $self = shift;
567 my $titlefield = q{050};
568 if ( exists $self->{item_description}->{$titlefield} ) {
569 return $self->{item_description}->{$titlefield};
571 return;
574 sub author {
575 my $self = shift;
576 my $field = q{010};
577 if ( exists $self->{item_description}->{$field} ) {
578 my $a = $self->{item_description}->{$field};
579 my $forename_field = q{011};
580 if ( exists $self->{item_description}->{$forename_field} ) {
581 $a .= ', ';
582 $a .= $self->{item_description}->{$forename_field};
584 return $a;
586 return;
589 sub series {
590 my $self = shift;
591 my $field = q{190};
592 if ( exists $self->{item_description}->{$field} ) {
593 return $self->{item_description}->{$field};
595 return;
598 sub publisher {
599 my $self = shift;
600 my $field = q{120};
601 if ( exists $self->{item_description}->{$field} ) {
602 return $self->{item_description}->{$field};
604 return;
607 sub publication_date {
608 my $self = shift;
609 my $field = q{170};
610 if ( exists $self->{item_description}->{$field} ) {
611 return $self->{item_description}->{$field};
613 return;
616 sub dewey_class {
617 my $self = shift;
618 my $field = q{230};
619 if ( exists $self->{item_description}->{$field} ) {
620 return $self->{item_description}->{$field};
622 return;
625 sub lc_class {
626 my $self = shift;
627 my $field = q{240};
628 if ( exists $self->{item_description}->{$field} ) {
629 return $self->{item_description}->{$field};
631 return;
634 sub girfield {
635 my ( $self, $field, $occ ) = @_;
636 if ( $self->number_of_girs ) {
638 # defaults to occurence 0 returns undef if occ requested > occs
639 if ( defined $occ && $occ >= @{ $self->{GIR} } ) {
640 return;
642 $occ ||= 0;
643 return $self->{GIR}->[$occ]->{$field};
645 else {
646 return;
650 sub number_of_girs {
651 my $self = shift;
652 if ( $self->{GIR} ) {
654 my $qty = @{ $self->{GIR} };
656 return $qty;
658 else {
659 return 0;
663 sub extract_gir {
664 my $s = shift;
665 my %qmap = (
666 LAC => 'barcode',
667 LAF => 'first_accession_number',
668 LAL => 'last_accession_number',
669 LCL => 'classification',
670 LCO => 'item_unique_id',
671 LCV => 'copy_value',
672 LFH => 'feature_heading',
673 LFN => 'fund_allocation',
674 LFS => 'filing_suffix',
675 LLN => 'loan_category',
676 LLO => 'branch',
677 LLS => 'label_sublocation',
678 LQT => 'part_order_quantity',
679 LRS => 'record_sublocation',
680 LSM => 'shelfmark',
681 LSQ => 'collection_code',
682 LST => 'stock_category',
683 LSZ => 'size_code',
684 LVC => 'coded_servicing_instruction',
685 LVT => 'servicing_instruction',
688 my $set_qualifier = $s->elem( 0, 0 ); # copy number
689 my $gir_element = { copy => $set_qualifier, };
690 my $element = 1;
691 while ( my $e = $s->elem($element) ) {
692 ++$element;
693 if ( exists $qmap{ $e->[1] } ) {
694 my $qualifier = $qmap{ $e->[1] };
695 $gir_element->{$qualifier} = $e->[0];
697 else {
699 carp "Unrecognized GIR code : $e->[1] for $e->[0]";
702 return $gir_element;
705 # mainly for invoice processing amt_ will derive from MOA price_ from PRI and tax_ from TAX/MOA pairsn
706 sub moa_amt {
707 my ( $self, $qualifier ) = @_;
708 foreach my $s ( @{ $self->{segs} } ) {
709 if ( $s->tag eq 'MOA' && $s->elem( 0, 0 ) eq $qualifier ) {
710 return $s->elem( 0, 1 );
713 return;
716 sub amt_discount {
717 my $self = shift;
718 return $self->moa_amt('52');
721 sub amt_prepayment {
722 my $self = shift;
723 return $self->moa_amt('113');
726 # total including allowances & tax
727 sub amt_total {
728 my $self = shift;
729 return $self->moa_amt('128');
732 # Used to give price in currency other than that given in price
733 sub amt_unitprice {
734 my $self = shift;
735 return $self->moa_amt('146');
738 # item amount after allowances excluding tax
739 sub amt_lineitem {
740 my $self = shift;
741 return $self->moa_amt('203');
744 sub pri_price {
745 my ( $self, $price_qualifier ) = @_;
746 foreach my $s ( @{ $self->{segs} } ) {
747 if ( $s->tag eq 'PRI' && $s->elem( 0, 0 ) eq $price_qualifier ) {
748 return {
749 price => $s->elem( 0, 1 ),
750 type => $s->elem( 0, 2 ),
751 type_qualifier => $s->elem( 0, 3 ),
755 return;
758 # unit price that will be chaged excl tax
759 sub price_net {
760 my $self = shift;
761 my $p = $self->pri_price('AAA');
762 if ( defined $p ) {
763 return $p->{price};
765 return;
768 # unit price excluding all allowances, charges and taxes
769 sub price_gross {
770 my $self = shift;
771 my $p = $self->pri_price('AAB');
772 if ( defined $p ) {
773 return $p->{price};
775 return;
778 # information price incl tax excluding allowances, charges
779 sub price_info {
780 my $self = shift;
781 my $p = $self->pri_price('AAE');
782 if ( defined $p ) {
783 return $p->{price};
785 return;
788 # information price incl tax,allowances, charges
789 sub price_info_inclusive {
790 my $self = shift;
791 my $p = $self->pri_price('AAE');
792 if ( defined $p ) {
793 return $p->{price};
795 return;
798 sub tax {
799 my $self = shift;
800 return $self->moa_amt('124');
803 sub availability_date {
804 my $self = shift;
805 if ( exists $self->{availability_date} ) {
806 return $self->{availability_date};
808 return;
811 # return text string representing action code
812 sub _translate_action {
813 my $code = shift;
814 my %action = (
815 2 => 'cancelled',
816 3 => 'change_requested',
817 4 => 'no_action',
818 5 => 'accepted',
819 10 => 'not_found',
820 24 => 'recorded', # Order accepted but a change notified
822 if ( $code && exists $action{$code} ) {
823 return $action{$code};
825 return $code;
829 __END__
831 =head1 NAME
833 Koha::Edifact::Line
835 =head1 SYNOPSIS
837 Class to abstractly handle a Line in an Edifact Transmission
839 =head1 DESCRIPTION
841 Allows access to Edifact line elements by name
843 =head1 BUGS
845 None documented at present
847 =head1 Methods
849 =head2 new
851 Called with an array ref of segments constituting the line
853 =head1 AUTHOR
855 Colin Campbell <colin.campbell@ptfs-europe.com>
857 =head1 COPYRIGHT
859 Copyright 2014,2015 PTFS-Europe Ltd
860 This program is free software, You may redistribute it under
861 under the terms of the GNU General Public License
864 =cut