Bug 7736: Support Ordering via Edifact EDI messages

Add support for processing incoming Edifact Quotes, Invoices
and order responses and generating and transmission of
Edifact Orders.
Basic workflow is that an incoming quote generates an aquisition
basket in Koha, with each line corresponding to an order record

The user can then generate an edifact order from this (or another)
basket, which is transferred to the vendor's site

The supplier generates an invoice on despatch and this will
result in corresponding invoices being generated in Koha
The orderlines on the invoice are receipted automatically.

We also support order response messages. This may include
simple order acknowledgements, supplier reports/amendments
on availability. Cancellation messages cause the koha order
to be cancelled, other messages are recorded against the order

Which messages are to be supported/processed is specifiable on a
vendor by vendor basis via the admin screens

You can also specify auto order i.e. to generate orders from quotes
without user intervention - This reflects existing
workflows where most work is done on the suppliers website
then generating a dummy quote

Received messages are stored in the edifact_messages table
and the original can be viewed via the online

Database changes are in installer/data/mysql/atomicchanges/edifact.sql
Note new perl dependencies:
    Net::SFTP:Foreign
    Text::Unidecode

Signed-off-by: Paul Johnson <p.johnson@staffs.ac.uk>

Signed-off-by: Sally Healey <sally.healey@cheshiresharedservices.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>

Signed-off-by: Brendan A Gallagher <brendan@bywatersolutions.com>
This commit is contained in:
Colin Campbell 2016-01-20 11:31:31 +00:00 committed by Brendan A Gallagher
parent 9658085d1e
commit e2e9916348
55 changed files with 6916 additions and 18 deletions

View File

@ -2476,6 +2476,10 @@ sub GetInvoices {
push @bind_strs, " borrowers.branchcode = ? ";
push @bind_args, $args{branchcode};
}
if($args{message_id}) {
push @bind_strs, " aqinvoices.message_id = ? ";
push @bind_args, $args{message_id};
}
$query .= " WHERE " . join(" AND ", @bind_strs) if @bind_strs;
$query .= " GROUP BY aqinvoices.invoiceid ";
@ -2600,7 +2604,7 @@ sub AddInvoice {
return unless(%invoice and $invoice{invoicenumber});
my @columns = qw(invoicenumber booksellerid shipmentdate billingdate
closedate shipmentcost shipmentcost_budgetid);
closedate shipmentcost shipmentcost_budgetid message_id);
my @set_strs;
my @set_args;

View File

@ -782,6 +782,16 @@ our $PERL_DEPS = {
'required' => '0',
'min_ver' => '0.56',
},
'Net::SFTP::Foreign' => {
'usage' => 'Edifact',
'required' => '0',
'min_ver' => '1.73',
},
'Text::Unidecode' => {
'usage' => 'Edifact',
'required' => '0',
'min_ver' => '0.04',
},
};
1;

1150
Koha/EDI.pm Normal file

File diff suppressed because it is too large Load Diff

337
Koha/Edifact.pm Normal file
View File

@ -0,0 +1,337 @@
package Koha::Edifact;
# Copyright 2014,2015 PTFS-Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use File::Slurp;
use Carp;
use Encode qw( from_to );
use Koha::Edifact::Segment;
use Koha::Edifact::Message;
my $separator = {
component => q{\:},
data => q{\+},
decimal => q{.},
release => q{\?},
reserved => q{ },
segment => q{\'},
};
sub new {
my ( $class, $param_hashref ) = @_;
my $transmission;
my $self = ();
if ( $param_hashref->{filename} ) {
if ( $param_hashref->{transmission} ) {
carp
"Cannot instantiate $class : both filename and transmission passed";
return;
}
$transmission = read_file( $param_hashref->{filename} );
}
else {
$transmission = $param_hashref->{transmission};
}
$self->{transmission} = _init($transmission);
bless $self, $class;
return $self;
}
sub interchange_header {
my ( $self, $field ) = @_;
my %element = (
sender => 1,
recipient => 2,
datetime => 3,
interchange_control_reference => 4,
application_reference => 6,
);
if ( !exists $element{$field} ) {
carp "No interchange header field $field available";
return;
}
my $data = $self->{transmission}->[0]->elem( $element{$field} );
return $data;
}
sub interchange_trailer {
my ( $self, $field ) = @_;
my $trailer = $self->{transmission}->[-1];
if ( $field eq 'interchange_control_count' ) {
return $trailer->elem(0);
}
elsif ( $field eq 'interchange_control_reference' ) {
return $trailer->elem(1);
}
carp "Trailer field $field not recognized";
return;
}
sub new_data_iterator {
my $self = shift;
my $offset = 0;
while ( $self->{transmission}->[$offset]->tag() ne 'UNH' ) {
++$offset;
if ( $offset == @{ $self->{transmission} } ) {
carp 'Cannot find message start';
return;
}
}
$self->{data_iterator} = $offset;
return 1;
}
sub next_segment {
my $self = shift;
if ( defined $self->{data_iterator} ) {
my $seg = $self->{transmission}->[ $self->{data_iterator} ];
if ( $seg->tag eq 'UNH' ) {
$self->{msg_type} = $seg->elem( 1, 0 );
}
elsif ( $seg->tag eq 'LIN' ) {
$self->{msg_type} = 'detail';
}
if ( $seg->tag ne 'UNZ' ) {
$self->{data_iterator}++;
}
else {
$self->{data_iterator} = undef;
}
return $seg;
}
return;
}
# for debugging return whole transmission
sub get_transmission {
my $self = shift;
return $self->{transmission};
}
sub message_type {
my $self = shift;
return $self->{msg_type};
}
sub _init {
my $msg = shift;
if ( !$msg ) {
return;
}
if ( $msg =~ s/^UNA(.{6})// ) {
if ( service_string_advice($1) ) {
return segmentize($msg);
}
return;
}
else {
my $s = substr $msg, 10;
croak "File does not start with a Service string advice :$s";
}
}
# return an array of Message objects
sub message_array {
my $self = shift;
# return an array of array_refs 1 ref to a message
my $msg_arr = [];
my $msg = [];
my $in_msg = 0;
foreach my $seg ( @{ $self->{transmission} } ) {
if ( $seg->tag eq 'UNH' ) {
$in_msg = 1;
push @{$msg}, $seg;
}
elsif ( $seg->tag eq 'UNT' ) {
$in_msg = 0;
if ( @{$msg} ) {
push @{$msg_arr}, Koha::Edifact::Message->new($msg);
$msg = [];
}
}
elsif ($in_msg) {
push @{$msg}, $seg;
}
}
return $msg_arr;
}
#
# internal parsing routines used in _init
#
sub service_string_advice {
my $ssa = shift;
# At present this just validates that the ssa
# is standard Edifact
# TBD reset the seps if non standard
if ( $ssa ne q{:+.? '} ) {
carp " Non standard Service String Advice [$ssa]";
return;
}
# else use default separators
return 1;
}
sub segmentize {
my $raw = shift;
# In practice edifact uses latin-1 but check
# Transport now converts to utf-8 on ingest
# Do not convert here
#my $char_set = 'iso-8859-1';
#if ( $raw =~ m/^UNB[+]UNO(.)/ ) {
# $char_set = msgcharset($1);
#}
#from_to( $raw, $char_set, 'utf8' );
my $re = qr{
(?> # dont backtrack into this group
[?]. # either the escape character
# followed by any other character
| # or
[^'?] # a character that is neither escape
# nor split
)+
}x;
my @segmented;
while ( $raw =~ /($re)/g ) {
push @segmented, Koha::Edifact::Segment->new( { seg_string => $1 } );
}
return \@segmented;
}
sub msgcharset {
my $code = shift;
if ( $code =~ m/^[^ABCDEF]$/ ) {
$code = 'default';
}
my %encoding_map = (
A => 'ascii',
B => 'ascii',
C => 'iso-8859-1',
D => 'iso-8859-1',
E => 'iso-8859-1',
F => 'iso-8859-1',
default => 'iso-8859-1',
);
return $encoding_map{$code};
}
1;
__END__
=head1 NAME
Edifact - Edifact message handler
=head1 DESCRIPTION
Koha module for parsing Edifact messages
=head1 SUBROUTINES
=head2 new
my $e = Koha::Edifact->new( { filename => 'myfilename' } );
or
my $e = Koha::Edifact->new( { transmission => $msg_variable } );
instantiate the Edifact parser, requires either to be passed an in-memory
edifact message as transmission or a filename which it will read on creation
=head2 interchange_header
will return the data in the header field designated by the parameter
specified. Valid parameters are: 'sender', 'recipient', 'datetime',
'interchange_control_reference', and 'application_reference'
=head2 interchange_trailer
called either with the string 'interchange_control_count' or
'interchange_control_reference' will return the corresponding field from
the interchange trailer
=head2 new_data_iterator
Sets the object's data_iterator to point to the UNH segment
=head2 next_segment
Returns the next segment pointed to by the data_iterator. Increments the
data_iterator member or destroys it if segment UNZ has been reached
=head2 get_transmission
This method is useful in debugg:ing. Call on an Edifact object
it returns the object's transmission member
=head2 message_type
return the object's message type
=head2 message_array
return an array of Message objects contained in the Edifact transmission
=head1 Internal Methods
=head2 _init
Called by the constructor to do the parsing of the transmission
=head2 service_string_advice
Examines the Service String Advice returns 1 if the default separartors are in use
undef otherwise
=head2 segmentize
takes a raw Edifact message and returns a reference to an array of
its segments
=head2 msgcharset
Return the character set the message was encoded in. The default is iso-8859-1
We preserve this info but will have converted to utf-8 on ingest
=head1 AUTHOR
Colin Campbell <colin.campbell@ptfs-europe.com>
=head1 COPYRIGHT
Copyright 2014,2015, PTFS-Europe Ltd
This program is free software, You may redistribute it under
under the terms of the GNU General Public License
=cut

864
Koha/Edifact/Line.pm Normal file
View File

@ -0,0 +1,864 @@
package Koha::Edifact::Line;
# Copyright 2014, 2015 PTFS-Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use utf8;
use MARC::Record;
use MARC::Field;
use Carp;
sub new {
my ( $class, $data_array_ref ) = @_;
my $self = _parse_lines($data_array_ref);
bless $self, $class;
return $self;
}
# helper routine used by constructor
# creates the hashref used as a data structure by the Line object
sub _parse_lines {
my $aref = shift;
my $lin = shift @{$aref};
my $id = $lin->elem( 2, 0 ); # may be undef in ordrsp
my $action = $lin->elem( 1, 0 );
my $d = {
line_item_number => $lin->elem(0),
action_notification => $action,
item_number_id => $id,
additional_product_ids => [],
};
my @item_description;
foreach my $s ( @{$aref} ) {
if ( $s->tag eq 'PIA' ) {
push @{ $d->{additional_product_ids} },
{
function_code => $s->elem(0),
item_number => $s->elem( 1, 0 ),
number_type => $s->elem( 1, 1 ),
};
}
elsif ( $s->tag eq 'IMD' ) {
push @item_description, $s;
}
elsif ( $s->tag eq 'QTY' ) {
$d->{quantity} = $s->elem( 0, 1 );
}
elsif ( $s->tag eq 'DTM' ) {
if ( $s->elem( 0, 0 ) eq '44' ) {
$d->{availability_date} = $s->elem( 0, 1 );
}
}
elsif ( $s->tag eq 'GIR' ) {
# we may get a Gir for each copy if QTY > 1
if ( !$d->{GIR} ) {
$d->{GIR} = [];
push @{ $d->{GIR} }, extract_gir($s);
}
else {
my $gir = extract_gir($s);
if ( $gir->{copy} ) { # may have to merge
foreach my $g ( @{ $d->{GIR} } ) {
if ( $gir->{copy} eq $g->{copy} ) {
foreach my $field ( keys %{$gir} ) {
if ( !exists $g->{$field} ) {
$g->{$field} = $gir->{$field};
}
}
undef $gir;
last;
}
}
if ( defined $gir ) {
push @{ $d->{GIR} }, $gir;
}
}
}
}
elsif ( $s->tag eq 'FTX' ) {
my $type = $s->elem(0);
my $ctype = 'coded_free_text';
if ( $type eq 'LNO' ) { # Ingrams Oasis Internal Notes field
$type = 'internal_notes';
$ctype = 'coded_internal_note';
}
elsif ( $type eq 'LIN' ) {
$type = 'orderline_free_text';
$ctype = 'coded_orderline_text';
}
elsif ( $type eq 'SUB' ) {
$type = 'coded_substitute_text';
}
else {
$type = 'free_text';
}
my $coded_text = $s->elem(2);
if ( ref $coded_text eq 'ARRAY' && $coded_text->[0] ) {
$d->{$ctype}->{table} = $coded_text->[1];
$d->{$ctype}->{code} = $coded_text->[0];
}
my $ftx = $s->elem(3);
if ( ref $ftx eq 'ARRAY' ) { # it comes in 70 character components
$ftx = join q{ }, @{$ftx};
}
if ( exists $d->{$type} ) { # we can only catenate repeats
$d->{$type} .= q{ };
$d->{$type} .= $ftx;
}
else {
$d->{$type} = $ftx;
}
}
elsif ( $s->tag eq 'MOA' ) {
$d->{monetary_amount} = $s->elem( 0, 1 );
}
elsif ( $s->tag eq 'PRI' ) {
$d->{price} = $s->elem( 0, 1 );
}
elsif ( $s->tag eq 'RFF' ) {
my $qualifier = $s->elem( 0, 0 );
if ( $qualifier eq 'QLI' ) { # Suppliers unique quotation reference
$d->{reference} = $s->elem( 0, 1 );
}
elsif ( $qualifier eq 'LI' ) { # Buyer's unique orderline number
$d->{ordernumber} = $s->elem( 0, 1 );
}
elsif ( $qualifier eq 'SLI' )
{ # Suppliers unique order line reference number
$d->{orderline_reference_number} = $s->elem( 0, 1 );
}
}
}
$d->{item_description} = _format_item_description(@item_description);
$d->{segs} = $aref;
return $d;
}
sub _format_item_description {
my @imd = @_;
my $bibrec = {};
# IMD : +Type code 'L' + characteristic code 3 char + Description in comp 3 & 4
foreach my $imd (@imd) {
my $type_code = $imd->elem(0);
my $ccode = $imd->elem(1);
my $desc = $imd->elem( 2, 3 );
if ( $imd->elem( 2, 4 ) ) {
$desc .= $imd->elem( 2, 4 );
}
if ( $type_code ne 'L' ) {
carp
"Only handles text item descriptions at present: code=$type_code";
next;
}
if ( exists $bibrec->{$ccode} ) {
$bibrec->{$ccode} .= q{ };
$bibrec->{$ccode} .= $desc;
}
else {
$bibrec->{$ccode} = $desc;
}
}
return $bibrec;
}
sub marc_record {
my $self = shift;
my $b = $self->{item_description};
my $bib = MARC::Record->new();
my @spec;
my @fields;
if ( exists $b->{'010'} ) {
@spec = qw( 100 a 011 c 012 b 013 d 014 e );
push @fields, new_field( $b, [ 100, 1, q{ } ], @spec );
}
if ( exists $b->{'020'} ) {
@spec = qw( 020 a 021 c 022 b 023 d 024 e );
push @fields, new_field( $b, [ 700, 1, q{ } ], @spec );
}
# corp conf
if ( exists $b->{'030'} ) {
push @fields, $self->corpcon(1);
}
if ( exists $b->{'040'} ) {
push @fields, $self->corpcon(7);
}
if ( exists $b->{'050'} ) {
@spec = qw( '050' a '060' b '065' c );
push @fields, new_field( $b, [ 245, 1, 0 ], @spec );
}
if ( exists $b->{100} ) {
@spec = qw( 100 a 101 b);
push @fields, new_field( $b, [ 250, q{ }, q{ } ], @spec );
}
@spec = qw( 110 a 120 b 170 c );
my $f = new_field( $b, [ 260, q{ }, q{ } ], @spec );
if ($f) {
push @fields, $f;
}
@spec = qw( 180 a 181 b 182 c 183 e);
$f = new_field( $b, [ 300, q{ }, q{ } ], @spec );
if ($f) {
push @fields, $f;
}
if ( exists $b->{190} ) {
@spec = qw( 190 a);
push @fields, new_field( $b, [ 490, q{ }, q{ } ], @spec );
}
if ( exists $b->{200} ) {
@spec = qw( 200 a);
push @fields, new_field( $b, [ 490, q{ }, q{ } ], @spec );
}
if ( exists $b->{210} ) {
@spec = qw( 210 a);
push @fields, new_field( $b, [ 490, q{ }, q{ } ], @spec );
}
if ( exists $b->{300} ) {
@spec = qw( 300 a);
push @fields, new_field( $b, [ 500, q{ }, q{ } ], @spec );
}
if ( exists $b->{310} ) {
@spec = qw( 310 a);
push @fields, new_field( $b, [ 520, q{ }, q{ } ], @spec );
}
if ( exists $b->{320} ) {
@spec = qw( 320 a);
push @fields, new_field( $b, [ 521, q{ }, q{ } ], @spec );
}
if ( exists $b->{260} ) {
@spec = qw( 260 a);
push @fields, new_field( $b, [ 600, q{ }, q{ } ], @spec );
}
if ( exists $b->{270} ) {
@spec = qw( 270 a);
push @fields, new_field( $b, [ 650, q{ }, q{ } ], @spec );
}
if ( exists $b->{280} ) {
@spec = qw( 280 a);
push @fields, new_field( $b, [ 655, q{ }, q{ } ], @spec );
}
# class
if ( exists $b->{230} ) {
@spec = qw( 230 a);
push @fields, new_field( $b, [ '082', q{ }, q{ } ], @spec );
}
if ( exists $b->{240} ) {
@spec = qw( 240 a);
push @fields, new_field( $b, [ '084', q{ }, q{ } ], @spec );
}
$bib->insert_fields_ordered(@fields);
return $bib;
}
sub corpcon {
my ( $self, $level ) = @_;
my $test_these = {
1 => [ '033', '032', '034' ],
7 => [ '043', '042', '044' ],
};
my $conf = 0;
foreach my $t ( @{ $test_these->{$level} } ) {
if ( exists $self->{item_description}->{$t} ) {
$conf = 1;
}
}
my $tag;
my @spec;
my ( $i1, $i2 ) = ( q{ }, q{ } );
if ($conf) {
$tag = ( $level * 100 ) + 11;
if ( $level == 1 ) {
@spec = qw( 030 a 031 e 032 n 033 c 034 d);
}
else {
@spec = qw( 040 a 041 e 042 n 043 c 044 d);
}
}
else {
$tag = ( $level * 100 ) + 10;
if ( $level == 1 ) {
@spec = qw( 030 a 031 b);
}
else {
@spec = qw( 040 a 041 b);
}
}
return new_field( $self->{item_description}, [ $tag, $i1, $i2 ], @spec );
}
sub new_field {
my ( $b, $tag_ind, @sfd_elem ) = @_;
my @sfd;
while (@sfd_elem) {
my $e = shift @sfd_elem;
my $c = shift @sfd_elem;
if ( exists $b->{$e} ) {
push @sfd, $c, $b->{$e};
}
}
if (@sfd) {
my $field = MARC::Field->new( @{$tag_ind}, @sfd );
return $field;
}
return;
}
# Accessor methods to line data
sub item_number_id {
my $self = shift;
return $self->{item_number_id};
}
sub line_item_number {
my $self = shift;
return $self->{line_item_number};
}
sub additional_product_ids {
my $self = shift;
return $self->{additional_product_ids};
}
sub action_notification {
my $self = shift;
my $a = $self->{action_notification};
if ($a) {
$a = _translate_action($a); # return the associated text string
}
return $a;
}
sub item_description {
my $self = shift;
return $self->{item_description};
}
sub monetary_amount {
my $self = shift;
return $self->{monetary_amount};
}
sub quantity {
my $self = shift;
return $self->{quantity};
}
sub price {
my $self = shift;
return $self->{price};
}
sub reference {
my $self = shift;
return $self->{reference};
}
sub orderline_reference_number {
my $self = shift;
return $self->{orderline_reference_number};
}
sub ordernumber {
my $self = shift;
return $self->{ordernumber};
}
sub free_text {
my $self = shift;
return $self->{free_text};
}
sub coded_free_text {
my $self = shift;
return $self->{coded_free_text}->{code};
}
sub internal_notes {
my $self = shift;
return $self->{internal_notes};
}
sub coded_internal_note {
my $self = shift;
return $self->{coded_internal_note}->{code};
}
sub orderline_free_text {
my $self = shift;
return $self->{orderline_free_text};
}
sub coded_orderline_text {
my $self = shift;
my $code = $self->{coded_orderline_text}->{code};
my $table = $self->{coded_orderline_text}->{table};
my $txt;
if ( $table eq '8B' || $table eq '7B' ) {
$txt = translate_8B($code);
}
elsif ( $table eq '12B' ) {
$txt = translate_12B($code);
}
if ( !$txt || $txt eq 'no match' ) {
$txt = $code;
}
return $txt;
}
sub substitute_free_text {
my $self = shift;
return $self->{substitute_free_text};
}
sub coded_substitute_text {
my $self = shift;
return $self->{coded_substitute_text}->{code};
}
# This will take a standard code as returned
# by (orderline|substitue)-free_text (FTX seg LIN)
# and expand it useing EditEUR code list 8B
sub translate_8B {
my ($code) = @_;
# list 7B is a subset of this
my %code_list_8B = (
AB => 'Publication abandoned',
AD => 'Apply direct',
AU => 'Publisher address unknown',
CS => 'Status uncertain',
FQ => 'Only available abroad',
HK => 'Paperback OP: Hardback available',
IB => 'In stock',
IP => 'In print and in stock at publisher',
MD => 'Manufactured on demand',
NK => 'Item not known',
NN => 'We do not supply this item',
NP => 'Not yet published',
NQ => 'Not stocked',
NS => 'Not sold separately',
OB => 'Temporarily out of stock',
OF => 'This format out of print: other format available',
OP => 'Out of print',
OR => 'Out pf print; New Edition coming',
PK => 'Hardback out of print: paperback available',
PN => 'Publisher no longer in business',
RE => 'Awaiting reissue',
RF => 'refer to other publisher or distributor',
RM => 'Remaindered',
RP => 'Reprinting',
RR => 'Rights restricted: cannot supply in this market',
SD => 'Sold',
SN => 'Our supplier cannot trace',
SO => 'Pack or set not available: single items only',
ST => 'Stocktaking: temporarily unavailable',
TO => 'Only to order',
TU => 'Temporarily unavailable',
UB => 'Item unobtainable from our suppliers',
UC => 'Unavailable@ reprint under consideration',
);
if ( exists $code_list_8B{$code} ) {
return $code_list_8B{$code};
}
else {
return 'no match';
}
}
sub translate_12B {
my ($code) = @_;
my %code_list_12B = (
100 => 'Order line accepted',
101 => 'Price query: orderline will be held awaiting customer response',
102 =>
'Discount query: order line will be held awaiting customer response',
103 => 'Minimum order value not reached: order line will be held',
104 =>
'Firm order required: order line will be held awaiting customer response',
110 => 'Order line accepted, substitute product will be supplied',
200 => 'Order line not accepted',
201 => 'Price query: order line not accepted',
202 => 'Discount query: order line not accepted',
203 => 'Minimum order value not reached: order line not accepted',
205 => 'Order line not accepted: quoted promotion is invalid',
206 => 'Order line not accepted: quoted promotion has ended',
207 =>
'Order line not accepted: customer ineligible for quoted promotion',
210 => 'Order line not accepted: substitute product is offered',
220 => 'Oustanding order line cancelled: reason unspecified',
221 => 'Oustanding order line cancelled: past order expiry date',
222 => 'Oustanding order line cancelled by customer request',
223 => 'Oustanding order line cancelled: unable to supply',
300 => 'Order line passed to new supplier',
301 => 'Order line passed to secondhand department',
400 => 'Backordered - awaiting supply',
401 => 'On order from our supplier',
402 => 'On order from abroad',
403 => 'Backordered, waiting to reach minimum order value',
404 => 'Despatched from our supplier, awaiting delivery',
405 => 'Our supplier sent wrong item(s), re-ordered',
406 => 'Our supplier sent short, re-ordered',
407 => 'Our supplier sent damaged item(s), re-ordered',
408 => 'Our supplier sent imperfect item(s), re-ordered',
409 => 'Our supplier cannot trace order, re-ordered',
410 => 'Ordered item(s) being processed by bookseller',
411 =>
'Ordered item(s) being processed by bookseller, awaiting customer action',
412 => 'Order line held awaiting customer instruction',
500 => 'Order line on hold - contact customer service',
800 => 'Order line already despatched',
900 => 'Cannot trace order line',
901 => 'Order line held: note title change',
902 => 'Order line held: note availability date delay',
903 => 'Order line held: note price change',
999 => 'Temporary hold: order action not yet determined',
);
if ( exists $code_list_12B{$code} ) {
return $code_list_12B{$code};
}
else {
return 'no match';
}
}
# item_desription_fields accessors
sub title {
my $self = shift;
my $titlefield = q{050};
if ( exists $self->{item_description}->{$titlefield} ) {
return $self->{item_description}->{$titlefield};
}
return;
}
sub author {
my $self = shift;
my $field = q{010};
if ( exists $self->{item_description}->{$field} ) {
my $a = $self->{item_description}->{$field};
my $forename_field = q{011};
if ( exists $self->{item_description}->{$forename_field} ) {
$a .= ', ';
$a .= $self->{item_description}->{$forename_field};
}
return $a;
}
return;
}
sub series {
my $self = shift;
my $field = q{190};
if ( exists $self->{item_description}->{$field} ) {
return $self->{item_description}->{$field};
}
return;
}
sub publisher {
my $self = shift;
my $field = q{120};
if ( exists $self->{item_description}->{$field} ) {
return $self->{item_description}->{$field};
}
return;
}
sub publication_date {
my $self = shift;
my $field = q{170};
if ( exists $self->{item_description}->{$field} ) {
return $self->{item_description}->{$field};
}
return;
}
sub dewey_class {
my $self = shift;
my $field = q{230};
if ( exists $self->{item_description}->{$field} ) {
return $self->{item_description}->{$field};
}
return;
}
sub lc_class {
my $self = shift;
my $field = q{240};
if ( exists $self->{item_description}->{$field} ) {
return $self->{item_description}->{$field};
}
return;
}
sub girfield {
my ( $self, $field, $occ ) = @_;
if ( $self->number_of_girs ) {
# defaults to occurence 0 returns undef if occ requested > occs
if ( defined $occ && $occ >= @{ $self->{GIR} } ) {
return;
}
$occ ||= 0;
return $self->{GIR}->[$occ]->{$field};
}
else {
return;
}
}
sub number_of_girs {
my $self = shift;
if ( $self->{GIR} ) {
my $qty = @{ $self->{GIR} };
return $qty;
}
else {
return 0;
}
}
sub extract_gir {
my $s = shift;
my %qmap = (
LAC => 'barcode',
LAF => 'first_accession_number',
LAL => 'last_accession_number',
LCL => 'classification',
LCO => 'item_unique_id',
LCV => 'copy_value',
LFH => 'feature_heading',
LFN => 'fund_allocation',
LFS => 'filing_suffix',
LLN => 'loan_category',
LLO => 'branch',
LLS => 'label_sublocation',
LQT => 'part_order_quantity',
LRS => 'record_sublocation',
LSM => 'shelfmark',
LSQ => 'collection_code',
LST => 'stock_category',
LSZ => 'size_code',
LVC => 'coded_servicing_instruction',
LVT => 'servicing_instruction',
);
my $set_qualifier = $s->elem( 0, 0 ); # copy number
my $gir_element = { copy => $set_qualifier, };
my $element = 1;
while ( my $e = $s->elem($element) ) {
++$element;
if ( exists $qmap{ $e->[1] } ) {
my $qualifier = $qmap{ $e->[1] };
$gir_element->{$qualifier} = $e->[0];
}
else {
carp "Unrecognized GIR code : $e->[1] for $e->[0]";
}
}
return $gir_element;
}
# mainly for invoice processing amt_ will derive from MOA price_ from PRI and tax_ from TAX/MOA pairsn
sub moa_amt {
my ( $self, $qualifier ) = @_;
foreach my $s ( @{ $self->{segs} } ) {
if ( $s->tag eq 'MOA' && $s->elem( 0, 0 ) eq $qualifier ) {
return $s->elem( 0, 1 );
}
}
return;
}
sub amt_discount {
my $self = shift;
return $self->moa_amt('52');
}
sub amt_prepayment {
my $self = shift;
return $self->moa_amt('113');
}
# total including allowances & tax
sub amt_total {
my $self = shift;
return $self->moa_amt('128');
}
# Used to give price in currency other than that given in price
sub amt_unitprice {
my $self = shift;
return $self->moa_amt('146');
}
# item amount after allowances excluding tax
sub amt_lineitem {
my $self = shift;
return $self->moa_amt('203');
}
sub pri_price {
my ( $self, $price_qualifier ) = @_;
foreach my $s ( @{ $self->{segs} } ) {
if ( $s->tag eq 'PRI' && $s->elem( 0, 0 ) eq $price_qualifier ) {
return {
price => $s->elem( 0, 1 ),
type => $s->elem( 0, 2 ),
type_qualifier => $s->elem( 0, 3 ),
};
}
}
return;
}
# unit price that will be chaged excl tax
sub price_net {
my $self = shift;
my $p = $self->pri_price('AAA');
if ( defined $p ) {
return $p->{price};
}
return;
}
# unit price excluding all allowances, charges and taxes
sub price_gross {
my $self = shift;
my $p = $self->pri_price('AAB');
if ( defined $p ) {
return $p->{price};
}
return;
}
# information price incl tax excluding allowances, charges
sub price_info {
my $self = shift;
my $p = $self->pri_price('AAE');
if ( defined $p ) {
return $p->{price};
}
return;
}
# information price incl tax,allowances, charges
sub price_info_inclusive {
my $self = shift;
my $p = $self->pri_price('AAE');
if ( defined $p ) {
return $p->{price};
}
return;
}
sub tax {
my $self = shift;
return $self->moa_amt('124');
}
sub availability_date {
my $self = shift;
if ( exists $self->{availability_date} ) {
return $self->{availability_date};
}
return;
}
# return text string representing action code
sub _translate_action {
my $code = shift;
my %action = (
2 => 'cancelled',
3 => 'change_requested',
4 => 'no_action',
5 => 'accepted',
10 => 'not_found',
24 => 'recorded', # Order accepted but a change notified
);
if ( $code && exists $action{$code} ) {
return $action{$code};
}
return $code;
}
1;
__END__
=head1 NAME
Koha::Edifact::Line
=head1 SYNOPSIS
Class to abstractly handle a Line in an Edifact Transmission
=head1 DESCRIPTION
Allows access to Edifact line elements by name
=head1 BUGS
None documented at present
=head1 Methods
=head2 new
Called with an array ref of segments constituting the line
=head1 AUTHOR
Colin Campbell <colin.campbell@ptfs-europe.com>
=head1 COPYRIGHT
Copyright 2014,2015 PTFS-Europe Ltd
This program is free software, You may redistribute it under
under the terms of the GNU General Public License
=cut

249
Koha/Edifact/Message.pm Normal file
View File

@ -0,0 +1,249 @@
package Koha::Edifact::Message;
# Copyright 2014,2015 PTFS-Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use utf8;
use Koha::Edifact::Line;
sub new {
my ( $class, $data_array_ref ) = @_;
my $header = shift @{$data_array_ref};
my $bgm = shift @{$data_array_ref};
my $msg_function = $bgm->elem(2);
my $dtm = [];
while ( $data_array_ref->[0]->tag eq 'DTM' ) {
push @{$dtm}, shift @{$data_array_ref};
}
my $self = {
function => $msg_function,
header => $header,
bgm => $bgm,
message_reference_number => $header->elem(0),
dtm => $dtm,
datasegs => $data_array_ref,
};
bless $self, $class;
return $self;
}
sub message_refno {
my $self = shift;
return $self->{message_reference_number};
}
sub function {
my $self = shift;
my $msg_function = $self->{bgm}->elem(2);
if ( $msg_function == 9 ) {
return 'original';
}
elsif ( $msg_function == 7 ) {
return 'retransmission';
}
return;
}
sub message_reference_number {
my $self = shift;
return $self->{header}->elem(0);
}
sub message_type {
my $self = shift;
return $self->{header}->elem( 1, 0 );
}
sub message_code {
my $self = shift;
return $self->{bgm}->elem( 0, 0 );
}
sub docmsg_number {
my $self = shift;
return $self->{bgm}->elem(1);
}
sub message_date {
my $self = shift;
# usually the first if not only dtm
foreach my $d ( @{ $self->{dtm} } ) {
if ( $d->elem( 0, 0 ) eq '137' ) {
return $d->elem( 0, 1 );
}
}
return; # this should not happen
}
sub tax_point_date {
my $self = shift;
if ( $self->message_type eq 'INVOIC' ) {
foreach my $d ( @{ $self->{dtm} } ) {
if ( $d->elem( 0, 0 ) eq '131' ) {
return $d->elem( 0, 1 );
}
}
}
return;
}
sub expiry_date {
my $self = shift;
if ( $self->message_type eq 'QUOTES' ) {
foreach my $d ( @{ $self->{dtm} } ) {
if ( $d->elem( 0, 0 ) eq '36' ) {
return $d->elem( 0, 1 );
}
}
}
return;
}
sub shipment_charge {
my $self = shift;
# A large number of different charges can be expressed at invoice and
# item level but the only one koha takes cognizance of is shipment
# should we wrap all invoice level charges into it??
if ( $self->message_type eq 'INVOIC' ) {
my $delivery = 0;
my $amt = 0;
foreach my $s ( @{ $self->{datasegs} } ) {
if ( $s->tag eq 'LIN' ) {
last;
}
if ( $s->tag eq 'ALC' ) {
if ( $s->elem(0) eq 'C' ) { # Its a charge
if ( $s->elem( 4, 0 ) eq 'DL' ) { # delivery charge
$delivery = 1;
}
}
next;
}
if ( $s->tag eq 'MOA' ) {
$amt += $s->elem( 0, 1 );
}
}
return $amt;
}
return;
}
# return NAD fields
sub buyer_ean {
my $self = shift;
foreach my $s ( @{ $self->{datasegs} } ) {
if ( $s->tag eq 'LIN' ) {
last;
}
if ( $s->tag eq 'NAD' ) {
my $qualifier = $s->elem(0);
if ( $qualifier eq 'BY' ) {
return $s->elem( 1, 0 );
}
}
}
return;
}
sub supplier_ean {
my $self = shift;
foreach my $s ( @{ $self->{datasegs} } ) {
if ( $s->tag eq 'LIN' ) {
last;
}
if ( $s->tag eq 'NAD' ) {
my $qualifier = $s->elem(0);
if ( $qualifier eq 'SU' ) {
return $s->elem( 1, 0 );
}
}
}
return;
}
sub lineitems {
my $self = shift;
if ( $self->{quotation_lines} ) {
return $self->{quotation_lines};
}
else {
my $items = [];
my $item_arr = [];
foreach my $seg ( @{ $self->{datasegs} } ) {
my $tag = $seg->tag;
if ( $tag eq 'LIN' ) {
if ( @{$item_arr} ) {
push @{$items}, Koha::Edifact::Line->new($item_arr);
}
$item_arr = [$seg];
next;
}
elsif ( $tag =~ m/^(UNS|CNT|UNT)$/sxm ) {
if ( @{$item_arr} ) {
push @{$items}, Koha::Edifact::Line->new($item_arr);
}
last;
}
else {
if ( @{$item_arr} ) {
push @{$item_arr}, $seg;
}
}
}
$self->{quotation_lines} = $items;
return $items;
}
}
1;
__END__
=head1 NAME
Koha::Edifact::Message
=head1 DESCRIPTION
Class modelling an Edifact Message for parsing
=head1 METHODS
=head2 new
Passed an array of segments extracts message level info
and parses lineitems as Line objects
=head1 AUTHOR
Colin Campbell <colin.campbell@ptfs-europe.com>
=head1 COPYRIGHT
Copyright 2014,2015 PTFS-Europe Ltd
This program is free software, You may redistribute it under
under the terms of the GNU General Public License
=cut

831
Koha/Edifact/Order.pm Normal file
View File

@ -0,0 +1,831 @@
package Koha::Edifact::Order;
use strict;
use warnings;
use utf8;
# Copyright 2014,2015 PTFS-Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Carp;
use DateTime;
use Readonly;
use Business::ISBN;
use Koha::Database;
use C4::Budgets qw( GetBudget );
Readonly::Scalar my $seg_terminator => q{'};
Readonly::Scalar my $separator => q{+};
Readonly::Scalar my $component_separator => q{:};
Readonly::Scalar my $release_character => q{?};
Readonly::Scalar my $NINES_12 => 999_999_999_999;
Readonly::Scalar my $NINES_14 => 99_999_999_999_999;
Readonly::Scalar my $CHUNKSIZE => 35;
sub new {
my ( $class, $parameter_hashref ) = @_;
my $self = {};
if ( ref $parameter_hashref ) {
$self->{orderlines} = $parameter_hashref->{orderlines};
$self->{recipient} = $parameter_hashref->{vendor};
$self->{sender} = $parameter_hashref->{ean};
$self->{is_response} = $parameter_hashref->{is_response};
# convenient alias
$self->{basket} = $self->{orderlines}->[0]->basketno;
$self->{message_date} = DateTime->now( time_zone => 'local' );
}
# validate that its worth proceeding
if ( !$self->{orderlines} ) {
carp 'No orderlines passed to create order';
return;
}
if ( !$self->{recipient} ) {
carp
"No vendor passed to order creation: basket = $self->{basket}->basketno()";
return;
}
if ( !$self->{sender} ) {
carp
"No sender ean passed to order creation: basket = $self->{basket}->basketno()";
return;
}
# do this once per object not once per orderline
my $database = Koha::Database->new();
$self->{schema} = $database->schema;
bless $self, $class;
return $self;
}
sub filename {
my $self = shift;
if ( !$self->{orderlines} ) {
return;
}
my $filename = 'ordr' . $self->{basket}->basketno;
$filename .= '.CEP';
return $filename;
}
sub encode {
my ($self) = @_;
$self->{interchange_control_reference} = int rand($NINES_14);
$self->{message_count} = 0;
# $self->{segs}; # Message segments
$self->{transmission} = q{};
$self->{transmission} .= $self->initial_service_segments();
$self->{transmission} .= $self->user_data_message_segments();
$self->{transmission} .= $self->trailing_service_segments();
return $self->{transmission};
}
sub msg_date_string {
my $self = shift;
return $self->{message_date}->ymd();
}
sub initial_service_segments {
my $self = shift;
#UNA service string advice - specifies standard separators
my $segs = _const('service_string_advice');
#UNB interchange header
$segs .= $self->interchange_header();
#UNG functional group header NOT USED
return $segs;
}
sub interchange_header {
my $self = shift;
# syntax identifier
my $hdr =
'UNB+UNOC:3'; # controling agency character set syntax version number
# Interchange Sender
$hdr .= _interchange_sr_identifier( $self->{sender}->ean,
$self->{sender}->id_code_qualifier ); # interchange sender
$hdr .= _interchange_sr_identifier( $self->{recipient}->san,
$self->{recipient}->id_code_qualifier ); # interchange Recipient
$hdr .= $separator;
# DateTime of preparation
$hdr .= $self->{message_date}->format_cldr('yyMMdd:HHmm');
$hdr .= $separator;
$hdr .= $self->interchange_control_reference();
$hdr .= $separator;
# Recipents reference password not usually used in edifact
$hdr .= q{+ORDERS}; # application reference
#Edifact does not usually include the following
# $hdr .= $separator; # Processing priority not usually used in edifact
# $hdr .= $separator; # Acknowledgewment request : not usually used in edifact
# $hdr .= q{+EANCOM} # Communications agreement id
# $hdr .= q{+1} # Test indicator
#
$hdr .= $seg_terminator;
return $hdr;
}
sub user_data_message_segments {
my $self = shift;
#UNH message_header :: seg count begins here
$self->message_header();
$self->order_msg_header();
my $line_number = 0;
foreach my $ol ( @{ $self->{orderlines} } ) {
++$line_number;
$self->order_line( $line_number, $ol );
}
$self->message_trailer();
my $data_segment_string = join q{}, @{ $self->{segs} };
return $data_segment_string;
}
sub message_trailer {
my $self = shift;
# terminate the message
$self->add_seg("UNS+S$seg_terminator");
# CNT Control_Total
# Could be (code 1) total value of QTY segments
# or ( code = 2 ) number of lineitems
my $num_orderlines = @{ $self->{orderlines} };
$self->add_seg("CNT+2:$num_orderlines$seg_terminator");
# UNT Message Trailer
my $segments_in_message =
1 + @{ $self->{segs} }; # count incl UNH & UNT (!!this one)
my $reference = $self->message_reference('current');
$self->add_seg("UNT+$segments_in_message+$reference$seg_terminator");
return;
}
sub trailing_service_segments {
my $self = shift;
my $trailer = q{};
#UNE functional group trailer NOT USED
#UNZ interchange trailer
$trailer .= $self->interchange_trailer();
return $trailer;
}
sub interchange_control_reference {
my $self = shift;
if ( $self->{interchange_control_reference} ) {
return sprintf '%014d', $self->{interchange_control_reference};
}
else {
carp 'calling for ref of unencoded order';
return 'NONE ASSIGNED';
}
}
sub message_reference {
my ( $self, $function ) = @_;
if ( $function eq 'new' || !$self->{message_reference_no} ) {
# unique 14 char mesage ref
$self->{message_reference_no} = sprintf 'ME%012d', int rand($NINES_12);
}
return $self->{message_reference_no};
}
sub message_header {
my $self = shift;
$self->{segs} = []; # initialize the message
$self->{message_count}++; # In practice alwaya 1
my $hdr = q{UNH+} . $self->message_reference('new');
$hdr .= _const('message_identifier');
$self->add_seg($hdr);
return;
}
sub interchange_trailer {
my $self = shift;
my $t = "UNZ+$self->{message_count}+";
$t .= $self->interchange_control_reference;
$t .= $seg_terminator;
return $t;
}
sub order_msg_header {
my $self = shift;
my @header;
# UNH see message_header
# BGM
push @header,
beginning_of_message(
$self->{basket}->basketno,
$self->{recipient}->san,
$self->{is_response}
);
# DTM
push @header, message_date_segment( $self->{message_date} );
# NAD-RFF buyer supplier ids
push @header,
name_and_address(
'BUYER',
$self->{sender}->ean,
$self->{sender}->id_code_qualifier
);
push @header,
name_and_address(
'SUPPLIER',
$self->{recipient}->san,
$self->{recipient}->id_code_qualifier
);
# repeat for for other relevant parties
# CUX currency
# ISO 4217 code to show default currency prices are quoted in
# e.g. CUX+2:GBP:9'
# TBD currency handling
$self->add_seg(@header);
return;
}
sub beginning_of_message {
my $basketno = shift;
my $supplier_san = shift;
my $response = shift;
my $document_message_no = sprintf '%011d', $basketno;
# Peters & Bolinda use the BIC recommendation to use 22V a code not in Edifact
# If the order is in response to a quote
my %bic_sans = (
'5013546025065' => 'Peters',
'9377779308820' => 'Bolinda',
);
# my $message_function = 9; # original 7 = retransmission
# message_code values
# 220 prder
# 224 rush order
# 228 sample order :: order for approval / inspection copies
# 22C continuation order for volumes in a set etc.
# my $message_code = '220';
if ( exists $bic_sans{$supplier_san} && $response ) {
return "BGM+22V+$document_message_no+9$seg_terminator";
}
return "BGM+220+$document_message_no+9$seg_terminator";
}
sub name_and_address {
my ( $party, $id_code, $id_agency ) = @_;
my %qualifier_code = (
BUYER => 'BY',
DELIVERY => 'DP', # delivery location if != buyer
INVOICEE => 'IV', # if different from buyer
SUPPLIER => 'SU',
);
if ( !exists $qualifier_code{$party} ) {
carp "No qualifier code for $party";
return;
}
if ( $id_agency eq '14' ) {
$id_agency = '9'; # ean coded differently in this seg
}
return "NAD+$qualifier_code{$party}+${id_code}::$id_agency$seg_terminator";
}
sub order_line {
my ( $self, $linenumber, $orderline ) = @_;
my $schema = $self->{schema};
if ( !$orderline->biblionumber )
{ # cannot generate an orderline without a bib record
return;
}
my $biblionumber = $orderline->biblionumber->biblionumber;
my @biblioitems = $schema->resultset('Biblioitem')
->search( { biblionumber => $biblionumber, } );
my $biblioitem = $biblioitems[0]; # makes the assumption there is 1 only
# or else all have same details
my $id_string = $orderline->line_item_id;
# LIN line-number in msg :: if we had a 13 digit ean we could add
$self->add_seg( lin_segment( $linenumber, $id_string ) );
# PIA isbn or other id
my @identifiers;
foreach my $id ( $biblioitem->ean, $biblioitem->issn, $biblioitem->isbn ) {
if ( $id && $id ne $id_string ) {
push @identifiers, $id;
}
}
$self->add_seg( additional_product_id( join( ' ', @identifiers ) ) );
# biblio description
$self->add_seg( item_description( $orderline->biblionumber, $biblioitem ) );
# QTY order quantity
my $qty = join q{}, 'QTY+21:', $orderline->quantity, $seg_terminator;
$self->add_seg($qty);
# DTM Optional date constraints on delivery
# we dont currently support this in koha
# GIR copy-related data
my @items;
if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
my @linked_itemnumbers = $orderline->aqorders_items;
foreach my $item (@linked_itemnumbers) {
my $i_obj = $schema->resultset('Item')->find( $item->itemnumber );
if ( defined $i_obj ) {
push @items, $i_obj;
}
}
}
else {
my $item_hash = {
itemtype => $biblioitem->itemtype,
shelfmark => $biblioitem->cn_class,
};
my $branch = $orderline->basketno->deliveryplace;
if ($branch) {
$item_hash->{branch} = $branch;
}
for ( 1 .. $orderline->quantity ) {
push @items, $item_hash;
}
}
my $budget = GetBudget( $orderline->budget_id );
my $ol_fields = { budget_code => $budget->{budget_code}, };
if ( $orderline->order_vendornote ) {
$ol_fields->{servicing_instruction} = $orderline->order_vendornote;
}
$self->add_seg( gir_segments( $ol_fields, @items ) );
# TBD what if #items exceeds quantity
# FTX free text for current orderline TBD
# dont really have a special instructions field to encode here
# Encode notes here
# PRI-CUX-DTM unit price on which order is placed : optional
# Coutts read this as 0.00 if not present
if ( $orderline->listprice ) {
my $price = sprintf 'PRI+AAE:%.2f:CA', $orderline->listprice;
$price .= $seg_terminator;
$self->add_seg($price);
}
# RFF unique orderline reference no
my $rff = join q{}, 'RFF+LI:', $orderline->ordernumber, $seg_terminator;
$self->add_seg($rff);
# RFF : suppliers unique quotation reference number
if ( $orderline->suppliers_reference_number ) {
$rff = join q{}, 'RFF+', $orderline->suppliers_reference_qualifier,
':', $orderline->suppliers_reference_number, $seg_terminator;
$self->add_seg($rff);
}
# LOC-QTY multiple delivery locations
#TBD to specify extra delivery locs
# NAD order line name and address
#TBD Optionally indicate a name & address or order originator
# TDT method of delivey ol-specific
# TBD requests a special delivery option
return;
}
sub item_description {
my ( $bib, $biblioitem ) = @_;
my $bib_desc = {
author => $bib->author,
title => $bib->title,
publisher => $biblioitem->publishercode,
year => $biblioitem->publicationyear,
};
my @itm = ();
# 009 Author
# 050 Title :: title
# 080 Vol/Part no
# 100 Edition statement
# 109 Publisher :: publisher
# 110 place of pub
# 170 Date of publication :: year
# 220 Binding :: binding
my %code = (
author => '009',
title => '050',
publisher => '109',
year => '170',
binding => '220',
);
for my $field (qw(author title publisher year binding )) {
if ( $bib_desc->{$field} ) {
my $data = encode_text( $bib_desc->{$field} );
push @itm, imd_segment( $code{$field}, $data );
}
}
return @itm;
}
sub imd_segment {
my ( $code, $data ) = @_;
my $seg_prefix = "IMD+L+$code+:::";
# chunk_line
my @chunks;
while ( my $x = substr $data, 0, $CHUNKSIZE, q{} ) {
if ( length $x == $CHUNKSIZE ) {
if ( $x =~ s/([?]{1,2})$// ) {
$data = "$1$data"; # dont breakup ?' ?? etc
}
}
push @chunks, $x;
}
my @segs;
my $odd = 1;
foreach my $c (@chunks) {
if ($odd) {
push @segs, "$seg_prefix$c";
}
else {
$segs[-1] .= ":$c$seg_terminator";
}
$odd = !$odd;
}
if ( @segs && $segs[-1] !~ m/$seg_terminator$/o ) {
$segs[-1] .= $seg_terminator;
}
return @segs;
}
sub gir_segments {
my ( $orderfields, @onorderitems ) = @_;
my $budget_code = $orderfields->{budget_code};
my @segments;
my $sequence_no = 1;
foreach my $item (@onorderitems) {
my $seg = sprintf 'GIR+%03d', $sequence_no;
$seg .= add_gir_identity_number( 'LFN', $budget_code );
if ( C4::Context->preference('AcqCreateItem') eq 'ordering' ) {
$seg .=
add_gir_identity_number( 'LLO', $item->homebranch->branchcode );
$seg .= add_gir_identity_number( 'LST', $item->itype );
$seg .= add_gir_identity_number( 'LSQ', $item->location );
$seg .= add_gir_identity_number( 'LSM', $item->itemcallnumber );
# itemcallnumber -> shelfmark
}
else {
if ( $item->{branch} ) {
$seg .= add_gir_identity_number( 'LLO', $item->{branch} );
}
$seg .= add_gir_identity_number( 'LST', $item->{itemtype} );
$seg .= add_gir_identity_number( 'LSM', $item->{shelfmark} );
}
if ( $orderfields->{servicing_instruction} ) {
$seg .= add_gir_identity_number( 'LVT',
$orderfields->{servicing_instruction} );
}
$sequence_no++;
push @segments, $seg;
}
return @segments;
}
sub add_gir_identity_number {
my ( $number_qualifier, $number ) = @_;
if ($number) {
return "+${number}:${number_qualifier}";
}
return q{};
}
sub add_seg {
my ( $self, @s ) = @_;
foreach my $segment (@s) {
if ( $segment !~ m/$seg_terminator$/o ) {
$segment .= $seg_terminator;
}
}
push @{ $self->{segs} }, @s;
return;
}
sub lin_segment {
my ( $line_number, $item_number_id ) = @_;
if ($item_number_id) {
$item_number_id = "++${item_number_id}:EN";
}
else {
$item_number_id = q||;
}
return "LIN+$line_number$item_number_id$seg_terminator";
}
sub additional_product_id {
my $isbn_field = shift;
my ( $product_id, $product_code );
if ( $isbn_field =~ m/(\d{13})/ ) {
$product_id = $1;
$product_code = 'EN';
}
elsif ( $isbn_field =~ m/(\d{9})[Xx\d]/ ) {
$product_id = $1;
$product_code = 'IB';
}
# TBD we could have a manufacturers no issn etc
if ( !$product_id ) {
return;
}
# function id set to 5 states this is the main product id
return "PIA+5+$product_id:$product_code$seg_terminator";
}
sub message_date_segment {
my $dt = shift;
# qualifier:message_date:format_code
my $message_date = $dt->ymd(q{}); # no sep in edifact format
return "DTM+137:$message_date:102$seg_terminator";
}
sub _const {
my $key = shift;
Readonly my %S => {
service_string_advice => q{UNA:+.? '},
message_identifier => q{+ORDERS:D:96A:UN:EAN008'},
};
return ( $S{$key} ) ? $S{$key} : q{};
}
sub _interchange_sr_identifier {
my ( $identification, $qualifier ) = @_;
if ( !$identification ) {
$identification = 'RANDOM';
$qualifier = '92';
carp 'undefined identifier';
}
# 14 EAN International
# 31B US SAN (preferred)
# also 91 assigned by supplier
# also 92 assigned by buyer
if ( $qualifier !~ m/^(?:14|31B|91|92)/xms ) {
$qualifier = '92';
}
return "+$identification:$qualifier";
}
sub encode_text {
my $string = shift;
if ($string) {
$string =~ s/[?]/??/g;
$string =~ s/'/?'/g;
$string =~ s/:/?:/g;
$string =~ s/[+]/?+/g;
}
return $string;
}
1;
__END__
=head1 NAME
Koha::Edifact::Order
=head1 SYNOPSIS
Format an Edifact Order message from a Koha basket
=head1 DESCRIPTION
Generates an Edifact format Order message for a Koha basket.
Normally the only methods used directly by the caller would be
new to set up the message, encode to return the formatted message
and filename to obtain a name under which to store the message
=head1 BUGS
Should integrate into Koha::Edifact namespace
Can caller interface be made cleaner?
Make handling of GIR segments more customizable
=head1 METHODS
=head2 new
my $edi_order = Edifact::Order->new(
orderlines => \@orderlines,
vendor => $vendor_edi_account,
ean => $library_ean
);
instantiate the Edifact::Order object, all parameters are Schema::Resultset objects
Called in Koha::Edifact create_edi_order
=head2 filename
my $filename = $edi_order->filename()
returns a filename for the edi order. The filename embeds a reference to the
basket the message was created to encode
=head2 encode
my $edifact_message = $edi_order->encode();
Encodes the basket as a valid edifact message ready for transmission
=head2 initial_service_segments
Creates the service segments which begin the message
=head2 interchange_header
Return an interchange header encoding sender and recipient
ids message date and standards
=head2 user_data_message_segments
Include message data within the encoded message
=head2 message_trailer
Terminate message data including control data on number
of messages and segments included
=head2 trailing_service_segments
Include the service segments occuring at the end of the message
=head2 interchange_control_reference
Returns the unique interchange control reference as a 14 digit number
=head2 message_reference
On generates and subsequently returns the unique message
reference number as a 12 digit number preceded by ME, to generate a new number
pass the string 'new'.
In practice we encode 1 message per transmission so there is only one message
referenced. were we to encode multiple messages a new reference would be
neaded for each
=head2 message_header
Commences a new message
=head2 interchange_trailer
returns the UNZ segment which ends the tranmission encoding the
message count and control reference for the interchange
=head2 order_msg_header
Formats the message header segments
=head2 beginning_of_message
Returns the BGM segment which includes the Koha basket number
=head2 name_and_address
Parameters: Function ( BUYER, DELIVERY, INVOICE, SUPPLIER)
Id
Agency
Returns a NAD segment containg the id and agency for for the Function
value. Handles the fact that NAD segments encode the value for 'EAN' differently
to elsewhere.
=head2 order_line
Creates the message segments wncoding an order line
=head2 item_description
Encodes the biblio item fields Author, title, publisher, date of publication
binding
=head2 imd_segment
Formats an IMD segment, handles the chunking of data into the 35 character
lengths required and the creation of repeat segments
=head2 gir_segments
Add item level information
=head2 add_gir_identity_number
Handle the formatting of a GIR element
return empty string if no data
=head2 add_seg
Adds a parssed array of segments to the objects segment list
ensures all segments are properly terminated by '
=head2 lin_segment
Adds a LIN segment consisting of the line number and the ean number
if the passed isbn is valid
=head2 additional_product_id
Add a PIA segment for an additional product id
=head2 message_date_segment
Passed a DateTime object returns a correctly formatted DTM segment
=head2 _const
Stores and returns constant strings for service_string_advice
and message_identifier
TBD replace with class variables
=head2 _interchange_sr_identifier
Format sender and receipient identifiers for use in the interchange header
=head2 encode_text
Encode textual data into the standard character set ( iso 8859-1 )
and quote any Edifact metacharacters
=head2 msg_date_string
Convenient routine which returns message date as a Y-m-d string
useful if the caller wants to log date of creation
=head1 AUTHOR
Colin Campbell <colin.campbell@ptfs-europe.com>
=head1 COPYRIGHT
Copyright 2014,2015,2016 PTFS-Europe Ltd
This program is free software, You may redistribute it under
under the terms of the GNU General Public License
=cut

204
Koha/Edifact/Segment.pm Normal file
View File

@ -0,0 +1,204 @@
package Koha::Edifact::Segment;
# Copyright 2014,2016 PTFS-Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use utf8;
sub new {
my ( $class, $parm_ref ) = @_;
my $self = {};
if ( $parm_ref->{seg_string} ) {
$self = _parse_seg( $parm_ref->{seg_string} );
}
bless $self, $class;
return $self;
}
sub tag {
my $self = shift;
return $self->{tag};
}
# return specified element may be data or an array ref if components
sub elem {
my ( $self, $element_number, $component_number ) = @_;
if ( $element_number < @{ $self->{elem_arr} } ) {
my $e = $self->{elem_arr}->[$element_number];
if ( defined $component_number ) {
if ( ref $e eq 'ARRAY' ) {
if ( $component_number < @{$e} ) {
return $e->[$component_number];
}
}
elsif ( $component_number == 0 ) {
# a string could be an element with a single component
return $e;
}
return;
}
else {
return $e;
}
}
return; #element undefined ( out of range
}
sub element {
my ( $self, @params ) = @_;
return $self->elem(@params);
}
sub as_string {
my $self = shift;
my $string = $self->{tag};
foreach my $e ( @{ $self->{elem_arr} } ) {
$string .= q|+|;
if ( ref $e eq 'ARRAY' ) {
$string .= join q{:}, @{$e};
}
else {
$string .= $e;
}
}
return $string;
}
# parse a string into fields
sub _parse_seg {
my $s = shift;
my $e = {
tag => substr( $s, 0, 3 ),
elem_arr => _get_elements( substr( $s, 3 ) ),
};
return $e;
}
##
# String parsing
#
sub _get_elements {
my $seg = shift;
$seg =~ s/^[+]//; # dont start with a dummy element`
my @elem_array = map { _components($_) } split /(?<![?])[+]/, $seg;
return \@elem_array;
}
sub _components {
my $element = shift;
my @c = split /(?<![?])[:]/, $element;
if ( @c == 1 ) { # single element return a string
return de_escape( $c[0] );
}
@c = map { de_escape($_) } @c;
return \@c;
}
sub de_escape {
my $string = shift;
# remove escaped characters from the component string
$string =~ s/[?]([:?+'])/$1/g;
return $string;
}
1;
__END__
=head1 NAME
Koha::Edifact::Segment - Class foe Edifact Segments
=head1 DESCRIPTION
Used by Koha::Edifact to represent segments in a parsed Edifact message
=head1 METHODS
=head2 new
my $s = Koha::Edifact::Segment->new( { seg_string => $raw });
passed a string representation of the segment, parses it
and retums a Segment object
=head2 tag
returns the three character segment tag
=head2 elem
$data = $s->elem($element_number, $component_number)
return the contents of a specified element and if specified
component of that element
=head2 element
syntactic sugar this wraps the rlem method in a fuller name
=head2 as_string
returns a string representation of the segment
=head2 _parse_seg
passed a string representation of a segment returns a hash ref with
separate tag and data elements
=head2 _get_elements
passed the data portion of a segment, splits it into elements, passing each to
components to further parse them. Returns a reference to an array of
elements
=head2 _components
Passed a string element splits it into components and returns a reference
to an array of components, if only one component is present that is returned
directly.
quote characters are removed from the components
=head2 de_escape
Removes Edifact escapes from the passed string and returns the modified
string
=head1 AUTHOR
Colin Campbell <colin.campbell@ptfs-europe.com>
=head1 COPYRIGHT
Copyright 2014,2016, PTFS-Europe Ltd
This program is free software, You may redistribute it under
under the terms of the GNU General Public License
=cut

479
Koha/Edifact/Transport.pm Normal file
View File

@ -0,0 +1,479 @@
package Koha::Edifact::Transport;
# Copyright 2014,2015 PTFS-Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use utf8;
use DateTime;
use Carp;
use English qw{ -no_match_vars };
use Net::FTP;
use Net::SFTP::Foreign;
use File::Slurp;
use File::Copy;
use File::Basename qw( fileparse );
use File::Spec;
use Koha::Database;
use Encode qw( from_to );
sub new {
my ( $class, $account_id ) = @_;
my $database = Koha::Database->new();
my $schema = $database->schema();
my $acct = $schema->resultset('VendorEdiAccount')->find($account_id);
my $self = {
account => $acct,
schema => $schema,
working_dir => File::Spec->tmpdir(), #temporary work directory
transfer_date => DateTime->now( time_zone => 'local' ),
};
bless $self, $class;
return $self;
}
sub working_directory {
my ( $self, $new_value ) = @_;
if ($new_value) {
$self->{working_directory} = $new_value;
}
return $self->{working_directory};
}
sub download_messages {
my ( $self, $message_type ) = @_;
$self->{message_type} = $message_type;
my @retrieved_files;
if ( $self->{account}->transport eq 'SFTP' ) {
@retrieved_files = $self->sftp_download();
}
elsif ( $self->{account}->transport eq 'FILE' ) {
@retrieved_files = $self->file_download();
}
else { # assume FTP
@retrieved_files = $self->ftp_download();
}
return @retrieved_files;
}
sub upload_messages {
my ( $self, @messages ) = @_;
if (@messages) {
if ( $self->{account}->transport eq 'SFTP' ) {
$self->sftp_upload(@messages);
}
elsif ( $self->{account}->transport eq 'FILE' ) {
$self->file_upload(@messages);
}
else { # assume FTP
$self->ftp_upload(@messages);
}
}
return;
}
sub file_download {
my $self = shift;
my @downloaded_files;
my $file_ext = _get_file_ext( $self->{message_type} );
my $dir = $self->{account}->download_directory; # makes code more readable
# C = ready to retrieve E = Edifact
my $msg_hash = $self->message_hash();
if ( opendir my $dh, $dir ) {
my @file_list = readdir $dh;
closedir $dh;
foreach my $filename (@file_list) {
if ( $filename =~ m/[.]$file_ext$/ ) {
if ( copy( "$dir/$filename", $self->{working_dir} ) ) {
}
else {
carp "copy of $filename failed";
next;
}
push @downloaded_files, $filename;
my $processed_name = $filename;
substr $processed_name, -3, 1, 'E';
move( "$dir/$filename", "$dir/$processed_name" );
}
}
$self->ingest( $msg_hash, @downloaded_files );
}
else {
carp "Cannot open $dir";
return;
}
return @downloaded_files;
}
sub sftp_download {
my $self = shift;
my $file_ext = _get_file_ext( $self->{message_type} );
# C = ready to retrieve E = Edifact
my $msg_hash = $self->message_hash();
my @downloaded_files;
my $sftp = Net::SFTP::Foreign->new(
host => $self->{account}->host,
user => $self->{account}->username,
password => $self->{account}->password,
timeout => 10,
);
if ( $sftp->error ) {
return $self->_abort_download( undef,
'Unable to connect to remote host: ' . $sftp->error );
}
$sftp->setcwd( $self->{account}->download_directory )
or return $self->_abort_download( $sftp,
"Cannot change remote dir : $sftp->error" );
my $file_list = $sftp->ls()
or return $self->_abort_download( $sftp,
"cannot get file list from server: $sftp->error" );
foreach my $file ( @{$file_list} ) {
my $filename = $file->{filename};
if ( $filename =~ m/[.]$file_ext$/ ) {
$sftp->get( $filename, "$self->{working_dir}/$filename" );
if ( $sftp->error ) {
$self->_abort_download( $sftp,
"Error retrieving $filename: $sftp->error" );
last;
}
push @downloaded_files, $filename;
my $processed_name = $filename;
substr $processed_name, -3, 1, 'E';
#$sftp->atomic_rename( $filename, $processed_name );
my $ret = $sftp->rename( $filename, $processed_name );
if ( !$ret ) {
$self->_abort_download( $sftp,
"Error renaming $filename: $sftp->error" );
last;
}
}
}
$sftp->disconnect;
$self->ingest( $msg_hash, @downloaded_files );
return @downloaded_files;
}
sub ingest {
my ( $self, $msg_hash, @downloaded_files ) = @_;
foreach my $f (@downloaded_files) {
$msg_hash->{filename} = $f;
my $file_content =
read_file( "$self->{working_dir}/$f", binmode => ':raw' );
if ( !defined $file_content ) {
carp "Unable to read download file $f";
next;
}
from_to( $file_content, 'iso-8859-1', 'utf8' );
$msg_hash->{raw_msg} = $file_content;
$self->{schema}->resultset('EdifactMessage')->create($msg_hash);
}
return;
}
sub ftp_download {
my $self = shift;
my $file_ext = _get_file_ext( $self->{message_type} );
# C = ready to retrieve E = Edifact
my $msg_hash = $self->message_hash();
my @downloaded_files;
my $ftp = Net::FTP->new(
$self->{account}->host,
Timeout => 10,
Passive => 1
)
or return $self->_abort_download( undef,
"Cannot connect to $self->{account}->host: $EVAL_ERROR" );
$ftp->login( $self->{account}->username, $self->{account}->password )
or return $self->_abort_download( $ftp, "Cannot login: $ftp->message()" );
$ftp->cwd( $self->{account}->download_directory )
or return $self->_abort_download( $ftp,
"Cannot change remote dir : $ftp->message()" );
my $file_list = $ftp->ls()
or
return $self->_abort_download( $ftp, 'cannot get file list from server' );
foreach my $filename ( @{$file_list} ) {
if ( $filename =~ m/[.]$file_ext$/ ) {
if ( !$ftp->get( $filename, "$self->{working_dir}/$filename" ) ) {
$self->_abort_download( $ftp,
"Error retrieving $filename: $ftp->message" );
last;
}
push @downloaded_files, $filename;
my $processed_name = $filename;
substr $processed_name, -3, 1, 'E';
$ftp->rename( $filename, $processed_name );
}
}
$ftp->quit;
$self->ingest( $msg_hash, @downloaded_files );
return @downloaded_files;
}
sub ftp_upload {
my ( $self, @messages ) = @_;
my $ftp = Net::FTP->new(
$self->{account}->host,
Timeout => 10,
Passive => 1
)
or return $self->_abort_download( undef,
"Cannot connect to $self->{account}->host: $EVAL_ERROR" );
$ftp->login( $self->{account}->username, $self->{account}->password )
or return $self->_abort_download( $ftp, "Cannot login: $ftp->message()" );
$ftp->cwd( $self->{account}->upload_directory )
or return $self->_abort_download( $ftp,
"Cannot change remote dir : $ftp->message()" );
foreach my $m (@messages) {
my $content = $m->raw_msg;
if ($content) {
open my $fh, '<', \$content;
if ( $ftp->put( $fh, $m->filename ) ) {
close $fh;
$m->transfer_date( $self->{transfer_date} );
$m->status('sent');
$m->update;
}
else {
# error in transfer
}
}
}
$ftp->quit;
return;
}
sub sftp_upload {
my ( $self, @messages ) = @_;
my $sftp = Net::SFTP::Foreign->new(
host => $self->{account}->host,
user => $self->{account}->username,
password => $self->{account}->password,
timeout => 10,
);
$sftp->die_on_error("Cannot ssh to $self->{account}->host");
$sftp->setcwd( $self->{account}->upload_directory )
or return $self->_abort_download( $sftp,
"Cannot change remote dir : $sftp->error" );
foreach my $m (@messages) {
my $content = $m->raw_msg;
if ($content) {
open my $fh, '<', \$content;
if ( $sftp->put( $fh, $m->filename ) ) {
close $fh;
$m->transfer_date( $self->{transfer_date} );
$m->status('sent');
$m->update;
}
else {
# error in transfer
}
}
}
# sftp will be closed on object destructor
return;
}
sub file_upload {
my ( $self, @messages ) = @_;
my $dir = $self->{account}->upload_directory;
if ( -d $dir ) {
foreach my $m (@messages) {
my $content = $m->raw_msg;
if ($content) {
my $filename = $m->filename;
my $new_filename = "$dir/$filename";
if ( open my $fh, '>', $new_filename ) {
print {$fh} $content;
close $fh;
$m->transfer_date( $self->{transfer_date} );
$m->status('sent');
$m->update;
}
else {
carp "Could not transfer $m->filename : $ERRNO";
next;
}
}
}
}
else {
carp "Upload directory $dir does not exist";
}
return;
}
sub _abort_download {
my ( $self, $handle, $log_message ) = @_;
my $a = $self->{account}->description;
if ($handle) {
$handle->abort();
}
$log_message .= ": $a";
carp $log_message;
#returns undef i.e. an empty array
return;
}
sub _get_file_ext {
my $type = shift;
# Extension format
# 1st char Status C = Ready For pickup A = Completed E = Extracted
# 2nd Char Standard E = Edifact
# 3rd Char Type of message
my %file_types = (
QUOTE => 'CEQ',
INVOICE => 'CEI',
ORDRSP => 'CEA',
ALL => 'CE.',
);
if ( exists $file_types{$type} ) {
return $file_types{$type};
}
return 'XXXX'; # non matching type
}
sub message_hash {
my $self = shift;
my $msg = {
message_type => $self->{message_type},
vendor_id => $self->{account}->vendor_id,
edi_acct => $self->{account}->id,
status => 'new',
deleted => 0,
transfer_date => $self->{transfer_date}->ymd(),
};
return $msg;
}
1;
__END__
=head1 NAME
Koha::Edifact::Transport
=head1 SYNOPSIS
my $download = Koha::Edifact::Transport->new( $vendor_edi_account_id );
$downlowd->download_messages('QUOTE');
=head1 DESCRIPTION
Module that handles Edifact download and upload transport
currently can use sftp or ftp
Or FILE to access a local directory (useful for testing)
=head1 METHODS
=head2 new
Creates an object of Edifact::Transport requires to be passed the id
identifying the relevant edi vendor account
=head2 working_directory
getter and setter for the working_directory attribute
=head2 download_messages
called with the message type to download will perform the download
using the appropriate transport method
=head2 upload_messages
passed an array of messages will upload them to the supplier site
=head2 sftp_download
called by download_messages to perform the download using SFTP
=head2 ingest
loads downloaded files into the database
=head2 ftp_download
called by download_messages to perform the download using FTP
=head2 ftp_upload
called by upload_messages to perform the upload using ftp
=head2 sftp_upload
called by upload_messages to perform the upload using sftp
=head2 _abort_download
internal routine to halt operation on error and supply a stacktrace
=head2 _get_file_ext
internal method returning standard suffix for file names
according to message type
=head2 set_transport_direct
sets the direct ingest flag so that the object reads files from
the local file system useful in debugging
=head1 AUTHOR
Colin Campbell <colin.campbell@ptfs-europe.com>
=head1 COPYRIGHT
Copyright 2014,2015 PTFS-Europe Ltd
This program is free software, You may redistribute it under
under the terms of the GNU General Public License
=cut

View File

@ -263,6 +263,21 @@ __PACKAGE__->belongs_to(
},
);
=head2 edifact_messages
Type: has_many
Related object: L<Koha::Schema::Result::EdifactMessage>
=cut
__PACKAGE__->has_many(
"edifact_messages",
"Koha::Schema::Result::EdifactMessage",
{ "foreign.basketno" => "self.basketno" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 borrowernumbers
Type: many_to_many
@ -274,8 +289,8 @@ Composing rels: L</aqbasketusers> -> borrowernumber
__PACKAGE__->many_to_many("borrowernumbers", "aqbasketusers", "borrowernumber");
# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-07-11 09:26:55
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:pT+YFf9nfD/dmBuE4RNCFw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-09-02 11:37:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tsMzwP7eofOR27sfZSTqFQ
# You can replace this text with custom content, and it will be preserved on regeneration

View File

@ -311,6 +311,21 @@ __PACKAGE__->has_many(
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 edifact_messages
Type: has_many
Related object: L<Koha::Schema::Result::EdifactMessage>
=cut
__PACKAGE__->has_many(
"edifact_messages",
"Koha::Schema::Result::EdifactMessage",
{ "foreign.vendor_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 invoiceprice
Type: belongs_to
@ -351,10 +366,25 @@ __PACKAGE__->belongs_to(
},
);
=head2 vendor_edi_accounts
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2016-03-09 15:14:35
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JPswQh/s5S4nZnUzMckLnw
Type: has_many
Related object: L<Koha::Schema::Result::VendorEdiAccount>
=cut
__PACKAGE__->has_many(
"vendor_edi_accounts",
"Koha::Schema::Result::VendorEdiAccount",
{ "foreign.vendor_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
# You can replace this text with custom content, and it will be preserved on regeneration
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2016-03-10 19:36:24
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:mNH0CKfuRQqoOLXieV43DQ
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@ -257,6 +257,21 @@ __PACKAGE__->has_many(
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 vendor_edi_accounts
Type: has_many
Related object: L<Koha::Schema::Result::VendorEdiAccount>
=cut
__PACKAGE__->has_many(
"vendor_edi_accounts",
"Koha::Schema::Result::VendorEdiAccount",
{ "foreign.shipment_budget" => "self.budget_id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 borrowernumbers
Type: many_to_many
@ -268,8 +283,8 @@ Composing rels: L</aqbudgetborrowers> -> borrowernumber
__PACKAGE__->many_to_many("borrowernumbers", "aqbudgetborrowers", "borrowernumber");
# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-02-09 15:51:54
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SZKnWPCMNFUm/TzeBxeDZA
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2015-03-04 10:26:49
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:E4J/D0+2j0/8JZd0YRnoeA
# You can replace this text with custom content, and it will be preserved on regeneration

View File

@ -70,6 +70,12 @@ __PACKAGE__->table("aqinvoices");
is_foreign_key: 1
is_nullable: 1
=head2 message_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=cut
__PACKAGE__->add_columns(
@ -89,6 +95,8 @@ __PACKAGE__->add_columns(
{ data_type => "decimal", is_nullable => 1, size => [28, 6] },
"shipmentcost_budgetid",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"message_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
);
=head1 PRIMARY KEY
@ -135,6 +143,26 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 message
Type: belongs_to
Related object: L<Koha::Schema::Result::EdifactMessage>
=cut
__PACKAGE__->belongs_to(
"message",
"Koha::Schema::Result::EdifactMessage",
{ id => "message_id" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "SET NULL",
on_update => "RESTRICT",
},
);
=head2 shipmentcost_budgetid
Type: belongs_to
@ -156,8 +184,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-07-11 09:26:55
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3se4f767VfvBKaZ8tlXwHQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-09-18 16:21:46
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:FPZXlNt8dkjhgt2Rtc+krQ
# You can replace this text with custom content, and it will be preserved on regeneration

View File

@ -228,6 +228,29 @@ __PACKAGE__->table("aqorders");
is_nullable: 1
size: 16
=head2 line_item_id
data_type: 'varchar'
is_nullable: 1
size: 35
=head2 suppliers_reference_number
data_type: 'varchar'
is_nullable: 1
size: 35
=head2 suppliers_reference_qualifier
data_type: 'varchar'
is_nullable: 1
size: 3
=head2 suppliers_report
data_type: 'text'
is_nullable: 1
=cut
__PACKAGE__->add_columns(
@ -311,6 +334,14 @@ __PACKAGE__->add_columns(
is_nullable => 1,
size => 16,
},
"line_item_id",
{ data_type => "varchar", is_nullable => 1, size => 35 },
"suppliers_reference_number",
{ data_type => "varchar", is_nullable => 1, size => 35 },
"suppliers_reference_qualifier",
{ data_type => "varchar", is_nullable => 1, size => 3 },
"suppliers_report",
{ data_type => "text", is_nullable => 1 },
);
=head1 PRIMARY KEY
@ -513,9 +544,9 @@ Composing rels: L</aqorder_users> -> borrowernumber
__PACKAGE__->many_to_many("borrowernumbers", "aqorder_users", "borrowernumber");
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2016-03-09 15:14:35
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:somjoqKl7W2FYfhmgw4LQQ
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2016-03-10 19:38:20
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:2kQhxUE0pZ3PpwOqGtvB+g
# You can replace this text with custom content, and it will be preserved on regeneration
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@ -397,6 +397,21 @@ __PACKAGE__->might_have(
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 edifact_eans
Type: has_many
Related object: L<Koha::Schema::Result::EdifactEan>
=cut
__PACKAGE__->has_many(
"edifact_eans",
"Koha::Schema::Result::EdifactEan",
{ "foreign.branchcode" => "self.branchcode" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 hold_fill_targets
Type: has_many
@ -513,9 +528,9 @@ Composing rels: L</branchrelations> -> categorycode
__PACKAGE__->many_to_many("categorycodes", "branchrelations", "categorycode");
# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-11-06 15:26:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CGNPB/MkGLOihDThj43/4A
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-11-26 11:08:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:FjNI9OEpa5OKfwwCkggu0w
# You can replace this text with custom content, and it will be preserved on regeneration
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@ -0,0 +1,117 @@
use utf8;
package Koha::Schema::Result::EdifactEan;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Koha::Schema::Result::EdifactEan
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<edifact_ean>
=cut
__PACKAGE__->table("edifact_ean");
=head1 ACCESSORS
=head2 ee_id
data_type: 'integer'
extra: {unsigned => 1}
is_auto_increment: 1
is_nullable: 0
=head2 branchcode
data_type: 'varchar'
is_foreign_key: 1
is_nullable: 0
size: 10
=head2 ean
data_type: 'varchar'
is_nullable: 0
size: 15
=head2 id_code_qualifier
data_type: 'varchar'
default_value: 14
is_nullable: 0
size: 3
=cut
__PACKAGE__->add_columns(
"ee_id",
{
data_type => "integer",
extra => { unsigned => 1 },
is_auto_increment => 1,
is_nullable => 0,
},
"branchcode",
{ data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 },
"ean",
{ data_type => "varchar", is_nullable => 0, size => 15 },
"id_code_qualifier",
{ data_type => "varchar", default_value => 14, is_nullable => 0, size => 3 },
);
=head1 PRIMARY KEY
=over 4
=item * L</ee_id>
=back
=cut
__PACKAGE__->set_primary_key("ee_id");
=head1 RELATIONS
=head2 branchcode
Type: belongs_to
Related object: L<Koha::Schema::Result::Branch>
=cut
__PACKAGE__->belongs_to(
"branchcode",
"Koha::Schema::Result::Branch",
{ branchcode => "branchcode" },
{ is_deferrable => 1, on_delete => "RESTRICT", on_update => "RESTRICT" },
);
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2015-06-12 10:22:04
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WWcMBSXeuzgCPqM0KMxfBg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
__PACKAGE__->belongs_to('branch',
"Koha::Schema::Result::Branch",
{ 'branchcode' => 'branchcode' },
{
is_deferrable => 1,
join_type => 'LEFT',
on_delete => 'CASCADE',
on_update => 'CASCADE',
},
);
1;

View File

@ -0,0 +1,202 @@
use utf8;
package Koha::Schema::Result::EdifactMessage;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Koha::Schema::Result::EdifactMessage
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<edifact_messages>
=cut
__PACKAGE__->table("edifact_messages");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
is_auto_increment: 1
is_nullable: 0
=head2 message_type
data_type: 'varchar'
is_nullable: 0
size: 10
=head2 transfer_date
data_type: 'date'
datetime_undef_if_invalid: 1
is_nullable: 1
=head2 vendor_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=head2 edi_acct
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=head2 status
data_type: 'text'
is_nullable: 1
=head2 basketno
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=head2 raw_msg
data_type: 'mediumtext'
is_nullable: 1
=head2 filename
data_type: 'text'
is_nullable: 1
=head2 deleted
data_type: 'tinyint'
default_value: 0
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"message_type",
{ data_type => "varchar", is_nullable => 0, size => 10 },
"transfer_date",
{ data_type => "date", datetime_undef_if_invalid => 1, is_nullable => 1 },
"vendor_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"edi_acct",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"status",
{ data_type => "text", is_nullable => 1 },
"basketno",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"raw_msg",
{ data_type => "mediumtext", is_nullable => 1 },
"filename",
{ data_type => "text", is_nullable => 1 },
"deleted",
{ data_type => "tinyint", default_value => 0, is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 RELATIONS
=head2 aqinvoices
Type: has_many
Related object: L<Koha::Schema::Result::Aqinvoice>
=cut
__PACKAGE__->has_many(
"aqinvoices",
"Koha::Schema::Result::Aqinvoice",
{ "foreign.message_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 basketno
Type: belongs_to
Related object: L<Koha::Schema::Result::Aqbasket>
=cut
__PACKAGE__->belongs_to(
"basketno",
"Koha::Schema::Result::Aqbasket",
{ basketno => "basketno" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "RESTRICT",
on_update => "RESTRICT",
},
);
=head2 edi_acct
Type: belongs_to
Related object: L<Koha::Schema::Result::VendorEdiAccount>
=cut
__PACKAGE__->belongs_to(
"edi_acct",
"Koha::Schema::Result::VendorEdiAccount",
{ id => "edi_acct" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "RESTRICT",
on_update => "RESTRICT",
},
);
=head2 vendor
Type: belongs_to
Related object: L<Koha::Schema::Result::Aqbookseller>
=cut
__PACKAGE__->belongs_to(
"vendor",
"Koha::Schema::Result::Aqbookseller",
{ id => "vendor_id" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "RESTRICT",
on_update => "RESTRICT",
},
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2015-02-25 10:41:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:l4h8AsG2RJupxXQcEw8yzQ
1;

View File

@ -0,0 +1,115 @@
use utf8;
package Koha::Schema::Result::MsgInvoice;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Koha::Schema::Result::MsgInvoice
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<msg_invoice>
=cut
__PACKAGE__->table("msg_invoice");
=head1 ACCESSORS
=head2 mi_id
data_type: 'integer'
is_auto_increment: 1
is_nullable: 0
=head2 msg_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=head2 invoiceid
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=cut
__PACKAGE__->add_columns(
"mi_id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"msg_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"invoiceid",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
);
=head1 PRIMARY KEY
=over 4
=item * L</mi_id>
=back
=cut
__PACKAGE__->set_primary_key("mi_id");
=head1 RELATIONS
=head2 invoiceid
Type: belongs_to
Related object: L<Koha::Schema::Result::Aqinvoice>
=cut
__PACKAGE__->belongs_to(
"invoiceid",
"Koha::Schema::Result::Aqinvoice",
{ invoiceid => "invoiceid" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "RESTRICT",
on_update => "RESTRICT",
},
);
=head2 msg
Type: belongs_to
Related object: L<Koha::Schema::Result::EdifactMessage>
=cut
__PACKAGE__->belongs_to(
"msg",
"Koha::Schema::Result::EdifactMessage",
{ id => "msg_id" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "RESTRICT",
on_update => "RESTRICT",
},
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-09-02 11:37:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:F1jqlEH57dpxn2Pvm/vPGA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@ -0,0 +1,249 @@
use utf8;
package Koha::Schema::Result::VendorEdiAccount;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Koha::Schema::Result::VendorEdiAccount
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<vendor_edi_accounts>
=cut
__PACKAGE__->table("vendor_edi_accounts");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
is_auto_increment: 1
is_nullable: 0
=head2 description
data_type: 'text'
is_nullable: 0
=head2 host
data_type: 'varchar'
is_nullable: 1
size: 40
=head2 username
data_type: 'varchar'
is_nullable: 1
size: 40
=head2 password
data_type: 'varchar'
is_nullable: 1
size: 40
=head2 last_activity
data_type: 'date'
datetime_undef_if_invalid: 1
is_nullable: 1
=head2 vendor_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=head2 download_directory
data_type: 'text'
is_nullable: 1
=head2 upload_directory
data_type: 'text'
is_nullable: 1
=head2 san
data_type: 'varchar'
is_nullable: 1
size: 20
=head2 id_code_qualifier
data_type: 'varchar'
default_value: 14
is_nullable: 1
size: 3
=head2 transport
data_type: 'varchar'
default_value: 'FTP'
is_nullable: 1
size: 6
=head2 quotes_enabled
data_type: 'tinyint'
default_value: 0
is_nullable: 0
=head2 invoices_enabled
data_type: 'tinyint'
default_value: 0
is_nullable: 0
=head2 orders_enabled
data_type: 'tinyint'
default_value: 0
is_nullable: 0
=head2 responses_enabled
data_type: 'tinyint'
default_value: 0
is_nullable: 0
=head2 auto_orders
data_type: 'tinyint'
default_value: 0
is_nullable: 0
=head2 shipment_budget
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"description",
{ data_type => "text", is_nullable => 0 },
"host",
{ data_type => "varchar", is_nullable => 1, size => 40 },
"username",
{ data_type => "varchar", is_nullable => 1, size => 40 },
"password",
{ data_type => "varchar", is_nullable => 1, size => 40 },
"last_activity",
{ data_type => "date", datetime_undef_if_invalid => 1, is_nullable => 1 },
"vendor_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"download_directory",
{ data_type => "text", is_nullable => 1 },
"upload_directory",
{ data_type => "text", is_nullable => 1 },
"san",
{ data_type => "varchar", is_nullable => 1, size => 20 },
"id_code_qualifier",
{ data_type => "varchar", default_value => 14, is_nullable => 1, size => 3 },
"transport",
{ data_type => "varchar", default_value => "FTP", is_nullable => 1, size => 6 },
"quotes_enabled",
{ data_type => "tinyint", default_value => 0, is_nullable => 0 },
"invoices_enabled",
{ data_type => "tinyint", default_value => 0, is_nullable => 0 },
"orders_enabled",
{ data_type => "tinyint", default_value => 0, is_nullable => 0 },
"responses_enabled",
{ data_type => "tinyint", default_value => 0, is_nullable => 0 },
"auto_orders",
{ data_type => "tinyint", default_value => 0, is_nullable => 0 },
"shipment_budget",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 RELATIONS
=head2 edifact_messages
Type: has_many
Related object: L<Koha::Schema::Result::EdifactMessage>
=cut
__PACKAGE__->has_many(
"edifact_messages",
"Koha::Schema::Result::EdifactMessage",
{ "foreign.edi_acct" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 shipment_budget
Type: belongs_to
Related object: L<Koha::Schema::Result::Aqbudget>
=cut
__PACKAGE__->belongs_to(
"shipment_budget",
"Koha::Schema::Result::Aqbudget",
{ budget_id => "shipment_budget" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "RESTRICT",
on_update => "RESTRICT",
},
);
=head2 vendor
Type: belongs_to
Related object: L<Koha::Schema::Result::Aqbookseller>
=cut
__PACKAGE__->belongs_to(
"vendor",
"Koha::Schema::Result::Aqbookseller",
{ id => "vendor_id" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "RESTRICT",
on_update => "RESTRICT",
},
);
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2015-08-19 11:41:15
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:0CgJuFAItI71dfSG88NWhg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@ -36,6 +36,8 @@ use C4::Members qw/GetMember/; #needed for permissions checking for changing ba
use C4::Items;
use C4::Suggestions;
use Date::Calc qw/Add_Delta_Days/;
use Koha::Database;
use Koha::EDI qw( create_edi_order get_edifact_ean );
=head1 NAME
@ -67,6 +69,7 @@ the supplier this script have to display the basket.
my $query = new CGI;
our $basketno = $query->param('basketno');
my $ean = $query->param('ean');
my $booksellerid = $query->param('booksellerid');
my $duplinbatch = $query->param('duplinbatch');
@ -84,6 +87,10 @@ my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user(
my $basket = GetBasket($basketno);
$booksellerid = $basket->{booksellerid} unless $booksellerid;
my $bookseller = Koha::Acquisition::Bookseller->fetch({ id => $booksellerid });
my $schema = Koha::Database->new()->schema();
my $rs = $schema->resultset('VendorEdiAccount')->search(
{ vendor_id => $booksellerid, } );
$template->param( ediaccount => ($rs->count > 0));
unless (CanUserManageBasket($loggedinuser, $basket, $userflags)) {
$template->param(
@ -196,6 +203,9 @@ if ( $op eq 'delete_confirm' ) {
} elsif ($op eq 'reopen') {
ReopenBasket($query->param('basketno'));
print $query->redirect('/cgi-bin/koha/acqui/basket.pl?basketno='.$basket->{'basketno'})
}
elsif ( $op eq 'ediorder' ) {
edi_close_and_order()
} elsif ( $op eq 'mod_users' ) {
my $basketusers_ids = $query->param('users_ids');
my @basketusers = split( /:/, $basketusers_ids );
@ -468,3 +478,70 @@ sub get_order_infos {
}
output_html_with_http_headers $query, $cookie, $template->output;
sub edi_close_and_order {
my $confirm = $query->param('confirm') || $confirm_pref eq '2';
if ($confirm) {
my $edi_params = {
basketno => $basketno,
ean => $ean,
};
if ( $basket->{branch} ) {
$edi_params->{branchcode} = $basket->{branch};
}
if ( create_edi_order($edi_params) ) {
#$template->param( edifile => 1 );
}
CloseBasket($basketno);
# if requested, create basket group, close it and attach the basket
if ( $query->param('createbasketgroup') ) {
my $branchcode;
if ( C4::Context->userenv
and C4::Context->userenv->{'branch'}
and C4::Context->userenv->{'branch'} ne "NO_LIBRARY_SET" )
{
$branchcode = C4::Context->userenv->{'branch'};
}
my $basketgroupid = NewBasketgroup(
{
name => $basket->{basketname},
booksellerid => $booksellerid,
deliveryplace => $branchcode,
billingplace => $branchcode,
closed => 1,
}
);
ModBasket(
{
basketno => $basketno,
basketgroupid => $basketgroupid
}
);
print $query->redirect(
"/cgi-bin/koha/acqui/basketgroup.pl?booksellerid=$booksellerid&closed=1"
);
}
else {
print $query->redirect(
"/cgi-bin/koha/acqui/booksellers.pl?booksellerid=$booksellerid"
);
}
exit;
}
else {
$template->param(
edi_confirm => 1,
booksellerid => $booksellerid,
basketno => $basket->{basketno},
basketname => $basket->{basketname},
basketgroupname => $basket->{basketname},
);
if ($ean) {
$template->param( ean => $ean );
}
}
return;
}

View File

@ -54,6 +54,7 @@ use CGI qw ( -utf8 );
use C4::Acquisition qw/CloseBasketgroup ReOpenBasketgroup GetOrders GetBasketsByBasketgroup GetBasketsByBookseller ModBasketgroup NewBasketgroup DelBasketgroup GetBasketgroups ModBasket GetBasketgroup GetBasket GetBasketGroupAsCSV/;
use C4::Branch qw/GetBranches/;
use C4::Members qw/GetMember/;
use Koha::EDI qw/create_edi_order get_edifact_ean/;
use Koha::Acquisition::Bookseller;
@ -206,12 +207,24 @@ sub printbasketgrouppdf{
}
sub generate_edifact_orders {
my $basketgroupid = shift;
my $baskets = GetBasketsByBasketgroup($basketgroupid);
my $ean = get_edifact_ean();
for my $basket ( @{$baskets} ) {
create_edi_order( { ean => $ean, basketno => $basket->{basketno}, } );
}
return;
}
my $op = $input->param('op') || 'display';
# possible values of $op :
# - add : adds a new basketgroup, or edit an open basketgroup, or display a closed basketgroup
# - mod_basket : modify an individual basket of the basketgroup
# - closeandprint : close and print an closed basketgroup in pdf. called by clicking on "Close and print" button in closed basketgroups list
# - print : print a closed basketgroup. called by clicking on "Print" button in closed basketgroups list
# - ediprint : generate edi order messages for the baskets in the group
# - export : export in CSV a closed basketgroup. called by clicking on "Export" button in closed basketgroups list
# - delete : delete an open basketgroup. called by clicking on "Delete" button in open basketgroups list
# - reopen : reopen a closed basketgroup. called by clicking on "Reopen" button in closed basketgroup list
@ -370,6 +383,10 @@ if ( $op eq "add" ) {
$redirectpath .= "&amp;listclosed=1" if $closedbg ;
print $input->redirect($redirectpath );
} elsif ( $op eq 'ediprint') {
my $basketgroupid = $input->param('basketgroupid');
generate_edifact_orders( $basketgroupid );
exit;
}else{
# no param : display the list of all basketgroups for a given vendor
my $basketgroups = &GetBasketgroups($booksellerid);

64
acqui/edi_ean.pl Executable file
View File

@ -0,0 +1,64 @@
#!/usr/bin/perl
# Copyright 2012 Mark Gavillet & PTFS Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# This is an awkward construct and should probably be totally replaced
# but as all sites so far are single ordering ean its not clear what we should
# replace it with
#
use strict;
use warnings;
use C4::Auth;
use C4::Koha;
use C4::Output;
use Koha::Database;
use CGI;
my $schema = Koha::Database->new()->schema();
my @eans = $schema->resultset('EdifactEan')->search(
{},
{
join => 'branch',
}
);
my $query = CGI->new();
my $basketno = $query->param('basketno');
if ( @eans == 1 ) {
my $ean = $eans[0]->ean;
print $query->redirect(
"/cgi-bin/koha/acqui/basket.pl?basketno=$basketno&op=ediorder&ean=$ean"
);
}
else {
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => 'acqui/edi_ean.tt',
query => $query,
type => 'intranet',
authnotrequired => 0,
flagsrequired => { acquisition => 'order_manage' },
debug => 1,
}
);
$template->param( eans => \@eans );
$template->param( basketno => $basketno );
output_html_with_http_headers( $query, $cookie, $template->output );
}

62
acqui/edifactmsgs.pl Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/perl
# Copyright 2014 PTFS Europe Ltd.
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use strict;
use warnings;
use CGI;
use Koha::Database;
use C4::Koha;
use C4::Auth;
use C4::Output;
my $q = CGI->new;
my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user(
{
template_name => 'acqui/edifactmsgs.tt',
query => $q,
type => 'intranet',
authnotrequired => 0,
flagsrequired => { acquisition => 'manage_edi' },
debug => 1,
}
);
my $schema = Koha::Database->new()->schema();
my $cmd = $q->param('op');
if ( $cmd && $cmd == 'delete' ) {
my $id = $q->param->('message_id');
my $msg = $schema->resultset('EdifactMessage')->find($id);
$msg->deleted(1);
$msg->update;
}
my @msgs = $schema->resultset('EdifactMessage')->search(
{
deleted => 0,
},
{
join => 'vendor',
order_by => { -desc => 'transfer_date' },
}
)->all;
$template->param( messages => \@msgs );
output_html_with_http_headers( $q, $cookie, $template->output );

72
acqui/edimsg.pl Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/perl
# Copyright 2014 PTFS Europe Ltd.
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use strict;
use warnings;
use CGI;
use Koha::Database;
use C4::Koha;
use C4::Auth;
use C4::Output;
my $q = CGI->new;
my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user(
{
template_name => 'acqui/edimsg.tt',
query => $q,
type => 'intranet',
authnotrequired => 0,
flagsrequired => { acquisition => 'manage_edi' },
debug => 1,
}
);
my $msg_id = $q->param('id');
my $schema = Koha::Database->new()->schema();
my $msg = $schema->resultset('EdifactMessage')->find($msg_id);
if ($msg) {
my $transmission = $msg->raw_msg;
my @segments = segmentize($transmission);
$template->param( segments => \@segments );
}
else {
$template->param( no_message => 1 );
}
output_html_with_http_headers( $q, $cookie, $template->output );
sub segmentize {
my $raw = shift;
my $re = qr{
(?> # dont backtrack into this group
[?]. # either the escape character
# followed by any other character
| # or
[^'?] # a character that is neither escape
# nor split
)+
}x;
my @segmented;
while ( $raw =~ /($re)/g ) {
push @segmented, "$1'";
}
return @segmented;
}

View File

@ -62,6 +62,7 @@ my $author = $input->param('author');
my $publisher = $input->param('publisher');
my $publicationyear = $input->param('publicationyear');
my $branch = $input->param('branch');
my $message_id = $input->param('message_id');
my $op = $input->param('op');
$shipmentdatefrom and $shipmentdatefrom = eval { dt_from_string( $shipmentdatefrom ) };
@ -83,7 +84,8 @@ if ( $op and $op eq 'do_search' ) {
author => $author,
publisher => $publisher,
publicationyear => $publicationyear,
branchcode => $branch
branchcode => $branch,
message_id => $message_id,
);
}

155
admin/edi_accounts.pl Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/perl
# Copyright 2011,2014 Mark Gavillet & PTFS Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use strict;
use warnings;
use CGI;
use C4::Auth;
use C4::Output;
use Koha::Database;
my $input = CGI->new();
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => 'admin/edi_accounts.tt',
query => $input,
type => 'intranet',
authnotrequired => 0,
flagsrequired => { acquisition => 'edi_manage' },
}
);
my $op = $input->param('op');
$op ||= 'display';
my $schema = Koha::Database->new()->schema();
if ( $op eq 'acct_form' ) {
show_account();
$template->param( acct_form => 1 );
my @vendors = $schema->resultset('Aqbookseller')->search(
undef,
{
columns => [ 'name', 'id' ],
order_by => { -asc => 'name' }
}
);
$template->param( vendors => \@vendors );
$template->param(
code_qualifiers => [
{
code => '14',
description => 'EAN International',
},
{
code => '31B',
description => 'US SAN Agency',
},
{
code => '91',
description => 'Assigned by supplier',
},
{
code => '92',
description => 'Assigned by buyer',
},
]
);
}
elsif ( $op eq 'delete_confirm' ) {
show_account();
$template->param( delete_confirm => 1 );
}
else {
if ( $op eq 'save' ) {
# validate & display
my $id = $input->param('id');
my $fields = {
description => $input->param('description'),
host => $input->param('host'),
username => $input->param('username'),
password => $input->param('password'),
vendor_id => $input->param('vendor_id'),
upload_directory => $input->param('upload_directory'),
download_directory => $input->param('download_directory'),
san => $input->param('san'),
transport => $input->param('transport'),
quotes_enabled => defined $input->param('quotes_enabled'),
invoices_enabled => defined $input->param('invoices_enabled'),
orders_enabled => defined $input->param('orders_enabled'),
responses_enabled => defined $input->param('responses_enabled'),
auto_orders => defined $input->param('auto_orders'),
id_code_qualifier => $input->param('id_code_qualifier'),
};
if ($id) {
$schema->resultset('VendorEdiAccount')->search(
{
id => $id,
}
)->update_all($fields);
}
else { # new record
$schema->resultset('VendorEdiAccount')->create($fields);
}
}
elsif ( $op eq 'delete_confirmed' ) {
$schema->resultset('VendorEdiAccount')
->search( { id => $input->param('id'), } )->delete_all;
}
# we do a default dispaly after deletes and saves
# as well as when thats all you want
$template->param( display => 1 );
my @ediaccounts = $schema->resultset('VendorEdiAccount')->search(
{},
{
join => 'vendor',
}
);
$template->param( ediaccounts => \@ediaccounts );
}
output_html_with_http_headers( $input, $cookie, $template->output );
sub get_account {
my $id = shift;
my $account = $schema->resultset('VendorEdiAccount')->find($id);
if ($account) {
return $account;
}
# passing undef will default to add
return;
}
sub show_account {
my $acct_id = $input->param('id');
if ($acct_id) {
my $acct = $schema->resultset('VendorEdiAccount')->find($acct_id);
if ($acct) {
$template->param( account => $acct );
}
}
return;
}

158
admin/edi_ean_accounts.pl Executable file
View File

@ -0,0 +1,158 @@
#!/usr/bin/perl
# Copyright 2012, 2014 Mark Gavillet & PTFS Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use strict;
use warnings;
use CGI;
use C4::Auth;
use C4::Output;
use Koha::Database;
my $input = CGI->new();
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => 'admin/edi_ean_accounts.tt',
query => $input,
type => 'intranet',
authnotrequired => 0,
flagsrequired => { acquisition => 'edi_manage' },
}
);
my $schema = Koha::Database->new()->schema();
my $op = $input->param('op');
$op ||= 'display';
if ( $op eq 'ean_form' ) {
show_ean();
$template->param( ean_form => 1 );
my @branches = $schema->resultset('Branch')->search(
undef,
{
columns => [ 'branchcode', 'branchname' ],
order_by => 'branchname',
}
);
$template->param( branches => \@branches );
$template->param(
code_qualifiers => [
{
code => '14',
description => 'EAN International',
},
{
code => '31B',
description => 'US SAN Agency',
},
{
code => '91',
description => 'Assigned by supplier',
},
{
code => '92',
description => 'Assigned by buyer',
},
]
);
}
elsif ( $op eq 'delete_confirm' ) {
show_ean();
$template->param( delete_confirm => 1 );
}
else {
if ( $op eq 'save' ) {
my $change = $input->param('oldean');
if ($change) {
editsubmit();
}
else {
addsubmit();
}
}
elsif ( $op eq 'delete_confirmed' ) {
delsubmit();
}
my @eans = $schema->resultset('EdifactEan')->search(
{},
{
join => 'branch',
}
);
$template->param( display => 1 );
$template->param( eans => \@eans );
}
output_html_with_http_headers( $input, $cookie, $template->output );
sub delsubmit {
my $ean = $schema->resultset('EdifactEan')->find(
{
branchcode => $input->param('branchcode'),
ean => $input->param('ean')
}
);
$ean->delete;
return;
}
sub addsubmit {
my $new_ean = $schema->resultset('EdifactEan')->new(
{
branchcode => $input->param('branchcode'),
ean => $input->param('ean'),
id_code_qualifier => $input->param('id_code_qualifier'),
}
);
$new_ean->insert();
return;
}
sub editsubmit {
$schema->resultset('EdifactEan')->search(
{
branchcode => $input->param('oldbranchcode'),
ean => $input->param('oldean'),
}
)->update_all(
{
branchcode => $input->param('branchcode'),
ean => $input->param('ean'),
id_code_qualifier => $input->param('id_code_qualifier'),
}
);
return;
}
sub show_ean {
my $branchcode = $input->param('branchcode');
my $ean = $input->param('ean');
if ( $branchcode && $ean ) {
my $e = $schema->resultset('EdifactEan')->find(
{
ean => $ean,
branchcode => $branchcode,
}
);
$template->param( ean => $e );
}
return;
}

View File

@ -0,0 +1,78 @@
-- Holds details for vendors supplying goods by EDI
CREATE TABLE IF NOT EXISTS vendor_edi_accounts (
id int(11) NOT NULL auto_increment,
description text NOT NULL,
host varchar(40),
username varchar(40),
password varchar(40),
last_activity date,
vendor_id int(11) references aqbooksellers( id ),
download_directory text,
upload_directory text,
san varchar(20),
id_code_qualifier varchar(3) default '14',
transport varchar(6) default 'FTP',
quotes_enabled tinyint(1) not null default 0,
invoices_enabled tinyint(1) not null default 0,
orders_enabled tinyint(1) not null default 0,
responses_enabled tinyint(1) not null default 0,
auto_orders tinyint(1) not null default 0,
shipment_budget integer(11) references aqbudgets( budget_id ),
PRIMARY KEY (id),
KEY vendorid (vendor_id),
KEY shipmentbudget (shipment_budget),
CONSTRAINT vfk_vendor_id FOREIGN KEY ( vendor_id ) REFERENCES aqbooksellers ( id ),
CONSTRAINT vfk_shipment_budget FOREIGN KEY ( shipment_budget ) REFERENCES aqbudgets ( budget_id )
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Hold the actual edifact messages with links to associated baskets
CREATE TABLE IF NOT EXISTS edifact_messages (
id int(11) NOT NULL auto_increment,
message_type varchar(10) NOT NULL,
transfer_date date,
vendor_id int(11) references aqbooksellers( id ),
edi_acct integer references vendor_edi_accounts( id ),
status text,
basketno int(11) REFERENCES aqbasket( basketno),
raw_msg mediumtext,
filename text,
deleted boolean not null default 0,
PRIMARY KEY (id),
KEY vendorid ( vendor_id),
KEY ediacct (edi_acct),
KEY basketno ( basketno),
CONSTRAINT emfk_vendor FOREIGN KEY ( vendor_id ) REFERENCES aqbooksellers ( id ),
CONSTRAINT emfk_edi_acct FOREIGN KEY ( edi_acct ) REFERENCES vendor_edi_accounts ( id ),
CONSTRAINT emfk_basketno FOREIGN KEY ( basketno ) REFERENCES aqbasket ( basketno )
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- invoices link back to the edifact message it was generated from
ALTER TABLE aqinvoices ADD COLUMN message_id INT(11) REFERENCES edifact_messages( id );
-- clean up link on deletes
ALTER TABLE aqinvoices ADD CONSTRAINT edifact_msg_fk FOREIGN KEY ( message_id ) REFERENCES edifact_messages ( id ) ON DELETE SET NULL;
-- Hold the supplier ids from quotes for ordering
-- although this is an EAN-13 article number the standard says 35 characters ???
ALTER TABLE aqorders ADD COLUMN line_item_id varchar(35);
-- The suppliers unique reference usually a quotation line number ('QLI')
-- Otherwise Suppliers unique orderline reference ('SLI')
ALTER TABLE aqorders ADD COLUMN suppliers_reference_number varchar(35);
ALTER TABLE aqorders ADD COLUMN suppliers_reference_qualifier varchar(3);
ALTER TABLE aqorders ADD COLUMN suppliers_report text;
-- hold the EAN/SAN used in ordering
CREATE TABLE IF NOT EXISTS edifact_ean (
ee_id integer(11) unsigned not null auto_increment primary key,
branchcode VARCHAR(10) NOT NULL REFERENCES branches (branchcode),
ean varchar(15) NOT NULL,
id_code_qualifier VARCHAR(3) NOT NULL DEFAULT '14',
CONSTRAINT efk_branchcode FOREIGN KEY ( branchcode ) REFERENCES branches ( branchcode )
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Syspref budget to hold shipping costs
INSERT INTO systempreferences (variable, explanation, type) VALUES('EDIInvoicesShippingBudget','The budget code used to allocate shipping charges to when processing EDI Invoice messages', 'free');
-- Add a permission for managing EDI
INSERT INTO permissions (module_bit, code, description) values (11, 'edi_manage', 'Manage EDIFACT transmissions');

View File

@ -3112,6 +3112,10 @@ CREATE TABLE `aqorders` ( -- information related to the basket line items
`subscriptionid` int(11) default NULL, -- links this order line to a subscription (subscription.subscriptionid)
parent_ordernumber int(11) default NULL, -- ordernumber of parent order line, or same as ordernumber if no parent
`orderstatus` varchar(16) default 'new', -- the current status for this line item. Can be 'new', 'ordered', 'partial', 'complete' or 'cancelled'
line_item_id varchar(35) default NULL, -- Supplier's article id for Edifact orderline
suppliers_reference_number varchar(35) default NULL, -- Suppliers unique edifact quote ref
suppliers_reference_qualifier varchar(3) default NULL, -- Type of number above usually 'QLI'
`suppliers_report` text COLLATE utf8_unicode_ci, -- reports received from suppliers
PRIMARY KEY (`ordernumber`),
KEY `basketno` (`basketno`),
KEY `biblionumber` (`biblionumber`),
@ -3169,6 +3173,62 @@ CREATE TABLE aqorders_transfers (
CONSTRAINT aqorders_transfers_ordernumber_to FOREIGN KEY (ordernumber_to) REFERENCES aqorders (ordernumber) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table vendor_edi_accounts
--
DROP TABLE IF EXISTS vendor_edi_accounts;
CREATE TABLE IF NOT EXISTS vendor_edi_accounts (
id int(11) NOT NULL auto_increment,
description text NOT NULL,
host varchar(40),
username varchar(40),
password varchar(40),
last_activity date,
vendor_id int(11) references aqbooksellers( id ),
download_directory text,
upload_directory text,
san varchar(20),
id_code_qualifier varchar(3) default '14',
transport varchar(6) default 'FTP',
quotes_enabled tinyint(1) not null default 0,
invoices_enabled tinyint(1) not null default 0,
orders_enabled tinyint(1) not null default 0,
responses_enabled tinyint(1) NOT NULL DEFAULT '0',
auto_orders tinyint(1) NOT NULL DEFAULT '0',
shipment_budget integer(11) references aqbudgets( budget_id ),
PRIMARY KEY (id),
KEY vendorid (vendor_id),
KEY shipmentbudget (shipment_budget),
CONSTRAINT vfk_vendor_id FOREIGN KEY ( vendor_id ) REFERENCES aqbooksellers ( id ),
CONSTRAINT vfk_shipment_budget FOREIGN KEY ( shipment_budget ) REFERENCES aqbudgets ( budget_id )
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table edifact_messages
--
DROP TABLE IF EXISTS edifact_messages;
CREATE TABLE IF NOT EXISTS edifact_messages (
id int(11) NOT NULL auto_increment,
message_type varchar(10) NOT NULL,
transfer_date date,
vendor_id int(11) references aqbooksellers( id ),
edi_acct integer references vendor_edi_accounts( id ),
status text,
basketno int(11) references aqbasket( basketno),
raw_msg mediumtext,
filename text,
deleted boolean not null default 0,
PRIMARY KEY (id),
KEY vendorid ( vendor_id),
KEY ediacct (edi_acct),
KEY basketno ( basketno),
CONSTRAINT emfk_vendor FOREIGN KEY ( vendor_id ) REFERENCES aqbooksellers ( id ),
CONSTRAINT emfk_edi_acct FOREIGN KEY ( edi_acct ) REFERENCES vendor_edi_accounts ( id ),
CONSTRAINT emfk_basketno FOREIGN KEY ( basketno ) REFERENCES aqbasket ( basketno )
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table aqinvoices
--
@ -3183,8 +3243,10 @@ CREATE TABLE aqinvoices (
closedate date default NULL, -- invoice close date, NULL means the invoice is open
shipmentcost decimal(28,6) default NULL, -- shipment cost
shipmentcost_budgetid int(11) default NULL, -- foreign key to aqbudgets, link the shipment cost to a budget
message_id int(11) default NULL, -- foreign key to edifact invoice message
PRIMARY KEY (invoiceid),
CONSTRAINT aqinvoices_fk_aqbooksellerid FOREIGN KEY (booksellerid) REFERENCES aqbooksellers (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT edifact_msg_fk FOREIGN KEY ( message_id ) REFERENCES edifact_messages ( id ) ON DELETE SET NULL,
CONSTRAINT aqinvoices_fk_shipmentcost_budgetid FOREIGN KEY (shipmentcost_budgetid) REFERENCES aqbudgets (budget_id) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
@ -3646,6 +3708,20 @@ CREATE TABLE audio_alerts (
KEY precedence (precedence)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table 'edifact_ean'
--
DROP TABLE IF EXISTS edifact_ean;
CREATE TABLE IF NOT EXISTS edifact_ean (
ee_id int(11) NOT NULL AUTO_INCREMENT,
branchcode varchar(10) not null references branches (branchcode),
ean varchar(15) NOT NULL,
id_code_qualifier varchar(3) NOT NULL default '14',
PRIMARY KEY (ee_id),
CONSTRAINT efk_branchcode FOREIGN KEY ( branchcode ) REFERENCES branches ( branchcode )
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;

View File

@ -131,6 +131,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('DumpTemplateVarsIntranet', '0', NULL , 'If enabled, dump all Template Toolkit variable to a comment in the html source for the staff intranet.', 'YesNo'),
('DumpTemplateVarsOpac', '0', NULL , 'If enabled, dump all Template Toolkit variable to a comment in the html source for the opac.', 'YesNo'),
('EasyAnalyticalRecords','0','','If on, display in the catalogue screens tools to easily setup analytical record relationships','YesNo'),
('EDIInvoicesShippingBudget',NULL,NULL,'The budget code used to allocate shipping charges to when processing EDI Invoice messages','free'),
('emailLibrarianWhenHoldIsPlaced','0',NULL,'If ON, emails the librarian whenever a hold is placed','YesNo'),
('EnableAdvancedCatalogingEditor','0','','Enable the Rancor advanced cataloging editor','YesNo'),
('EnableBorrowerFiles','0',NULL,'If enabled, allows librarians to upload and attach arbitrary files to a borrower record.','YesNo'),

View File

@ -28,6 +28,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
(11, 'order_receive', 'Manage orders & basket'),
(11, 'budget_add_del', 'Add and delete budgets (but can''t modify budgets)'),
(11, 'budget_manage_all', 'Manage all budgets'),
(11, 'edi_manage', 'Manage EDIFACT transmissions'),
(13, 'edit_news', 'Write news for the OPAC and staff interfaces'),
(13, 'label_creator', 'Create printable labels and barcodes from catalog and patron data'),
(13, 'edit_calendar', 'Define days when the library is closed'),

View File

@ -9,4 +9,7 @@
[% IF ( CAN_user_parameters ) %]
<li><a href="/cgi-bin/koha/admin/currency.pl">Currencies</a></li>
[% END %]
[% IF CAN_user_acquisition_edi_manage %]
<li><a href="/cgi-bin/koha/acqui/edifactmsgs.pl">Edifact Messages</a></li>
[% END %]
</ul>

View File

@ -55,6 +55,8 @@
<li><a href="/cgi-bin/koha/admin/currency.pl">Currencies and exchange rates</a></li>
<li><a href="/cgi-bin/koha/admin/aqbudgetperiods.pl">Budgets</a></li>
<li><a href="/cgi-bin/koha/admin/aqbudgets.pl">Funds</a></li>
<li><a href="/cgi-bin/koha/admin/edi_accounts.pl">EDI accounts</a></li>
<li><a href="/cgi-bin/koha/admin/edi_ean_accounts.pl">EDI eans</a></li>
</ul>
[% IF CAN_user_plugins %]

View File

@ -51,6 +51,13 @@
window.open(url, 'TransferOrder','width=600,height=400,toolbar=false,scrollbars=yes');
}
function confirm_ediorder() {
var is_confirmed = confirm(_("Are you sure you want to close this basket and generate an Edifact order?"));
if (is_confirmed) {
window.location = "[% script_name %]?op=edi_confirm&basketno=[% basketno %]";
}
}
//]]>
</script>
[% ELSE %]
@ -157,7 +164,7 @@
</div>
[% ELSE %]
<div class="yui-b">
[% UNLESS ( confirm_close ) %]
[% IF !confirm_close && !edi_confirm %]
[% UNLESS ( selectbasketg ) %]
[% UNLESS ( closedate ) %]
<div id="toolbar" class="btn-toolbar">
@ -176,6 +183,9 @@
</div>
[% END %]
<div class="btn-group"><a href="[% script_name %]?op=export&amp;basketno=[% basketno %]&amp;booksellerid=[% booksellerid %]" class="btn btn-small" id="exportbutton"><i class="fa fa-download"></i> Export this basket as CSV</a></div>
[% IF ediaccount %]
<div class="btn-group"><a href="/cgi-bin/koha/acqui/edi_ean.pl?op=ediorder&amp;basketno=[% basketno %]&amp;booksellerid=[% booksellerid %]" class="btn btn-small" id="ediorderbutton"><i class="icon-download"></i>Create edifact order</a></div>
[% END %]
</div>
<!-- Modal for confirm deletion box-->
<div class="modal hide" id="deleteBasketModal" tabindex="-1" role="dialog" aria-labelledby="delbasketModalLabel" aria-hidden="true">
@ -392,6 +402,7 @@
<th>GST %</th>
<th>GST</th>
<th>Fund</th>
<th>Supplier report</th>
[% IF ( active ) %]
[% UNLESS ( closedate ) %]
<th>Modify</th>
@ -415,6 +426,7 @@
<th>&nbsp;</th>
<th>[% foot_loo.gstvalue | $Price %]</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
[% IF ( active ) %]
[% UNLESS ( closedate ) %]
<th>&nbsp;</th>
@ -436,6 +448,7 @@
<th>&nbsp;</th>
<th>[% total_gstvalue | $Price %]</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
[% IF ( active ) %]
[% UNLESS ( closedate ) %]
<th>&nbsp;</th>
@ -509,6 +522,7 @@
<td class="number">[% books_loo.gstrate * 100 | $Price %]</td>
<td class="number [% IF books_loo.gstvalue.search(zero_regex) %]error[% END %]">[% books_loo.gstvalue | $Price %]</td>
<td>[% books_loo.budget_name %]</td>
<td>[% books_loo.suppliers_report %]</td>
[% IF ( active ) %]
[% UNLESS ( closedate ) %]
<td>
@ -670,6 +684,28 @@
</form>
</div>
[% END %]
[% IF edi_confirm %]
<div id="closebasket_needsconfirmation" class="dialog alert">
<form action="/cgi-bin/koha/acqui/basket.pl" class="confirm">
<h1>Are you sure you want to generate an edifact order and close basket [% basketname|html %]?</h1>
[% IF CAN_user_acquisition_group_manage %]
<p>
<label for="createbasketgroup">Attach this basket to a new basket group with the same name</label>
<input type="checkbox" id="createbasketgroup" name="createbasketgroup"/>
</p>
[% END %]
<input type="hidden" id="basketno" value="[% basketno %]" name="basketno" />
<input type="hidden" value="ediorder" name="op" />
<input type="hidden" name="ean" value="[% ean %]" />
<input type="hidden" name="booksellerid" value="[% booksellerid %]" />
<input type="hidden" name="confirm" value="1" />
<input type="hidden" name="basketgroupname" value="[% basketgroupname %]" />
<input type="submit" class="approve" value="Yes, close (Y)" accesskey="y" />
<input type="submit" class="deny" value="No, don't close (N)" accesskey="n" onclick="javascript:window.location='/cgi-bin/koha/acqui/basket.pl?basketno=[% basketno %]';return false;" />
</form>
</div>
[% END %]
</div>
[% END %][%# IF (cannot_manage_basket) %]
</div>

View File

@ -144,6 +144,7 @@ function submitForm(form) {
<div class="btn-group"><a href="[% script_name %]?op=reopen&amp;basketgroupid=[% basketgroupid %]&amp;booksellerid=[% booksellerid %]&amp;mode=singlebg" class="btn btn-small" id="reopenbutton"><i class="fa fa-download"></i> Reopen this basket group</a></div>
<div class="btn-group"><a href="[% script_name %]?op=export&amp;basketgroupid=[% basketgroupid %]&amp;booksellerid=[% booksellerid %]" class="btn btn-small" id="exportbutton"><i class="fa fa-download"></i> Export this basket group as CSV</a></div>
<div class="btn-group"><a href="[% script_name %]?op=print&amp;basketgroupid=[% basketgroupid %]&amp;booksellerid=[% booksellerid %]" class="btn btn-small" id="printbutton"><i class="fa fa-download"></i> Print this basket group in PDF</a></div>
<div class="btn-group"><a href="[% script_name %]?op=ediprint&amp;basketgroupid=[% basketgroupid %]&amp;booksellerid=[% booksellerid %]" class="btn btn-small" id="printbutton"><i class="icon-download"></i>Generate edifact order</a></div>
</div>
[% ELSE %]
<div class="btn-group"><a href="[% script_name %]?op=delete&amp;basketgroupid=[% basketgroupid %]&amp;booksellerid=[% booksellerid %]" class="btn btn-small" id="delbutton"><i class="fa fa-remove"></i> Delete basket group</a></div>
@ -380,6 +381,7 @@ function submitForm(form) {
<form action="/cgi-bin/koha/acqui/basketgroup.pl" method="get"><input type="hidden" name="op" value="reopen" /><input type="hidden" name="booksellerid" value="[% basketgroup.booksellerid %]" /><input type="hidden" name="basketgroupid" value="[% basketgroup.id %]" /><input type="submit" value="Reopen" /></form>
<form action="/cgi-bin/koha/acqui/basketgroup.pl" method="get"><input type="hidden" name="op" value="print" /><input type="hidden" name="basketgroupid" value="[% basketgroup.id %]" /><input type="submit" value="Export as PDF" /></form>
<form action="/cgi-bin/koha/acqui/basketgroup.pl" method="get"><input type="hidden" name="op" value="export" /><input type="hidden" name="basketgroupid" value="[% basketgroup.id %]" /><input type="submit" value="Export as CSV" /></form>
<form action="/cgi-bin/koha/acqui/basketgroup.pl" method="get"><input type="hidden" name="op" value="ediprint" /><input type="hidden" name="basketgroupid" value="[% basketgroup.id %]" /><input type="submit" value="Generate edifact order" /></form>
</td>
</tr>
[% END %]

View File

@ -0,0 +1,38 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Acquisitions &rsaquo; Basket ([% basketno %])</title>
[% INCLUDE 'doc-head-close.inc' %]
</head>
<body>
[% INCLUDE 'header.inc' %]
[% INCLUDE 'acquisitions-search.inc' %]
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/acqui/acqui-home.pl">Acquisitions</a> &rsaquo; Basket ([% basketno %])</div>
<div id="doc3" class="yui-t2">
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<h2>Identify the branch account submitting the EDI order</h2>
<br />
<form action="/cgi-bin/koha/acqui/basket.pl" method="get">
<p>Select ordering branch account: </p>
<select id="ean" name="ean">
[% FOREACH eanacct IN eans %]
<option value="[% eanacct.ean %]">[% eanacct.branch.branchname %] ([% eanacct.ean %])</option>
[% END %]
</select>
<br />
<input type="hidden" id="basketno" value="[% basketno %]" name="basketno" />
<input type="hidden" value="ediorder" name="op" />
<input type="submit" value="Send EDI order" />
</form>
</div>
</div>
<div class="yui-b">
[% INCLUDE 'acquisitions-menu.inc' %]
</div>
</div>
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -0,0 +1,90 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Acquisitions</title>
<link rel="stylesheet" type="text/css" href="[% themelang %]/css/datatables.css" />
[% INCLUDE 'doc-head-close.inc' %]
[% INCLUDE 'datatables.inc' %]
<script type="text/javascript">
$(document).ready(function() {
$('#edi_msgs').dataTable($.extend(true, {}, dataTablesDefaults, {
'aaSorting': [[1, "desc" ]],
'sPaginationType': "four_button"
}));
});
</script>
</head>
<body id="acq_edifactmsgs" class="acq">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'acquisitions-search.inc' %]
<div id="breadcrumbs">
<a href="/cgi-bin/koha/mainpage.pl">Home</a>
&rsaquo; <a href="/cgi-bin/koha/acqui/acqui-home.pl">Acquisitions</a>
&rsaquo; <a href="/cgi-bin/koha/acqui/edifactmsgs.pl">Edifact Messages</a>
</div>
<div id="doc3" class="yui-t2">
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<h1>Edifact Messages</h1>
<div id="acqui_edifactmsgs">
<table id="edi_msgs">
<thead>
<th>Type</th>
<th>Transferred</th>
<th>Status</th>
<th>Vendor</th>
<th>Details</th>
<th>Filename</th>
<th> </th>
<th>Action</th>
</thead>
<tbody>
[% FOREACH msg IN messages %]
<tr>
<td>[% msg.message_type %]</td>
<td>[% msg.transfer_date %]</td>
<td>[% msg.status %]</td>
<td>
<a href="/cgi-bin/koha/acqui/supplier.pl?booksellerid=[% msg.vendor_id %]"</a>
[% msg.vendor.name %]
</td>
<td>
[% IF msg.message_type == 'QUOTE' || msg.message_type == 'ORDERS' %]
[% IF msg.basketno %]
<a href="/cgi-bin/koha/acqui/basket.pl?basketno=[% msg.basketno.basketno %]">
Basket: [% msg.basketno.basketno %]
</a>
[% END %]
[% ELSE %]
<!-- Assuming invoices -->
<a href="/cgi-bin/koha/acqui/invoices.pl?message_id=[% msg.id %]">
Invoices
</a>
[% END %]
</td>
</td>
<td>[% msg.filename %]</td>
<td><a class="popup" target="_blank" title="View Message" href="/cgi-bin/koha/acqui/edimsg.pl?id=[% msg.id %]"</a>View Message</td>
<td>
<a href="/cgi-bin/koha/acqui/edifactmsgs.pl?op=delete&amp;message_id=[% msg.id %]">Delete</a>
</td>
</tr>
[% END %]
</tbody>
</table>
</div>
</div>
</div>
<div class="yui-b">
[% INCLUDE 'acquisitions-menu.inc' %]
</div>
</div>
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -0,0 +1,35 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Acquisitions &rsaquo; Edifact Message Display</title>
[% INCLUDE 'doc-head-close.inc' %]
</head>
<body id="acq_edifactmsgs" class="acq">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'acquisitions-search.inc' %]
<div id="breadcrumbs">
<a href="/cgi-bin/koha/mainpage.pl">Home</a>
&rsaquo; <a href="/cgi-bin/koha/acqui/acqui-home.pl">Acquisitions</a>
&rsaquo; <a href="/cgi-bin/koha/acqui/edifactmsgs.pl">Edifact Messages</a>
</div>
<div id="doc3" class="yui-t2">
[% IF no_message %]
<div class"dialog message">The requested message cannot be displayed</div>
[% ELSE %]
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<ul>
[% FOREACH seg IN segments %]
<li>[% seg | html %]</li>
[% END %]
</ul>
[% END %]
</div>
</div>
<div class="yui-b">
[% INCLUDE 'acquisitions-menu.inc' %]
</div>
</div>
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -97,6 +97,11 @@
<dt><a href="/cgi-bin/koha/admin/aqbudgets.pl">Funds</a></dt>
<dd>Define funds within your budgets</dd>
<dt><a href="/cgi-bin/koha/admin/edi_accounts.pl">EDI Accounts</a></dt>
<dd>Manage vendor EDI accounts for import/export</dd>
<dt><a href="/cgi-bin/koha/admin/edi_ean_accounts.pl">EDI EANs</a></dt>
<dd>Manage Branch EDI EANs</dd>
</dl>
<h3>Additional parameters</h3>

View File

@ -0,0 +1,299 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Administration &rsaquo; EDI accounts
[% IF acct_form %]
[% IF account %]
&rsquo; Modify account
[% ELSE %]
&rsquo; Add new account
[% END %]
[% END %]
[% IF delete_confirm %]
&rsquo; Confirm deletion of account
[% END %]
</title>
[% INCLUDE 'doc-head-close.inc' %]
</head>
<body id="admin_edi_acct" class="admin">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
<div id="breadcrumbs">
<a href="/cgi-bin/koha/mainpage.pl">Home</a>
&rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
&rsaquo; <a href="/cgi-bin/koha/admin/edi_accounts.pl">EDI accounts</a>
[% IF acct_form %]
[% IF account %]
&rsaquo; Modify account
[% ELSE %]
&rsaquo; Add new account
[% END %]
[% ELSIF delete_confirm %]
&rsaquo; Confirm deletion of account
[% ELSE %]
&rsaquo; Accounts
[% END %]
</div>
<div id="doc3" class="yui-t2">
<div id="bd">
<div id="yui-main">
<div class="yui-b">
[% IF display %]
<div id="toolbar" class="btn-toolbar">
<a class="btn btn-small" id="newediacct" href="/cgi-bin/koha/admin/edi_accounts.pl?op=acct_form">
<i class="icon-plus"></i>
New account
</a>
</div>
[% END %]
[% IF acct_form %]
<form action="/cgi-bin/koha/admin/edi_accounts.pl" name="Actform" method="post">
<input type="hidden" name="op" value="save" />
[% IF account %]
<input type="hidden" name="id" value="[% account.id %]" />
[% END %]
<fieldset class="rows">
<legend>
[% IF account %]
Modify account
[% ELSE %]
New account
[% END %]
</legend>
<ol>
<li>
<label for="vendor_id">Vendor: </label>
<select name="vendor_id" id="vendor_id">
[% FOREACH vendor IN vendors %]
[% IF account.vendor_id == vendor.id %]
<option value="[% vendor.id %]" selected="selected">[% vendor.name %]</option>
[% ELSE %]
<option value="[% vendor.id %]">[% vendor.name %]</option>
[% END %]
[% END %]
</select>
</li>
<li>
<label for="description">Description: </label>
<input type="text" name="description" id="description" size="20" maxlength="90" value="[% account.description %]" />
</li>
<li>
[% transport_types = [
'FTP', 'SFTP', 'FILE'
]
%]
<label for="transport">Transport: </label>
<select name="transport" title="valid types of transport are FTP and SFTP"
id="transport">
[% FOREACH transport_type IN transport_types %]
[% IF transport_type == account.transport %]
<option value="[% transport_type %]" selected="selected">[% transport_type %]</option>
[% ELSE %]
<option value="[% transport_type %]">[% transport_type %]</option>
[% END %]
[% END %]
</select>
</li>
<li>
<label for="host">Remote host: </label>
<input type="text" name="host" id="host" size="20" maxlength="90" value="[% account.host %]" />
</li>
<li>
<label for="username">Username: </label>
<input type="text" name="username" id="username" size="20" maxlength="90" value="[% account.username %]" />
</li>
<li>
<label for="password">Password: </label>
<input type="text" name="password" id="password" size="20" maxlength="90" value="[% account.password %]" />
</li>
<li>
<label for="download_directory">Download directory: </label>
<input type="text" name="download_directory" id="download_directory" size="20" maxlength="90"
title="The download directory specifies the directory on the ftpsite from which we download quotes and invoices"
value="[% account.download_directory %]" />
</li>
<li>
<label for="upload_directory">Upload directory: </label>
<input type="text" name="upload_directory" id="upload_directory" size="20" maxlength="90"
title="The upload directory specifies the directory on the ftp site to which we upload orders"
value="[% account.upload_directory %]" />
</li>
<li>
<label for="id_code_qualifier">Qualifier:</label>
<select name="id_code_qualifier" id="id_code_qualifier">
[% FOREACH qualifier IN code_qualifiers %]
[% IF qualifier.code == account.id_code_qualifier %]
<option value="[% qualifier.code %]" selected="selected">
[% qualifier.description %]
</option>
[% ELSE %]
<option value="[% qualifier.code %]">
[% qualifier.description %]
</option>
[% END %]
[% END %]
</select>
</li>
<li>
<label for="san">SAN: </label>
<input type="text" name="san" id="san" size="20" maxlength="90" value="[% account.san %]" />
</li>
<li>
<label for="quotes_enabled">Quotes enabled: </label>
[% IF account.quotes_enabled %]
<input type="checkbox" name="quotes_enabled" id="quotes_enabled" value="[% account.quotes_enabled %]" checked />
[% ELSE %]
<input type="checkbox" name="quotes_enabled" id="quotes_enabled" value="[% account.quotes_enabled %]" />
[% END %]
</li>
<li>
<label for="orders_enabled">Orders enabled: </label>
[% IF account.orders_enabled %]
<input type="checkbox" name="orders_enabled" id="orders_enabled" value="[% account.orders_enabled %]" checked />
[% ELSE %]
<input type="checkbox" name="orders_enabled" id="orders_enabled" value="[% account.orders_enabled %]" />
[% END %]
</li>
<li>
<label for="invoices_enabled">Invoices enabled: </label>
[% IF account.invoices_enabled %]
<input type="checkbox" name="invoices_enabled" id="invoices_enabled" value="[% account.invoices_enabled %]" checked />
[% ELSE %]
<input type="checkbox" name="invoices_enabled" id="invoices_enabled" value="[% account.invoices_enabled %]" />
[% END %]
</li>
<li>
<label for="responses_enabled">Responses enabled: </label>
[% IF account.responses_enabled %]
<input type="checkbox" name="responses_enabled" id="responses_enabled" value="[% account.responses_enabled %]" checked />
[% ELSE %]
<input type="checkbox" name="responses_enabled" id="responses_enabled" value="[% account.responses_enabled %]" />
[% END %]
</li>
<li>
<label for="auto_orders">Automatic ordring (Quotes generate orders without staff intervention): </label>
[% IF account.auto_orders %]
<input type="checkbox" name="auto_orders" id="auto_orders" value="[% account.auto_orders %]" checked />
[% ELSE %]
<input type="checkbox" name="auto_orders" id="auto_orders" value="[% account.auto_orders %]" />
[% END %]
</li>
</ol>
</fieldset>
<fieldset class="action">
<input type="submit" value="Submit" />
<a href="/cgi-bin/koha/admin/edi_accounts.pl" class="cancel">Cancel</a>
</fieldset>
</form>
[% END %]
[% IF delete_confirm %]
<div class="dialog alert">
<h3>Delete this account?</h3>
<table>
<tr>
<th>Vendor</th>
<td>[% account.vendor %]</td>
</tr>
<tr>
<th>Description</th>
<td>[% account.description %]</td>
</tr>
<tr>
<th>SAN</th>
<td>[% account.san %]</td>
</tr>
</table>
<form action="/cgi-bin/koha/admin/edi_accounts.pl" method="post">
<table>
</table>
<input type="hidden" name="op" value="delete_confirmed" />
<input type="hidden" name="id" value="[% account.id %]" />
<input type="submit" class="approve" value="Yes, Delete" />
</form>
<form action="/cgi-bin/koha/admin/edi_accounts.pl" method="get">
<input type="submit" class="deny" value="No, do not Delete" />
</form>
[% END %]
[% IF display %]
<h2>Vendor EDI accounts</h2>
<table>
<tr>
<th>ID</th>
<th>Vendor</th>
<th>Description</th>
<th>Transport</th>
<th>Remote host</th>
<th>Username</th>
<th>Password</th>
<th>Download Directory</th>
<th>Upload Directory</th>
<th>id_code_type</th>
<th>id_code</th>
<th>Quotes</th>
<th>Orders</th>
<th>Invoices</th>
<th>Responses</th>
<th>Auto ordering</th>
<th>Actions</th>
</tr>
[% FOREACH account IN ediaccounts %]
[% IF loop.even %]<tr>
[% ELSE %]<tr class="highlight">
[% END %]
<td>[% account.id %]</td>
<td><a href="/cgi-bin/koha/acqui/supplier.pl?supplierid=[% account.vendor_id %]">[% account.vendor.name %]</a></td>
<td>[% account.description %]</td>
<td>[% account.transport %]</td>
<td>[% account.host %]</td>
<td>[% account.username %]</td>
<td>[% IF account.password %]xxxxx[% END %]</td>
<td>[% account.download_directory %]</td>
<td>[% account.upload_directory %]</td>
<td>[% account.id_code_qualifier %]</td>
<td>[% account.san %]</td>
[% IF account.quotes_enabled %]
<td>Y</td>
[% ELSE %]
<td>N</td>
[% END %]
[% IF account.orders_enabled %]
<td>Y</td>
[% ELSE %]
<td>N</td>
[% END %]
[% IF account.invoices_enabled %]
<td>Y</td>
[% ELSE %]
<td>N</td>
[% END %]
[% IF account.responses_enabled %]
<td>Y</td>
[% ELSE %]
<td>N</td>
[% END %]
[% IF account.auto_orders %]
<td>Y</td>
[% ELSE %]
<td>N</td>
[% END %]
<td align="center">
<a href="/cgi-bin/koha/admin/edi_accounts.pl?op=acct_form&id=[% account.id %]">Edit</a> | <a href="/cgi-bin/koha/admin/edi_accounts.pl?op=delete_confirm&id=[% account.id %]">Delete</a>
</td>
</tr>
[% END %]
</table>
[% END %]
</div>
</div>
<div class="yui-b">
[% INCLUDE 'admin-menu.inc' %]
</div>
</div>
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -0,0 +1,153 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Administration &rsaquo; EDI EANs</title>
[% IF ean_form %]
[% IF ean %]
&rsquo; Modify branch EAN
[% ELSE %]
&rsquo; Add new branch EAN
[% END %]
[% END %]
[% IF delete_confirm %]
&rsquo; Confirm deletion of EAN
[% END %]
</title>
[% INCLUDE 'doc-head-close.inc' %]
</head>
<body id="admin_edi_ean" class="admin">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
<div id="breadcrumbs">
<a href="/cgi-bin/koha/mainpage.pl">Home</a>
&rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
&rsaquo; <a href="/cgi-bin/koha/admin/edi_ean_accounts.pl">EDI EANs</a>
[% IF ean_form %]
[% IF ean %]
&rsaquo; Modify branch EAN
[% ELSE %]
&rsaquo; Add new branch EAN
[% END %]
[% ELSIF delete_confirm %]
&rsaquo; Confirm deletion of Ean
[% ELSE %]
&rsaquo; Branch EANs
[% END %]
</div>
<div id="doc3" class="yui-t2">
<div id="bd">
<div id="yui-main">
<div class="yui-b">
[% IF display %]
<div id="toolbar" class="btn-toolbar">
<a class="btn btn-small" id="newediean" href="/cgi-bin/koha/admin/edi_ean_accounts.pl?op=ean_form">
<i class="icon-plus"></i>
New EAN
</a>
</div>
[% END %]
[% IF ean_form %]
<form action="/cgi-bin/koha/admin/edi_ean_accounts.pl" name="Eanform" method="post">
<input type="hidden" name="op" value="save" />
[% IF ean %]
<input type="hidden" name="oldbranchcode" value="[% branchcode %]" />
<input type="hidden" name="oldean" value="[% ean %]" />
[% END %]
<fieldset class="rows">
<legend>
[% IF ean %]
Modify EAN
[% ELSE %]
New EAN
[% END %]
</legend>
<ol>
<li>
<label for="branchcode">Branchcode: </label>
<select name="branchcode" id="branchcode">
[% FOREACH branch IN branches %]
[% IF branch.branchcode == ean.branch.branchcode %]
<option value="[% branch.branchcode %]" selected="selected">[% branch.branchname %]</option>
[% ELSE %]
<option value="[% branch.branchcode %]">[% branch.branchname %]</option>
[% END %]
[% END %]
</select>
</li>
<li>
<label for="ean">EAN: </label>
<input type="text" name="ean" id="ean`" size="20" maxlength="90" value="[% ean.ean %]" />
</li>
<li>
<label for="id_code_qualifier">
<select name="id_code_qualifier" id="id_code_qualifier">
[% FOREACH qualifier IN code_qualifiers %]
[% IF qualifier.code == ean.id_code_qualifier %]
<option value="[% qualifier.code %]" selected="selected">
[% qualifier.description %]
</option>
[% ELSE %]
<option value="[% qualifier.code %]">
[% qualifier.description %]
</option>
[% END %]
[% END %]
</li>
</ol>
</fieldset>
<fieldset class="action">
<input type="submit" value="Submit"/>
<a href="/cgi-bin/koha/admin/edi_ean_accounts.pl" class="cancel">Cancel</a>
</fieldset>
</form>
[% END %]
[% IF delete_confirm %]
<div class="dialog alert">
<h3>Delete EAN [% ean.ean %] for branch [% ean.branch.branchname %]?</h3>
<form action="/cgi-bin/koha/admin/edi_ean_accounts.pl" method="post">
<input type="hidden" name="op" value="delete_confirmed" />
<input type="hidden" name="branchcode" value="[% ean.branch.branchcode %]" />
<input type="hidden" name="ean" value="[% ean.ean %]" />
<input type="submit" class="approve" value="Yes, Delete" />
</form>
<form action="/cgi-bin/koha/admin/edi_ean_accounts.pl" method="get">
<input type="submit" class="deny" value="No, do not Delete" />
</form>
</div>
[% END %]
[% IF display %]
<h2>Branch EANs</h2>
<table>
<tr>
<th>Branch</th>
<th>EAN</th>
<th>Code Type</th>
<th>Actions</th>
</tr>
[% FOREACH ean IN eans %]
[% IF loop.even %]<tr>
[% ELSE %]<tr class="highlight">
[% END %]
<td>[% ean.branch.branchname %]</td>
<td>[% ean.ean %]</td>
<td>[% ean.id_code_qualifier %]</td>
<td align="center">
<a href="/cgi-bin/koha/admin/edi_ean_accounts.pl?op=ean_form&branchcode=[% ean.branch.branchcode %]&ean=[% ean.ean %]">Edit</a> | <a href="/cgi-bin/koha/admin/edi_ean_accounts.pl?op=delete_confirm&branchcode=[% ean.branch.branchcode %]&ean=[% ean.ean %]">Delete</a></td>
</tr>
[% END %]
</table>
[% END %]
</div>
</div>
<div class="yui-b">
[% INCLUDE 'admin-menu.inc' %]
</div>
</div>
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -104,6 +104,11 @@
<dd>Use tool plugins</dd>
[% END %]
[% IF CAN_user_acquisition_edi_manage %]
<dt><a href="/cgi-bin/koha/tools/edi.pl">EDIfact messages</a></dt>
<dd>Manage EDIfact transmissions</dd>
[% END %]
</dl>
</div>
<div class="yui-u">

163
misc/cronjobs/edi_cron.pl Executable file
View File

@ -0,0 +1,163 @@
#!/usr/bin/perl
#
# Copyright 2013,2014,2015 PTFS Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use warnings;
use strict;
use utf8;
# Handles all the edi processing for a site
# loops through the vendor_edifact records and uploads and downloads
# edifact files if the appropriate type is enabled
# downloaded quotes, invoices and responses are processed here
# if orders are enabled and present they are generated and sent
# can be run as frequently as required
# log messages are appended to logdir/editrace.log
use C4::Context;
use Log::Log4perl qw(:easy);
use Koha::Database;
use Koha::EDI qw( process_quote process_invoice process_ordrsp);
use Koha::Edifact::Transport;
use Fcntl qw( :DEFAULT :flock :seek );
my $logdir = C4::Context->logdir;
# logging set to trace as this may be what you
# want on implementation
Log::Log4perl->easy_init(
{
level => $TRACE,
file => ">>$logdir/editrace.log",
}
);
# we dont have a lock dir in context so use the logdir
my $pidfile = "$logdir/edicron.pid";
my $pid_handle = check_pidfile();
my $schema = Koha::Database->new()->schema();
my @edi_accts = $schema->resultset('VendorEdiAccount')->all();
my $logger = Log::Log4perl->get_logger();
for my $acct (@edi_accts) {
if ( $acct->quotes_enabled ) {
my $downloader = Koha::Edifact::Transport->new( $acct->id );
$downloader->download_messages('QUOTE');
}
if ( $acct->invoices_enabled ) {
my $downloader = Koha::Edifact::Transport->new( $acct->id );
$downloader->download_messages('INVOICE');
}
if ( $acct->orders_enabled ) {
# select pending messages
my @pending_orders = $schema->resultset('EdifactMessage')->search(
{
message_type => 'ORDERS',
vendor_id => $acct->vendor_id,
status => 'Pending',
}
);
my $uploader = Koha::Edifact::Transport->new( $acct->id );
$uploader->upload_messages(@pending_orders);
}
if ( $acct->responses_enabled ) {
my $downloader = Koha::Edifact::Transport->new( $acct->id );
$downloader->download_messages('ORDRSP');
}
}
# process any downloaded quotes
my @downloaded_quotes = $schema->resultset('EdifactMessage')->search(
{
message_type => 'QUOTE',
status => 'new',
}
)->all;
foreach my $quote_file (@downloaded_quotes) {
my $filename = $quote_file->filename;
$logger->trace("Processing quote $filename");
process_quote($quote_file);
}
# process any downloaded invoices
my @downloaded_invoices = $schema->resultset('EdifactMessage')->search(
{
message_type => 'INVOICE',
status => 'new',
}
)->all;
foreach my $invoice (@downloaded_invoices) {
my $filename = $invoice->filename();
$logger->trace("Processing invoice $filename");
process_invoice($invoice);
}
my @downloaded_responses = $schema->resultset('EdifactMessage')->search(
{
message_type => 'ORDRSP',
status => 'new',
}
)->all;
foreach my $response (@downloaded_responses) {
my $filename = $response->filename();
$logger->trace("Processing order response $filename");
process_ordrsp($response);
}
if ( close $pid_handle ) {
unlink $pidfile;
exit 0;
}
else {
$logger->error("Error on pidfile close: $!");
exit 1;
}
sub check_pidfile {
# sysopen my $fh, $pidfile, O_EXCL | O_RDWR or log_exit "$0 already running"
sysopen my $fh, $pidfile, O_RDWR | O_CREAT
or log_exit("$0: open $pidfile: $!");
flock $fh => LOCK_EX or log_exit("$0: flock $pidfile: $!");
sysseek $fh, 0, SEEK_SET or log_exit("$0: sysseek $pidfile: $!");
truncate $fh, 0 or log_exit("$0: truncate $pidfile: $!");
print $fh "$$\n" or log_exit("$0: print $pidfile: $!");
return $fh;
}
sub log_exit {
my $error = shift;
$logger->error($error);
exit 1;
}

View File

@ -0,0 +1,41 @@
#!/usr/bin/perl
use strict;
use warnings;
use C4::Context;
# this script will remove those older than 5 days
my $tmpdir = '/tmp';
#
opendir( my $dh, $tmpdir) || die "Cannot open $tmpdir : $!";
my @files_in_tmp = grep { /\.CE[IQ]$/ && -f "$tmpdir/$_" } readdir($dh);
closedir $dh;
my $dbh = C4::Context->dbh;
my $query =<<'ENDSQL';
select filename from edifact_messages
where message_type IN ('QUOTE','INVOICE')
and datediff( CURDATE(), transfer_date ) > 5
ENDSQL
my $ingested;
@{$ingested} = $dbh->selectcol_arrayref($query);
my %ingested_hash = map { $_ => 1 } @{$ingested};
my @delete_list;
foreach (@files_in_tmp) {
if ( exists $ingested_hash{$_} ) {
push @delete_list, $_;
}
}
if ( @delete_list ) {
chdir $tmpdir;
unlink @delete_list;
}

75
t/EdiInvoice.t Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/perl
use strict;
use warnings;
use FindBin qw( $Bin );
use Test::More tests => 19;
BEGIN { use_ok('Koha::Edifact') }
my $invoice_file = "$Bin/edi_testfiles/BLSINV337023.CEI";
my $invoice = Koha::Edifact->new( { filename => $invoice_file, } );
isa_ok( $invoice, 'Koha::Edifact' );
my $x = $invoice->interchange_header('sender');
my $control_reference = '337023';
is( $x, '5013546025078', "sender returned" );
$x = $invoice->interchange_header('recipient');
is( $x, '5013546121974', "recipient returned" );
$x = $invoice->interchange_header('datetime');
is( $x->[0], '140729', "datetime returned" );
$x = $invoice->interchange_header('interchange_control_reference');
is( $x, $control_reference, "interchange_control_reference returned" );
$x = $invoice->interchange_header('application_reference');
is( $x, 'INVOIC', "application_reference returned" );
$x = $invoice->interchange_trailer('interchange_control_count');
is( $x, 6, "interchange_control_count returned" );
my $messages = $invoice->message_array();
# check inv number from BGM
my $msg_count = @{$messages};
is( $msg_count, 6, 'correct message count returned' );
is( $messages->[0]->message_type, 'INVOIC', 'Message shows correct type' );
my $expected_date = '20140729';
is( $messages->[0]->message_date,
$expected_date, 'Message date correctly returned' );
is( $messages->[0]->tax_point_date,
$expected_date, 'Tax point date correctly returned' );
my $expected_invoicenumber = '01975490';
my $invoicenumber = $messages->[1]->docmsg_number();
is( $messages->[0]->buyer_ean, '5013546121974', 'Buyer ean correct' );
is( $messages->[0]->supplier_ean, '5013546025078', 'Supplier ean correct' );
is( $invoicenumber, $expected_invoicenumber,
'correct invoicenumber extracted' );
my $lines = $messages->[1]->lineitems();
my $num_lines = @{$lines};
is( $num_lines, 8, "Correct number of lineitems returned" );
# sample invoice was from an early version where order was formatted basketno/ordernumber
my $expected_ordernumber = '2818/74593';
my $ordernumber = $lines->[7]->ordernumber;
is( $ordernumber, $expected_ordernumber, 'correct ordernumber returned' );
my $lineprice = $lines->[7]->price_net;
is( $lineprice, 4.55, 'correct net line price returned' );
my $tax = $lines->[7]->tax;
is( $tax, 0, 'correct tax amount returned' );

121
t/Edifact.t Executable file
View File

@ -0,0 +1,121 @@
#!/usr/bin/perl
use strict;
use warnings;
use FindBin qw( $Bin );
use Test::More tests => 34;
BEGIN { use_ok('Koha::Edifact') }
my $filename = "$Bin/edi_testfiles/prquotes_73050_20140430.CEQ";
my $quote = Koha::Edifact->new( { filename => $filename, } );
isa_ok( $quote, 'Koha::Edifact' );
my $x = $quote->interchange_header('sender');
is( $x, '5013546027856', "sender returned" );
$x = $quote->interchange_header('recipient');
is( $x, '5030670137480', "recipient returned" );
$x = $quote->interchange_header('datetime');
is( $x->[0], '140430', "datetime returned" );
my $control_reference = 'EDIQ2857763';
$x = $quote->interchange_header('interchange_control_reference');
is( $x, $control_reference, "interchange_control_reference returned" );
$x = $quote->interchange_header('application_reference');
is( $x, 'QUOTES', "application_reference returned" );
$x = $quote->interchange_trailer('interchange_control_count');
is( $x, 1, "interchange_control_count returned" );
my $msgs = $quote->message_array();
my $msg_count = @{$msgs};
is( $msg_count, 1, "correct message count returned" );
my $m = $msgs->[0];
is( $m->message_type, 'QUOTES', "Message shows correct type" );
is( $m->message_reference_number,
'MQ09791', "Message reference number returned" );
is( $m->docmsg_number, 'Q741588', "Message docmsg number returned" );
is( $m->message_date, '20140430', "Message date returned" );
my $lin = $m->lineitems();
my $num_lines = @{$lin};
is( $num_lines, 18, 'Correct number of lines in message' );
my $test_line = $lin->[-1];
is( $test_line->line_item_number, 18, 'correct line number returned' );
is( $test_line->item_number_id, '9780273761006', 'correct ean returned' );
is( $test_line->quantity, 1, 'quantity returned' );
my $test_title = 'International business [electronic resource]';
my $marcrec = $test_line->marc_record;
isa_ok( $marcrec, 'MARC::Record' );
my $title = $test_line->title();
# also tests components are concatenated
is( $title, $test_title, "Title returned" );
# problems currently with the record (needs leader ??)
#is( $marcrec->title(), $test_title, "Title returned from marc");
my $test_author = q{Rugman, Alan M.};
is( $test_line->author, $test_author, "Author returned" );
is( $test_line->publisher, 'Pearson Education', "Publisher returned" );
is( $test_line->publication_date, q{2012.}, "Pub. date returned" );
#
# Test data encoded in GIR
#
my $stock_category = $test_line->girfield('stock_category');
is( $stock_category, 'EBOOK', "stock_category returned" );
my $branch = $test_line->girfield('branch');
is( $branch, 'ELIB', "branch returned" );
my $fund_allocation = $test_line->girfield('fund_allocation');
is( $fund_allocation, '660BOO_2013', "fund_allocation returned" );
my $collection_code = $test_line->girfield('collection_code');
is( $collection_code, 'EBOO', "collection_code returned" );
#my $shelfmark = $test_line->girfield('shelfmark');
#my $classification = $test_line->girfield('classification');
## text the free_text returned from the line
my $test_line_2 = $lin->[12];
my $ftx_string = 'E*610.72* - additional items';
is( $test_line_2->orderline_free_text, $ftx_string, "ftx note retrieved" );
my $filename2 = "$Bin/edi_testfiles/QUOTES_413514.CEQ";
my $q2 = Koha::Edifact->new( { filename => $filename2, } );
my $messages = $q2->message_array();
my $orderlines = $messages->[0]->lineitems();
my $ol = $orderlines->[0];
my $y = $ol->girfield( 'copy_value', 5 );
is( $y, undef, 'No extra item generated' );
$y = $ol->girfield( 'copy_value', 1 );
is( $y, '16.99', 'Copy Price returned' );
$y = $ol->girfield( 'classification', 4 );
is( $y, '914.1061', 'Copy classification returned' );
$y = $ol->girfield( 'fund_allocation', 4 );
is( $y, 'REF', 'Copy fund returned' );
$y = $ol->girfield( 'branch', 4 );
is( $y, 'SOU', 'Copy Branch returned' );
$y = $ol->girfield( 'collection_code', 4 );
is( $y, 'ANF', 'Collection code returned' );
$y = $ol->girfield( 'stock_category', 4 );
is( $y, 'RS', 'Copy stock category returned' );

56
t/Ediorder.t Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/perl
use strict;
use warnings;
use FindBin qw( $Bin );
use Test::More tests => 6;
BEGIN { use_ok('Koha::Edifact::Order') }
# The following tests are for internal methods but they could
# error spectacularly so yest
# Check that quoting is done correctly
#
my $processed_text =
Koha::Edifact::Order::encode_text(q{string containing ?,',:,+});
cmp_ok(
$processed_text, 'eq',
q{string containing ??,?',?:,?+},
'Outgoing text correctly quoted'
);
# extend above test to test chunking in imd_segment
#
my $code = '010';
my $data_to_encode = $processed_text;
my @segs = Koha::Edifact::Order::imd_segment( $code, $data_to_encode );
my $testseg = "IMD+L+010+:::$processed_text";
$testseg .= q{'}; # add segment terminator
cmp_ok( $segs[0], 'eq', $testseg, 'IMD segment correctly formed' );
$data_to_encode = 'A' x 35;
$data_to_encode .= 'B' x 35;
$data_to_encode .= 'C' x 10;
@segs = Koha::Edifact::Order::imd_segment( $code, $data_to_encode );
cmp_ok(
$segs[0],
'eq',
q{IMD+L+010+:::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'},
'IMD segment correctly chunked'
);
cmp_ok( $segs[1], 'eq', q{IMD+L+010+:::CCCCCCCCCC'},
'IMD segment correctly split across segments' );
$data_to_encode .= '??';
# this used to cause an infinite loop
@segs = Koha::Edifact::Order::imd_segment( $code, $data_to_encode );
cmp_ok( $segs[1], 'eq', q{IMD+L+010+:::CCCCCCCCCC??'},
'IMD segment deals with quoted character at end' );

59
t/Ediordrsp.t Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/perl
use strict;
use warnings;
use FindBin qw( $Bin );
use Test::More tests => 16;
BEGIN { use_ok('Koha::Edifact') }
my $filedir = "$Bin/edi_testfiles";
my @files = map { "$filedir/$_" }
( 'ordrsp1.CEA', 'ordrsp2.CEA', 'ordrsp3.CEA', 'ordrsp4.CEA' );
my @responses;
for my $filename (@files) {
my $order_response = Koha::Edifact->new( { filename => $filename, } );
isa_ok( $order_response, 'Koha::Edifact' );
push @responses, $order_response;
}
# tests on file 1
# Order accepted with amendments
my $order_response = $responses[0];
my $msg = $order_response->message_array();
my $no_of_msg = @{$msg};
is( $no_of_msg, 1, "Correct number of messages returned" );
isa_ok( $msg->[0], 'Koha::Edifact::Message' );
my $lines = $msg->[0]->lineitems();
my $no_of_lines = @{$lines};
is( $no_of_lines, 3, "Correct number of orderlines returned" );
#
is( $lines->[0]->ordernumber(), 'P28837', 'Line 1 correct ordernumber' );
is(
$lines->[0]->coded_orderline_text(),
'Not yet published',
'NP returned and translated'
);
is( $lines->[1]->ordernumber(), 'P28838', 'Line 2 correct ordernumber' );
is( $lines->[1]->action_notification(),
'cancelled', 'Cancelled action returned' );
is( $lines->[1]->coded_orderline_text(),
'Out of print', 'OP returned and translated' );
is( $lines->[2]->ordernumber(), 'P28846', 'Line 3 correct ordernumber' );
is( $lines->[2]->action_notification(),
'recorded', 'Accepted with change action returned' );
is( $lines->[0]->availability_date(), '19971120',
'Availability date returned' );

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
UNA:+.? 'UNB+UNOC:3+4012345000094+5412345000176+140430:1849+EDIQ2857776++ORDRSP'UNH+ME001234+ORDRSP:D:96A:UN:EAN005'BGM+231+R967634+4'DTM+137:19971028:102'NAD+BY+5412345000176::9'NAD+SU+4012345000094::9'CUX+2:GBP:9'LIN+1+24'PIA+5+0316907235:IB'QTY+21:2'QTY+83:2'DTM+44:19971120:102'FTX+LIN++NP:8B:28'PRI+AAE:15.99:CA:SRP'RFF+LI:P28837'LIN+2+2'PIA+5+0856674427:IB'QTY+21:1'FTX+LIN++OP:8B:28'RFF+LI:P28838'LIN+3+24'PIA+5+0870701436:IB'PIA+3+0870701428:IB'QTY+21:1'FTX+LIN++OP:8B:28'PRI+AAE:25:CA:SRP'RFF+LI:P28846'UNS+S'CNT+2:3'UNT+29+ME001234'UNZ+1+EDIQ2857776'

View File

@ -0,0 +1 @@
UNA:+.? 'UNB+UNOC:3+4012345000094+5412345000176+140430:1849+EDIQ2857775++ORDRSP'UNH+ME001235+ORDRSP:D:96A:UN:EAN005'BGM+231+R967635+27'DTM+137:19971028:102'FTX+GEN++ACS:9B:28'RFF+ON:H67209'NAD+BY+5412345000176::9'NAD+SU+4012345000094::9'UNS+S'CNT+2:0'UNT+10+ME001235'UNZ+1+EDIQ2857775'

View File

@ -0,0 +1 @@
UNA:+.? 'UNB+UNOC:3+4012345000094+5412345000176+140430:1849+EDIQ2857774++ORDRSP'UNH+ME001236+ORDRSP:D:96A:UN:EAN005'BGM+23C+R967635+4'DTM+137:20010624:102'NAD+BY+5412345000176::9'NAD+SU+4012345000094::9'LIN+1+4'PIA+5+00076511236:IB'QTY+21:3’GIR+001+5346:LCO+1000431:LAC+FIC:LFN+AN:LLO'GIR+002+5347:LCO+1000432:LAC+FIC:LFN+AN:LLO'GIR+003+5348:LCO+1000433:LAC+FIC:LFN+BB:LLO'RFF+LI:0190045'LIN+2+4’PIA+5+0863183913:IB'QTY+21:1'GIR+001+6210:LCO+1000434:LAC+FIC:LFN+BB:LLO+398:LCL'GIR+001+JON:LFS+14DAY:LLN'RFF+LI:0190056'UNS+S'CNT+2:2'UNT+21:ME001236'UNZ+1+EDIQ2857774'

View File

@ -0,0 +1 @@
UNA:+.? 'UNB+UNOC:3+4012345000094+5412345000176+140430:1849+EDIQ2857773++ORDRSP'UNH+ME001345+ORDRSP:D:96A:UN:EAN005'BGM+23C+F964312+4'DTM+137:20010430:102'NAD+BY+5412345000176::9'NAD+SU+4012345000094::9'LIN+1+4'PIA+5+0007107781:IB'QTY+21:4'GIR+L01+214365:LAC+214366:LAC+DA:LLO+920:LCL+SEC:LFS'GIR+L01+NFIC:LFS'GIR+L02+214367:LAC+214368:LAC+FG:LLO+920:LCL+SEC:LFS'GIR+L02+NFIC:LFS'RFF+LI:0184364’LOC+7+BR1::92'QTY+11:2'LOC+20+FG::92'QTY+11:2'UNS+S'CNT+2:1'UNT+20:ME001345'UNZ+1+EDIQ2857773'

File diff suppressed because one or more lines are too long