edi4r 0.9.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/AuthorCopyright +10 -0
- data/COPYING +56 -0
- data/ChangeLog +106 -0
- data/README +66 -0
- data/TO-DO +35 -0
- data/Tutorial +609 -0
- data/VERSION +1 -0
- data/bin/edi2xml.rb +103 -0
- data/bin/editool.rb +151 -0
- data/bin/xml2edi.rb +50 -0
- data/data/edifact/iso9735/SDCD.10000.csv +10 -0
- data/data/edifact/iso9735/SDCD.20000.csv +10 -0
- data/data/edifact/iso9735/SDCD.30000.csv +11 -0
- data/data/edifact/iso9735/SDCD.40000.csv +31 -0
- data/data/edifact/iso9735/SDCD.40100.csv +31 -0
- data/data/edifact/iso9735/SDED.10000.csv +37 -0
- data/data/edifact/iso9735/SDED.20000.csv +37 -0
- data/data/edifact/iso9735/SDED.30000.csv +43 -0
- data/data/edifact/iso9735/SDED.40000.csv +129 -0
- data/data/edifact/iso9735/SDED.40100.csv +130 -0
- data/data/edifact/iso9735/SDMD.10000.csv +0 -0
- data/data/edifact/iso9735/SDMD.20000.csv +0 -0
- data/data/edifact/iso9735/SDMD.30000.csv +6 -0
- data/data/edifact/iso9735/SDMD.40000.csv +17 -0
- data/data/edifact/iso9735/SDMD.40100.csv +17 -0
- data/data/edifact/iso9735/SDSD.10000.csv +8 -0
- data/data/edifact/iso9735/SDSD.20000.csv +8 -0
- data/data/edifact/iso9735/SDSD.30000.csv +12 -0
- data/data/edifact/iso9735/SDSD.40000.csv +34 -0
- data/data/edifact/iso9735/SDSD.40100.csv +34 -0
- data/data/edifact/untdid/EDCD.d01b.csv +200 -0
- data/data/edifact/untdid/EDCD.d96a.csv +161 -0
- data/data/edifact/untdid/EDED.d01b.csv +641 -0
- data/data/edifact/untdid/EDED.d96a.csv +462 -0
- data/data/edifact/untdid/EDMD.d01b.csv +3419 -0
- data/data/edifact/untdid/EDMD.d96a.csv +2144 -0
- data/data/edifact/untdid/EDSD.d01b.csv +158 -0
- data/data/edifact/untdid/EDSD.d96a.csv +127 -0
- data/data/edifact/untdid/IDCD.d01b.csv +95 -0
- data/data/edifact/untdid/IDMD.d01b.csv +238 -0
- data/data/edifact/untdid/IDSD.d01b.csv +75 -0
- data/lib/edi4r.rb +928 -0
- data/lib/edi4r/diagrams.rb +567 -0
- data/lib/edi4r/edi4r-1.2.dtd +20 -0
- data/lib/edi4r/edifact-rexml.rb +221 -0
- data/lib/edi4r/edifact.rb +1627 -0
- data/lib/edi4r/rexml.rb +256 -0
- data/lib/edi4r/standards.rb +495 -0
- data/test/eancom2webedi.rb +380 -0
- data/test/groups.edi +1 -0
- data/test/in1.edi +1 -0
- data/test/in1.inh +3 -0
- data/test/in2.edi +1 -0
- data/test/in2.xml +350 -0
- data/test/test_basics.rb +209 -0
- data/test/test_edi_split.rb +53 -0
- data/test/test_loopback.rb +21 -0
- data/test/test_minidemo.rb +84 -0
- data/test/test_rexml.rb +98 -0
- data/test/test_tut_examples.rb +131 -0
- data/test/webedi2eancom.rb +408 -0
- metadata +110 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
<!ELEMENT Interchange (Header?, (Message|MsgGroup)*, Trailer?)>
|
2
|
+
<!ELEMENT MsgGroup (Header, Message+, Trailer?)>
|
3
|
+
<!ELEMENT Message (Header, (Segment|SegmentGroup)+, Trailer?)>
|
4
|
+
<!ELEMENT Header (Segment, Parameter*)>
|
5
|
+
<!ELEMENT Trailer (Segment, Parameter*)>
|
6
|
+
<!ELEMENT SegmentGroup (Segment|SegmentGroup)+>
|
7
|
+
<!ELEMENT Segment (CDE | DE)+>
|
8
|
+
<!ELEMENT CDE (DE+)>
|
9
|
+
<!ELEMENT DE (#PCDATA)>
|
10
|
+
<!ELEMENT Parameter (#PCDATA)>
|
11
|
+
|
12
|
+
<!ATTLIST Parameter name NMTOKEN #REQUIRED>
|
13
|
+
<!ATTLIST DE name NMTOKEN #REQUIRED
|
14
|
+
instance NMTOKEN "1">
|
15
|
+
<!ATTLIST CDE name NMTOKEN #REQUIRED
|
16
|
+
instance NMTOKEN "1">
|
17
|
+
<!ATTLIST Segment name NMTOKEN #REQUIRED>
|
18
|
+
<!ATTLIST SegmentGroup name NMTOKEN #REQUIRED>
|
19
|
+
<!ATTLIST Interchange standard_key (E|I) #REQUIRED
|
20
|
+
version CDATA #REQUIRED>
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# UN/EDIFACT add-ons to EDI module,
|
2
|
+
# Methods for XML support for the UN/EDIFACT module
|
3
|
+
#
|
4
|
+
# :include: ../../AuthorCopyright
|
5
|
+
#
|
6
|
+
# $Id: edifact-rexml.rb,v 1.1 2006/08/01 11:14:18 werntges Exp $
|
7
|
+
#--
|
8
|
+
# $Log: edifact-rexml.rb,v $
|
9
|
+
# Revision 1.1 2006/08/01 11:14:18 werntges
|
10
|
+
# Initial revision
|
11
|
+
#
|
12
|
+
#
|
13
|
+
# Derived from "edifact.rb" (precursor) by HWW
|
14
|
+
#
|
15
|
+
# To-do list:
|
16
|
+
# SV4 - Support & testing
|
17
|
+
#++
|
18
|
+
#
|
19
|
+
# This is the XML add-on for UN/EDIFACT module of edi4r (hence '::E')
|
20
|
+
#
|
21
|
+
# It leaves all real work to the base classes. Only the UNA information
|
22
|
+
# is treated in a special way (as a "Parameter" element of the header)
|
23
|
+
# and dealt with here.
|
24
|
+
|
25
|
+
module EDI::E
|
26
|
+
|
27
|
+
class Interchange
|
28
|
+
#
|
29
|
+
# Returns a REXML document that represents the interchange
|
30
|
+
#
|
31
|
+
# xdoc:: REXML document that contains the XML representation of
|
32
|
+
# a UN/EDIFACT interchange
|
33
|
+
#
|
34
|
+
def Interchange.parse_xml( xdoc )
|
35
|
+
_root = xdoc.root
|
36
|
+
_header = _root.elements["Header"]
|
37
|
+
_trailer = _root.elements["Trailer"]
|
38
|
+
_una = _header.elements["Parameter[@name='UNA']"]
|
39
|
+
_una = _una.text if _una
|
40
|
+
raise "Empty UNA" if _una and _una.empty? # remove later!
|
41
|
+
# S001: Works for both batch and interactive EDI:
|
42
|
+
_s001 = _header.elements["Segment/CDE[@name='S001']"]
|
43
|
+
_version = _s001.elements["DE[@name='0002']"].text.to_i
|
44
|
+
_charset = _s001.elements["DE[@name='0001']"].text
|
45
|
+
params = { :charset => _charset, :version => _version }
|
46
|
+
if _una
|
47
|
+
params[:una_string] = _una
|
48
|
+
params[:show_una] = true
|
49
|
+
end
|
50
|
+
ic = Interchange.new( params )
|
51
|
+
if _root.elements["Message"].nil? # correct ??
|
52
|
+
_root.elements.each('MsgGroup') do |xel|
|
53
|
+
ic.add( MsgGroup.parse_xml( ic, xel ), false )
|
54
|
+
end
|
55
|
+
else
|
56
|
+
_root.elements.each('Message') do |xel|
|
57
|
+
ic.add( Message.parse_xml( ic, xel ), false )
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ic.header = Segment.parse_xml( ic, _header.elements["Segment"] )
|
62
|
+
ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )
|
63
|
+
ic.validate
|
64
|
+
ic
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Read +maxlen+ bytes from $stdin (default) or from given stream
|
69
|
+
# (UN/EDIFACT data expected), and peek into first segment (UNB/UIB).
|
70
|
+
#
|
71
|
+
# Returns an empty Interchange object with a properly header filled.
|
72
|
+
#
|
73
|
+
# Intended use:
|
74
|
+
# Efficient routing by reading just UNB data: sender/recipient/ref/test
|
75
|
+
#
|
76
|
+
def Interchange.peek_xml(xdoc) # Handle to REXML document
|
77
|
+
_root = xdoc.root
|
78
|
+
_header = _root.elements["Header"]
|
79
|
+
_trailer = _root.elements["Trailer"]
|
80
|
+
_una = _header.elements["Parameter[@name='UNA']"]
|
81
|
+
_una = _una.text if _una
|
82
|
+
raise "Empty UNA" if _una and _una.empty? # remove later!
|
83
|
+
# S001: Works for both batch and interactive EDI:
|
84
|
+
_s001 = _header.elements["Segment/CDE[@name='S001']"]
|
85
|
+
_version = _s001.elements["DE[@name='0002']"].text.to_i
|
86
|
+
_charset = _s001.elements["DE[@name='0001']"].text
|
87
|
+
params = { :charset => _charset, :version => _version }
|
88
|
+
if _una
|
89
|
+
params[:una_string] = _una
|
90
|
+
params[:show_una] = true
|
91
|
+
end
|
92
|
+
ic = Interchange.new( params )
|
93
|
+
|
94
|
+
ic.header = Segment.parse_xml( ic, _header.elements["Segment"] )
|
95
|
+
ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )
|
96
|
+
|
97
|
+
ic
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
#
|
102
|
+
# Returns a REXML document that represents the interchange
|
103
|
+
#
|
104
|
+
def to_xml( xdoc = REXML::Document.new )
|
105
|
+
rc = super
|
106
|
+
# Add parameter(s) to header in rc[1]
|
107
|
+
unless @una.nil? #@una.empty?
|
108
|
+
xel = REXML::Element.new('Parameter')
|
109
|
+
rc[1] << xel
|
110
|
+
xel.attributes["name"] = 'UNA'
|
111
|
+
xel.text = @una.to_s
|
112
|
+
end
|
113
|
+
# rc
|
114
|
+
xdoc
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
#
|
119
|
+
# Returns a REXML document that represents the interchange
|
120
|
+
# according to DIN 16557-4
|
121
|
+
#
|
122
|
+
def to_din16557_4( xdoc = REXML::Document.new )
|
123
|
+
externalID = "SYSTEM \"edifact.dtd\""
|
124
|
+
doc_element_name = 'EDIFACTINTERCHANGE'
|
125
|
+
xdoc << REXML::XMLDecl.new
|
126
|
+
xdoc << REXML::DocType.new( doc_element_name, externalID )
|
127
|
+
|
128
|
+
doc_el = REXML::Element.new( doc_element_name )
|
129
|
+
xel = REXML::Element.new( 'UNA' )
|
130
|
+
xel.attributes["UNA1"] = una.ce_sep.chr
|
131
|
+
xel.attributes["UNA2"] = una.de_sep.chr
|
132
|
+
xel.attributes["UNA3"] = una.decimal_sign.chr
|
133
|
+
xel.attributes["UNA4"] = una.esc_char.chr
|
134
|
+
xel.attributes["UNA5"] = una.rep_sep.chr
|
135
|
+
xel.attributes["UNA6"] = una.seg_term.chr
|
136
|
+
xdoc.elements << doc_el
|
137
|
+
doc_el.elements << xel
|
138
|
+
|
139
|
+
super( xdoc.root )
|
140
|
+
xdoc
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
class Segment
|
146
|
+
def to_din16557_4( xdoc )
|
147
|
+
xel = REXML::Element.new( self.name )
|
148
|
+
names.uniq.each do |nm|
|
149
|
+
# Array of all items with this name
|
150
|
+
a = self[nm]; max = a.size
|
151
|
+
raise "DIN16557-4 does not support more than 9 repetitions" if max > 9
|
152
|
+
raise "Lookup error (should never occur)" if max == 0
|
153
|
+
if max == 1
|
154
|
+
obj = a.first
|
155
|
+
obj.to_din16557_4( xel ) unless obj.empty?
|
156
|
+
else
|
157
|
+
a.each_with_index do |obj, i|
|
158
|
+
obj.to_din16557_4( xel, i+1 ) unless obj.empty?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
xdoc.elements << xel
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
class CDE
|
168
|
+
def to_din16557_4( xel, rep=nil )
|
169
|
+
prefix = name
|
170
|
+
prefix += rep.to_s if rep
|
171
|
+
names.uniq.each do |nm|
|
172
|
+
# Array of all items with this name
|
173
|
+
a = self[nm]; max = a.size
|
174
|
+
raise "DIN16557-4 does not support more than 9 repetitions" if max > 9
|
175
|
+
raise "Lookup error (should never occur)" if max == 0
|
176
|
+
if max == 1
|
177
|
+
obj = a.first
|
178
|
+
obj.to_din16557_4( xel, nil, prefix ) unless obj.empty?
|
179
|
+
else
|
180
|
+
a.each_with_index do |obj, i|
|
181
|
+
obj.to_din16557_4( xel, i+1, prefix ) unless obj.empty?
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
class DE
|
190
|
+
def to_din16557_4( xel, rep=nil, prefix='' )
|
191
|
+
nm = prefix + 'D' + name
|
192
|
+
nm += rep.to_s if rep
|
193
|
+
xel.attributes[nm] = to_s( true )
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
=begin
|
198
|
+
de_instance_counter = Hash.new(0)
|
199
|
+
xseg_or_cde.elements.each('DE') do |xde|
|
200
|
+
de_name = xde.attributes['name']
|
201
|
+
i = (xde.attributes['instance'] || 1).to_i - 1
|
202
|
+
seg_or_cde[de_name][i].parse( xde.text, true )
|
203
|
+
end
|
204
|
+
=end
|
205
|
+
end # module EDI::E
|
206
|
+
|
207
|
+
|
208
|
+
|
209
|
+
module EDI
|
210
|
+
class Collection_HT
|
211
|
+
#
|
212
|
+
# NOTE: Makes sense only in the UN/EDIFACT context,
|
213
|
+
# so we list this method here.
|
214
|
+
#
|
215
|
+
def to_din16557_4( xparent )
|
216
|
+
header.to_din16557_4( xparent )
|
217
|
+
each {|obj| obj.to_din16557_4( xparent )}
|
218
|
+
trailer.to_din16557_4( xparent )
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,1627 @@
|
|
1
|
+
# UN/EDIFACT add-ons to EDI module,
|
2
|
+
# API to parse and create UN/EDIFACT data
|
3
|
+
#
|
4
|
+
# :include: ../../AuthorCopyright
|
5
|
+
#
|
6
|
+
# $Id: edifact.rb,v 1.10 2006/08/01 11:14:07 werntges Exp $
|
7
|
+
#--
|
8
|
+
# $Log: edifact.rb,v $
|
9
|
+
# Revision 1.10 2006/08/01 11:14:07 werntges
|
10
|
+
# Release 0.9.4.1 -- see ChangeLog
|
11
|
+
#
|
12
|
+
# Revision 1.9 2006/05/26 16:56:41 werntges
|
13
|
+
# V 0.9.3 snapshot. Many improvements (see ChangeLog), RDoc, more I-EDI support
|
14
|
+
#
|
15
|
+
# Revision 1.8 2006/05/01 22:23:55 werntges
|
16
|
+
# Preparing for 0.9.2: See ChangeLog for new features
|
17
|
+
#
|
18
|
+
# Revision 1.7 2006/04/28 14:31:50 werntges
|
19
|
+
# 0.9.1 snapshot
|
20
|
+
#
|
21
|
+
# Revision 1.6 2006/03/28 22:23:40 werntges
|
22
|
+
# changed to using symbols as parameter keys, e.g. :charset
|
23
|
+
# implemented as new module EDI::E, abandoning Interchange_E and alike
|
24
|
+
# bug fixes re. UNA (@una, setters)
|
25
|
+
#
|
26
|
+
# Revision 1.5 2006/03/22 16:52:42 werntges
|
27
|
+
# snapshot after edi4r-0.8.2.gem
|
28
|
+
#
|
29
|
+
# Revision 1.4 2004/02/19 17:31:52 heinz
|
30
|
+
# HWW: Snapshot after REMADV mapping
|
31
|
+
#
|
32
|
+
# Revision 1.3 2004/02/14 12:10:19 heinz
|
33
|
+
# HWW: Minor improvements
|
34
|
+
#
|
35
|
+
# Revision 1.2 2004/02/11 23:31:59 heinz
|
36
|
+
# HWW: First release after finishing basic tests
|
37
|
+
#
|
38
|
+
# Revision 1.1 2004/02/10 00:25:13 heinz
|
39
|
+
# Initial revision
|
40
|
+
#
|
41
|
+
#
|
42
|
+
# Derived from "edi.rb" V 1.11 on 2004-02-09 by HWW
|
43
|
+
#
|
44
|
+
# To-do list:
|
45
|
+
# validate - add functionality
|
46
|
+
# charset - check for valid chars (add UNOD-UNOZ)
|
47
|
+
# UNT count - compensate for empty segments which won't show!
|
48
|
+
# MsgGroup - improve support
|
49
|
+
# NDB - enable support of subsets
|
50
|
+
# NDB - support codelists
|
51
|
+
# SV4 - Support for repetitions
|
52
|
+
# SV4 - Support for new service segments
|
53
|
+
# SV4 - Support for I-EDI releases
|
54
|
+
#++
|
55
|
+
#
|
56
|
+
# This is the UN/EDIFACT module of edi4r (hence '::E')
|
57
|
+
#
|
58
|
+
# It implements EDIFACT versions of classes Interchange, MsgGroup, Message,
|
59
|
+
# Segment, CDE, and DE in sub-module 'E' of module 'EDI'.
|
60
|
+
|
61
|
+
module EDI::E
|
62
|
+
|
63
|
+
#
|
64
|
+
# Use pattern for allowed chars of UNOC charset if none given explicitly
|
65
|
+
#
|
66
|
+
Illegal_Charset_Patterns = Hash.new(/[^-A-Za-z0-9 .,()\/=!%"&*;<>'+:?\xa0-\xff]+/)
|
67
|
+
Illegal_Charset_Patterns['UNOA'] = /[^-A-Z0-9 .,()\/=!%"&*;<>'+:?]+/
|
68
|
+
Illegal_Charset_Patterns['UNOB'] = /[^-A-Za-z0-9 .,()\/=!%"&*;<>'+:?]+/
|
69
|
+
# more to come...
|
70
|
+
|
71
|
+
#########################################################################
|
72
|
+
#
|
73
|
+
# Utility: Separator method for UN/EDIFACT segments/CDEs
|
74
|
+
#
|
75
|
+
# The given string typically comprises an EDIFACT segment or a CDE.
|
76
|
+
# We want to split it into its elements and return those in an array.
|
77
|
+
# The tricky part is the proper handling of character escaping!
|
78
|
+
#
|
79
|
+
# Examples:
|
80
|
+
# CDE = "1234:ABC:567" --> ['1234','ABC','567']
|
81
|
+
# CDE = "1234::567" --> ['1234','','567']
|
82
|
+
# CDE = ":::SOMETEXT" --> ['','','','SOMETEXT']
|
83
|
+
# Seg = "TAG+1++2:3:4+A?+B=C" --> ['TAG','1','','2:3:4','A+B=C']
|
84
|
+
#
|
85
|
+
# NOTE: This function might be a good candidate for implementation in "C"
|
86
|
+
#
|
87
|
+
# Also see: ../../test/test_edi_split.rb
|
88
|
+
#
|
89
|
+
# str:: String to split
|
90
|
+
# s:: Separator char (an Integer)
|
91
|
+
# e:: Escape / release char (an Integer)
|
92
|
+
# max:: Max. desired number of result items, default = all
|
93
|
+
#
|
94
|
+
# Returns:
|
95
|
+
# Array of split results (strings without their terminating separator)
|
96
|
+
|
97
|
+
def edi_split( str, s, e, max=0 )
|
98
|
+
results, item, start = [], '', 0
|
99
|
+
while start < str.length do
|
100
|
+
# match_at = index of next separator, or -1 if none found
|
101
|
+
match_at = ((start...str.length).find{|i| str[i] == s}) || str.length
|
102
|
+
item += str[start...match_at]
|
103
|
+
# Count escapes in front of separator. No real separator if odd!
|
104
|
+
escapes = count_escapes( item, e )
|
105
|
+
if escapes & 1 == 1 # odd
|
106
|
+
raise EDISyntaxError, "Pending escape char in #{str}" if match_at == str.length
|
107
|
+
(escapes/2+1).times {item.chop!} # chop off duplicate escapes
|
108
|
+
item << s # add separator as regular character
|
109
|
+
else # even
|
110
|
+
(escapes/2).times {item.chop!} # chop off duplicate escapes
|
111
|
+
results << item
|
112
|
+
item = ''
|
113
|
+
end
|
114
|
+
start = match_at + 1
|
115
|
+
end
|
116
|
+
#
|
117
|
+
# Do not return trailing empty items
|
118
|
+
#
|
119
|
+
results << item unless item.empty?
|
120
|
+
return results if results.empty?
|
121
|
+
while results.last.empty?; results.pop; end
|
122
|
+
results
|
123
|
+
end
|
124
|
+
|
125
|
+
class EDISyntaxError < ArgumentError
|
126
|
+
end
|
127
|
+
|
128
|
+
def count_escapes( str, e ) # :nodoc:
|
129
|
+
n = 0
|
130
|
+
(str.length-1).downto(0) do |i|
|
131
|
+
if str[i]==e
|
132
|
+
n += 1
|
133
|
+
else
|
134
|
+
return n
|
135
|
+
end
|
136
|
+
end
|
137
|
+
n
|
138
|
+
end
|
139
|
+
|
140
|
+
module_function :edi_split, :count_escapes
|
141
|
+
|
142
|
+
|
143
|
+
#########################################################################
|
144
|
+
#
|
145
|
+
# Here we extend class Time by some methods that help us maximize
|
146
|
+
# its use in the UN/EDIFACT context.
|
147
|
+
#
|
148
|
+
# Basic idea:
|
149
|
+
# * Use the EDIFACT qualifiers of DE 2379 in DTM directly
|
150
|
+
# to parse dates and to create them upon output.
|
151
|
+
# * Use augmented Time objects as values of DE 2380 instead of strings
|
152
|
+
#
|
153
|
+
# Currently supported formats: 101, 102, 201, 203, 204
|
154
|
+
|
155
|
+
class ::Time
|
156
|
+
attr_accessor :format
|
157
|
+
|
158
|
+
def Time.edifact(str, fmt=102)
|
159
|
+
msg = "Time.edifact: #{str} does not match format #{fmt}"
|
160
|
+
case fmt.to_s
|
161
|
+
when '101'
|
162
|
+
rc = str =~ /(\d\d)(\d\d)(\d\d)(.+)?/
|
163
|
+
raise msg unless rc and rc==0; warn msg if $4
|
164
|
+
year = $1.to_i
|
165
|
+
year += (year < 69) ? 2000 : 1900 # See ParseDate
|
166
|
+
dtm = Time.local(year, $2, $3)
|
167
|
+
|
168
|
+
when '102'
|
169
|
+
rc = str =~ /(\d\d\d\d)(\d\d)(\d\d)(.+)?/
|
170
|
+
raise msg unless rc and rc==0; warn msg if $4
|
171
|
+
dtm = Time.local($1, $2, $3)
|
172
|
+
|
173
|
+
when '201'
|
174
|
+
rc = str =~ /(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(.+)?/
|
175
|
+
raise msg unless rc and rc==0; warn msg if $6
|
176
|
+
year = $1.to_i
|
177
|
+
year += (year < 69) ? 2000 : 1900 # See ParseDate
|
178
|
+
dtm = Time.local(year, $2, $3, $4, $5)
|
179
|
+
|
180
|
+
when '203'
|
181
|
+
rc = str =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(.+)?/
|
182
|
+
raise msg unless rc and rc==0; warn msg if $6
|
183
|
+
dtm = Time.local($1, $2, $3, $4, $5)
|
184
|
+
|
185
|
+
when '204'
|
186
|
+
rc = str =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(.+)?/
|
187
|
+
raise msg unless rc and rc==0; warn msg if $7
|
188
|
+
dtm = Time.local($1, $2, $3, $4, $5, $6)
|
189
|
+
|
190
|
+
else
|
191
|
+
raise "Time.edifact: Format #{fmt} not supported - sorry"
|
192
|
+
end
|
193
|
+
dtm.format = fmt.to_s
|
194
|
+
dtm
|
195
|
+
end
|
196
|
+
|
197
|
+
alias to_s_orig to_s
|
198
|
+
|
199
|
+
def to_s
|
200
|
+
return to_s_orig unless @format
|
201
|
+
case @format.to_s
|
202
|
+
when '101'
|
203
|
+
"%02d%02d%02d" % [year % 100, mon, day]
|
204
|
+
when '102'
|
205
|
+
"%04d%02d%02d" % [year, mon, day]
|
206
|
+
when '201'
|
207
|
+
"%02d%02d%02d%02d%02d" % [year % 100, mon, day, hour, min]
|
208
|
+
when '203'
|
209
|
+
"%04d%02d%02d%02d%02d" % [year, mon, day, hour, min]
|
210
|
+
when '204'
|
211
|
+
"%04d%02d%02d%02d%02d%2d" % [year, mon, day, hour, min, sec]
|
212
|
+
else # Should never occur
|
213
|
+
raise "Time.edifact: Format #{format
|
214
|
+
} not supported - sorry"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
#########################################################################
|
220
|
+
#
|
221
|
+
# Class UNA is a model of UN/EDIFACT's UNA pseudo-segment.
|
222
|
+
# It provides getters and setters that let you manipulate the six special
|
223
|
+
# characters of UN/EDIFACT. Note that the chars are passed as integers,
|
224
|
+
# i.e. ASCII codes.
|
225
|
+
#
|
226
|
+
class UNA < EDI::Object
|
227
|
+
|
228
|
+
attr_reader :pattern_esc, :pattern_unesc # :nodoc:
|
229
|
+
attr_reader :ce_sep, :de_sep, :rep_sep, :esc_char, :seg_term, :decimal_sign
|
230
|
+
|
231
|
+
#
|
232
|
+
# Sets the decimal sign. UN/EDIFACT allows only ?, and ?.
|
233
|
+
#
|
234
|
+
def decimal_sign= (chr)
|
235
|
+
chr = chr[0] if chr.is_a? String
|
236
|
+
raise "Illegal decimal sign: #{chr}" unless chr==?. || chr==?,
|
237
|
+
@decimal_sign = chr
|
238
|
+
set_chars
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# Sets the composite data element separator. Default is ?:
|
243
|
+
#
|
244
|
+
def ce_sep= (chr)
|
245
|
+
chr = chr[0] if chr.is_a? String
|
246
|
+
@ce_sep = chr
|
247
|
+
set_chars # Update derived Regexp objects!
|
248
|
+
end
|
249
|
+
|
250
|
+
#
|
251
|
+
# Sets the data element separator. Default is ?+
|
252
|
+
#
|
253
|
+
def de_sep= (chr)
|
254
|
+
chr = chr[0] if chr.is_a? String
|
255
|
+
@de_sep = chr
|
256
|
+
set_chars # Update derived Regexp objects!
|
257
|
+
end
|
258
|
+
|
259
|
+
#
|
260
|
+
# Sets the repetition separator. Default is ?* .
|
261
|
+
# Only applicable to Syntax Version 4 !
|
262
|
+
#
|
263
|
+
def rep_sep= (chr)
|
264
|
+
raise NoMethodError, "Syntax version 4 required" unless root.version==4
|
265
|
+
chr = chr[0] if chr.is_a? String
|
266
|
+
@rep_sep = chr
|
267
|
+
set_chars # Update derived Regexp objects!
|
268
|
+
end
|
269
|
+
|
270
|
+
#
|
271
|
+
# Sets the segment terminator. Default is ?'
|
272
|
+
#
|
273
|
+
def seg_term= (chr)
|
274
|
+
chr = chr[0] if chr.is_a? String
|
275
|
+
@seg_term = chr
|
276
|
+
set_chars # Update derived Regexp objects!
|
277
|
+
end
|
278
|
+
|
279
|
+
#
|
280
|
+
# Sets the escape character. Default is ??
|
281
|
+
#
|
282
|
+
def esc_char= (chr)
|
283
|
+
chr = chr[0] if chr.is_a? String
|
284
|
+
@esc_char = chr
|
285
|
+
set_chars # Update derived Regexp objects!
|
286
|
+
end
|
287
|
+
|
288
|
+
#
|
289
|
+
# Generates the UNA object
|
290
|
+
# * Requires that "version" and "charset" of parent/root (Interchange)
|
291
|
+
# be already defined.
|
292
|
+
# * Sets the UN/EDIFACT defaults if source string 'UNA......' not given
|
293
|
+
#
|
294
|
+
def initialize( root, source=nil )
|
295
|
+
super( root, root, 'UNA')
|
296
|
+
|
297
|
+
raise "UNA.new requires 'version' in the interchange" unless root.version
|
298
|
+
raise "UNA.new requires 'charset' in the interchange" unless root.charset
|
299
|
+
|
300
|
+
if source =~ /^UNA(......)$/ # Take what's given
|
301
|
+
@chars = $1
|
302
|
+
|
303
|
+
elsif (source == nil or source.empty?) # Use EDIFACT default rules
|
304
|
+
if root.version==2 and root.charset=='UNOB'
|
305
|
+
@chars = "\x11\x12.? \x14"
|
306
|
+
elsif root.version==4
|
307
|
+
@chars = ":+.?*'"
|
308
|
+
else
|
309
|
+
@chars = ":+.? '"
|
310
|
+
end
|
311
|
+
|
312
|
+
else
|
313
|
+
raise "This is not a valid UNA source string: #{source}"
|
314
|
+
end
|
315
|
+
|
316
|
+
@ce_sep, @de_sep, @decimal_sign,
|
317
|
+
@esc_char, @rep_sep, @seg_term = @chars.split('').map{|c| c[0]}
|
318
|
+
set_patterns
|
319
|
+
end
|
320
|
+
|
321
|
+
def to_s
|
322
|
+
'UNA'+@chars
|
323
|
+
end
|
324
|
+
|
325
|
+
private
|
326
|
+
|
327
|
+
def set_chars
|
328
|
+
@chars=[@ce_sep, @de_sep, @decimal_sign, @esc_char, @rep_sep, @seg_term ]
|
329
|
+
@chars=@chars.map{|c| c.chr}.join('')
|
330
|
+
# Prevent duplicates
|
331
|
+
raise "Must not assign special char more than once!" if @chars=~/(.).*\1/
|
332
|
+
set_patterns
|
333
|
+
end
|
334
|
+
|
335
|
+
#
|
336
|
+
# Adjust match patterns anew when one of the UNA separators / special
|
337
|
+
# characters is changed.
|
338
|
+
#
|
339
|
+
def set_patterns
|
340
|
+
special_chars = [ @ce_sep, @de_sep, @esc_char, @seg_term ]
|
341
|
+
special_chars.push @rep_sep if root.version == 4
|
342
|
+
special_chars = special_chars.map{|c| c.chr}
|
343
|
+
@pattern_esc = Regexp.new( [ '([', special_chars, '])' ].flatten.join)
|
344
|
+
@pattern_unesc = Regexp.new( [
|
345
|
+
'([^', @esc_char, ']?)', '[', @esc_char,
|
346
|
+
']([', special_chars,'])'
|
347
|
+
].flatten.join )
|
348
|
+
root.show_una = true
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
#########################################################################
|
353
|
+
#
|
354
|
+
# Interchange: Class of the top-level objects of UN/EDIFACT data
|
355
|
+
#
|
356
|
+
class Interchange < EDI::Interchange
|
357
|
+
|
358
|
+
attr_accessor :show_una
|
359
|
+
attr_reader :e_linebreak, :e_indent # :nodoc:
|
360
|
+
attr_reader :charset, :una
|
361
|
+
attr_reader :messages_created, :groups_created
|
362
|
+
|
363
|
+
|
364
|
+
@@interchange_defaults = {
|
365
|
+
:i_edi => false, :charset => 'UNOB', :version => 3,
|
366
|
+
:show_una => true, :una_string => nil,
|
367
|
+
:sender => nil, :recipient => nil,
|
368
|
+
:interchange_control_reference => '1', :application_reference => nil,
|
369
|
+
:interchange_agreement_id => nil,
|
370
|
+
:acknowledgment_request => nil, :test_indicator => nil,
|
371
|
+
:output_mode => :verbatim
|
372
|
+
}
|
373
|
+
@@interchange_default_keys = @@interchange_defaults.keys
|
374
|
+
|
375
|
+
# Create an empty UN/EDIFACT interchange
|
376
|
+
#
|
377
|
+
# == Supported parameters (passed hash-style):
|
378
|
+
#
|
379
|
+
# === Essentials, should not be changed later
|
380
|
+
# :charset :: Sets S001.0001, default = 'UNOB'
|
381
|
+
# :version :: Sets S001.0002, default = 3
|
382
|
+
# :i_edi :: Interactive EDI mode, a boolean (UIB instead of UNB ...), default = false
|
383
|
+
#
|
384
|
+
# === Optional parameters affecting to_s, with corresponding setters
|
385
|
+
# :show_una :: Adds UNA sement to output, default = true
|
386
|
+
# :output_mode :: See setter output_mode=(), default = :verbatim
|
387
|
+
# :una_string :: See class UNA for setters, default = nil
|
388
|
+
#
|
389
|
+
# === Optional UNB presets for your convenience, may be changed later
|
390
|
+
# :sender :: Presets DE S002/0004, default = nil
|
391
|
+
# :recipient :: Presets DE S003/0010, default = nil
|
392
|
+
# :interchange_control_reference :: Presets DE 0020, default = '1'
|
393
|
+
# :application_reference :: Presets DE 0026, default = nil
|
394
|
+
# :interchange_agreement_id :: Presets DE 0032, default = nil
|
395
|
+
# :acknowledgment_request :: Presets DE 0031, default = nil
|
396
|
+
# :test_indicator :: Presets DE 0035, default = nil
|
397
|
+
#
|
398
|
+
# === Notes
|
399
|
+
# * Date and time in S004 are set to the current values automatically.
|
400
|
+
# * Add or change any data element later. except those in S001.
|
401
|
+
#
|
402
|
+
# === Examples:
|
403
|
+
# - ic = EDI::E::Interchange.new # Empty interchange, default settings
|
404
|
+
# - ic = EDI::E::Interchange.new(:charset=>'UNOC',:output_mode=>:linebreak)
|
405
|
+
|
406
|
+
def initialize( user_par={} )
|
407
|
+
super( user_par ) # just in case...
|
408
|
+
if (illegal_keys = user_par.keys - @@interchange_default_keys) != []
|
409
|
+
msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
|
410
|
+
msg += "Valid param keys (symbols): #{@@interchange_default_keys.join(', ')}"
|
411
|
+
raise ArgumentError, msg
|
412
|
+
end
|
413
|
+
par = @@interchange_defaults.merge( user_par )
|
414
|
+
|
415
|
+
@messages_created = @groups_created = 0
|
416
|
+
|
417
|
+
@syntax = 'E' # par[:syntax] # E = UN/EDIFACT
|
418
|
+
@e_iedi = par[:i_edi]
|
419
|
+
@charset = par[:charset]
|
420
|
+
@version = par[:version]
|
421
|
+
@una = UNA.new(self, par[:una_string])
|
422
|
+
self.output_mode = par[:output_mode]
|
423
|
+
self.show_una = par[:show_una]
|
424
|
+
|
425
|
+
check_consistencies
|
426
|
+
init_ndb( @version )
|
427
|
+
|
428
|
+
if @e_iedi # Interactive EDI
|
429
|
+
|
430
|
+
raise "I-EDI not supported yet"
|
431
|
+
|
432
|
+
# Fill in what we already know about I-EDI:
|
433
|
+
|
434
|
+
@header = new_segment('UIB')
|
435
|
+
@trailer = new_segment('UIZ')
|
436
|
+
@header.cS001.d0001 = par[:charset]
|
437
|
+
@header.cS001.d0002 = par[:version]
|
438
|
+
|
439
|
+
@header.cS002.d0004 = par[:sender] unless par[:sender].nil?
|
440
|
+
@header.cS003.d0010 = par[:recipient] unless par[:recip].nil?
|
441
|
+
@header.cS302.d0300 = par[:interchange_control_reference]
|
442
|
+
# FIXME: More to do in S302...
|
443
|
+
|
444
|
+
x= :test_indicator; @header.d0035 = par[x] unless par[x].nil?
|
445
|
+
|
446
|
+
t = Time.now
|
447
|
+
@header.cS300.d0338 = t.strftime(par[:version]==4 ? '%Y%m%d':'%y%m%d')
|
448
|
+
@header.cS300.d0314 = t.strftime("%H%M")
|
449
|
+
|
450
|
+
@trailer.d0036 = 0
|
451
|
+
ch, ct = @header.cS302, @trailer.cS302
|
452
|
+
ct.d0300, ct.d0303, ct.d0051, ct.d0304 = ch.d0300, ch.d0303, ch.d0051, ch.d0304
|
453
|
+
else # Batch EDI
|
454
|
+
|
455
|
+
@header = new_segment('UNB')
|
456
|
+
@trailer = new_segment('UNZ')
|
457
|
+
@header.cS001.d0001 = par[:charset]
|
458
|
+
@header.cS001.d0002 = par[:version]
|
459
|
+
@header.cS002.d0004 = par[:sender] unless par[:sender].nil?
|
460
|
+
@header.cS003.d0010 = par[:recipient] unless par[:recip].nil?
|
461
|
+
@header.d0020 = par[:interchange_control_reference]
|
462
|
+
|
463
|
+
x= :application_reference; @header.d0026 = par[x] unless par[x].nil?
|
464
|
+
x= :acknowledgment_request; @header.d0031 = par[x] unless par[x].nil?
|
465
|
+
x= :interchange_agreement_id; @header.d0032 = par[x] unless par[x].nil?
|
466
|
+
x= :test_indicator; @header.d0035 = par[x] unless par[x].nil?
|
467
|
+
|
468
|
+
t = Time.now
|
469
|
+
@header.cS004.d0017 = t.strftime(par[:version]==4 ? '%Y%m%d':'%y%m%d')
|
470
|
+
@header.cS004.d0019 = t.strftime("%H%M")
|
471
|
+
|
472
|
+
@trailer.d0036 = 0
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
|
477
|
+
#
|
478
|
+
# Reads EDIFACT data from given stream (default: $stdin),
|
479
|
+
# parses it and returns an Interchange object
|
480
|
+
#
|
481
|
+
def Interchange.parse( hnd=$stdin, auto_validate=true )
|
482
|
+
ic = nil
|
483
|
+
buf = hnd.read
|
484
|
+
return ic if buf.empty?
|
485
|
+
|
486
|
+
ic, segment_list = Interchange.parse_buffer( buf )
|
487
|
+
# Remember to update ndb to SV4-1 now if d0076 of UNB/S001 tells so
|
488
|
+
|
489
|
+
# Deal with 'trash' after UNZ
|
490
|
+
|
491
|
+
if ic.is_iedi?
|
492
|
+
init_seg = Regexp.new('^UIB'); tag_init = 'UIB'
|
493
|
+
exit_seg = Regexp.new('^UIZ'); tag_exit = 'UIZ'
|
494
|
+
else
|
495
|
+
init_seg = Regexp.new('^UNB'); tag_init = 'UNB'
|
496
|
+
exit_seg = Regexp.new('^UNZ'); tag_exit = 'UNZ'
|
497
|
+
end
|
498
|
+
|
499
|
+
last_seg = nil
|
500
|
+
loop do
|
501
|
+
last_seg = segment_list.pop
|
502
|
+
case last_seg
|
503
|
+
when /^[A-Z]{3}/ # Segment tag?
|
504
|
+
unless last_seg =~ exit_seg
|
505
|
+
raise "Parse error: #{tag_exit} is not last segment! Found: #{last_seg}"
|
506
|
+
end
|
507
|
+
break
|
508
|
+
when /\n/, /\r\n/, ''
|
509
|
+
# ignore linebreaks at end of file, do not warn.
|
510
|
+
else
|
511
|
+
warn "WARNING: Data found after #{tag_exit} segment - ignored!"
|
512
|
+
warn "Found: \'#{last_seg}\'"
|
513
|
+
end
|
514
|
+
end
|
515
|
+
trailer = Segment.parse(ic, last_seg, tag_exit)
|
516
|
+
|
517
|
+
# Assure that there is only one UNB/UNZ or UIB/UIZ
|
518
|
+
|
519
|
+
err_flag = false
|
520
|
+
segment_list.each do |seg|
|
521
|
+
if seg =~ init_seg
|
522
|
+
warn "ERROR: Another interchange header found in file!"
|
523
|
+
err_flag = true
|
524
|
+
end
|
525
|
+
if seg =~ exit_seg
|
526
|
+
warn "ERROR: Another interchange trailer found in file!"
|
527
|
+
err_flag = true
|
528
|
+
end
|
529
|
+
end
|
530
|
+
raise "FATAL ERROR - exiting" if err_flag
|
531
|
+
|
532
|
+
# OK, ready to deal with content now:
|
533
|
+
|
534
|
+
case segment_list[0]
|
535
|
+
when /^UNH/
|
536
|
+
init_seg = Regexp.new('^UNH')
|
537
|
+
exit_seg = Regexp.new('^UNT')
|
538
|
+
group_mode = false
|
539
|
+
when /^UNG/
|
540
|
+
init_seg = Regexp.new('^UNG')
|
541
|
+
exit_seg = Regexp.new('^UNE')
|
542
|
+
group_mode = true
|
543
|
+
when /^UIH/ # There is no 'UIG'!
|
544
|
+
init_seg = Regexp.new('^UIH')
|
545
|
+
exit_seg = Regexp.new('^UIT')
|
546
|
+
group_mode = false
|
547
|
+
else
|
548
|
+
raise "Expected: UNH, UNG, or UIH. Found: #{segment_list[0]}"
|
549
|
+
end
|
550
|
+
|
551
|
+
while segbuf = segment_list.shift
|
552
|
+
case segbuf
|
553
|
+
|
554
|
+
when init_seg
|
555
|
+
sub_list = Array.new
|
556
|
+
sub_list.push segbuf
|
557
|
+
|
558
|
+
when exit_seg
|
559
|
+
sub_list.push segbuf
|
560
|
+
if group_mode
|
561
|
+
ic.add( MsgGroup.parse(ic, sub_list), auto_validate )
|
562
|
+
else
|
563
|
+
ic.add( Message.parse(ic, sub_list), auto_validate )
|
564
|
+
end
|
565
|
+
|
566
|
+
else
|
567
|
+
sub_list.push segbuf
|
568
|
+
end
|
569
|
+
|
570
|
+
end # while
|
571
|
+
|
572
|
+
# Finally add the trailer from the originally read data,
|
573
|
+
# thereby overwriting the temporary interchange trailer.
|
574
|
+
# Note that the temporary trailer got modified by add()ing
|
575
|
+
# to the interchange.
|
576
|
+
ic.trailer = trailer
|
577
|
+
ic
|
578
|
+
end
|
579
|
+
|
580
|
+
#
|
581
|
+
# Read +maxlen+ bytes from $stdin (default) or from given stream
|
582
|
+
# (UN/EDIFACT data expected), and peek into first segment (UNB/UIB).
|
583
|
+
#
|
584
|
+
# Returns an empty Interchange object with a properly header filled.
|
585
|
+
#
|
586
|
+
# Intended use:
|
587
|
+
# Efficient routing by reading just UNB data: sender/recipient/ref/test
|
588
|
+
#
|
589
|
+
def Interchange.peek(hnd=$stdin, maxlen=128) # Handle to input stream
|
590
|
+
buf = hnd.read( maxlen )
|
591
|
+
return nil if buf.empty?
|
592
|
+
ic, dummy = Interchange.parse_buffer( buf, 1 )
|
593
|
+
|
594
|
+
# Create a dummy trailer
|
595
|
+
tag = ic.is_iedi? ? 'UIZ' : 'UNZ'
|
596
|
+
trailer_string = tag.dup << ic.una.de_sep << '0' << ic.una.de_sep << '0'
|
597
|
+
ic.trailer= Segment.parse(ic, trailer_string, tag)
|
598
|
+
|
599
|
+
ic
|
600
|
+
end
|
601
|
+
|
602
|
+
#
|
603
|
+
# INTERNAL USE ONLY:
|
604
|
+
# Turn buffer into array of segments (array size <= s_max),
|
605
|
+
# read UNB/UIB, create an Interchange object with a header,
|
606
|
+
# return this interchange and the array of segments
|
607
|
+
#
|
608
|
+
def Interchange.parse_buffer( buf, s_max=0 ) # :nodoc:
|
609
|
+
case buf
|
610
|
+
# UN/EDIFACT case
|
611
|
+
when /^(UNA......)?\r?\n?U([IN])B.(UNO[A-Z]).([1-4])/
|
612
|
+
par = @@interchange_defaults.dup
|
613
|
+
par[:una_string], par[:charset], par[:version], par[:i_edi] =
|
614
|
+
$1, $3, $4.to_i, $2=='I'
|
615
|
+
ic = Interchange.new( par )
|
616
|
+
buf.sub!(/^UNA....../,'') # remove pseudo segment
|
617
|
+
|
618
|
+
else
|
619
|
+
raise "Is this really UN/EDIFACT? File starts with: #{buf[0,23]}"
|
620
|
+
end
|
621
|
+
|
622
|
+
segments = EDI::E.edi_split(buf, ic.una.seg_term, ic.una.esc_char, s_max)
|
623
|
+
# Remove <cr><lf> (some sources are not EDIFACT compliant)
|
624
|
+
segments.each {|s| s.sub!(/\s*(.*)/, '\1')}
|
625
|
+
ic.header = Segment.parse(ic, segments.shift, ic.is_iedi? ? 'UIB':'UNB')
|
626
|
+
|
627
|
+
[ic, segments]
|
628
|
+
end
|
629
|
+
|
630
|
+
#
|
631
|
+
# Returns +true+ if this is an I-EDI interchange (Interactive EDI)
|
632
|
+
#
|
633
|
+
def is_iedi?
|
634
|
+
@e_iedi
|
635
|
+
end
|
636
|
+
|
637
|
+
# This method modifies the behaviour of method to_s():
|
638
|
+
# UN/EDIFACT interchanges and their components are turned into strings
|
639
|
+
# either "verbatim" (default) or in some more readable way.
|
640
|
+
# This method corresponds to a parameter with same name at creation time.
|
641
|
+
#
|
642
|
+
# Valid values:
|
643
|
+
#
|
644
|
+
# :linebreak :: One-segment-per-line representation
|
645
|
+
# :indented :: Like :linebreak but with additional indentation
|
646
|
+
# (2 blanks per hierarchy level).
|
647
|
+
# :verbatim :: No linebreak (default), ISO compliant
|
648
|
+
#
|
649
|
+
def output_mode=( value )
|
650
|
+
super( value )
|
651
|
+
@e_linebreak = @e_indent = ''
|
652
|
+
case value
|
653
|
+
when :verbatim
|
654
|
+
# NOP (default)
|
655
|
+
when :linebreak
|
656
|
+
@e_linebreak = "\n"
|
657
|
+
when :indented
|
658
|
+
@e_linebreak = "\n"
|
659
|
+
@e_indent = ' '
|
660
|
+
else
|
661
|
+
raise "Unknown output mode '#{value}'. Supported modes: :linebreak, :indented, :verbatim (default)"
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
|
666
|
+
# Add either a MsgGroup or Message object to the interchange.
|
667
|
+
# Note: Don't mix both types!
|
668
|
+
#
|
669
|
+
# UNZ/UIZ counter DE 0036 is automatically incremented.
|
670
|
+
|
671
|
+
def add( obj, auto_validate=true )
|
672
|
+
super
|
673
|
+
@trailer.d0036 += 1 #if @trailer # @trailer doesn't exist yet when parsing
|
674
|
+
# FIXME: Warn/fail if UNH/UIH/UNG id is not unique (at validation?)
|
675
|
+
end
|
676
|
+
|
677
|
+
|
678
|
+
# Derive an empty message group from this interchange context.
|
679
|
+
# Parameters may be passed hash-like. See MsgGroup.new for details
|
680
|
+
#
|
681
|
+
def new_msggroup(params={}) # to be completed ...
|
682
|
+
@groups_created += 1
|
683
|
+
MsgGroup.new(self, params)
|
684
|
+
end
|
685
|
+
|
686
|
+
# Derive an empty message from this interchange context.
|
687
|
+
# Parameters may be passed hash-like. See Message.new for details
|
688
|
+
#
|
689
|
+
def new_message(params={})
|
690
|
+
@messages_created += 1
|
691
|
+
Message.new(self, params)
|
692
|
+
end
|
693
|
+
|
694
|
+
# Derive an empty segment from this interchange context
|
695
|
+
# For internal use only (header / trailer segment generation)
|
696
|
+
#
|
697
|
+
def new_segment(tag) # :nodoc:
|
698
|
+
Segment.new(self, tag)
|
699
|
+
end
|
700
|
+
|
701
|
+
|
702
|
+
# Parse a message group (when group mode detected)
|
703
|
+
# Internal use only.
|
704
|
+
|
705
|
+
def parse_msggroup(list) # :nodoc:
|
706
|
+
MsgGroup.parse(self, list)
|
707
|
+
end
|
708
|
+
|
709
|
+
# Parse a message (when message mode detected)
|
710
|
+
# Internal use only.
|
711
|
+
|
712
|
+
def parse_message(list) # :nodoc:
|
713
|
+
Message.parse(self, list)
|
714
|
+
end
|
715
|
+
|
716
|
+
# Parse a segment (header or trailer expected)
|
717
|
+
# Internal use only.
|
718
|
+
|
719
|
+
def parse_segment(buf, tag) # :nodoc:
|
720
|
+
Segment.parse(self, buf, tag)
|
721
|
+
end
|
722
|
+
|
723
|
+
|
724
|
+
# Returns the string representation of the interchange.
|
725
|
+
#
|
726
|
+
# Type conversion and escaping are provided.
|
727
|
+
# The UNA object is shown when +show_una+ is set to +true+ .
|
728
|
+
# See +output_mode+ for modifiers.
|
729
|
+
|
730
|
+
def to_s
|
731
|
+
s = show_una ? una.to_s + @e_linebreak : ''
|
732
|
+
postfix = '' << una.seg_term << @e_linebreak
|
733
|
+
s << super( postfix )
|
734
|
+
end
|
735
|
+
|
736
|
+
|
737
|
+
# Yields a readable, properly indented list of all contained objects,
|
738
|
+
# including the empty ones. This may be a very long string!
|
739
|
+
|
740
|
+
def inspect( indent='', symlist=[] )
|
741
|
+
symlist << :una
|
742
|
+
super
|
743
|
+
end
|
744
|
+
|
745
|
+
|
746
|
+
# Returns the number of warnings found, writes warnings to STDERR
|
747
|
+
|
748
|
+
def validate( err_count=0 )
|
749
|
+
if (h=self.size) != (t=@trailer.d0036)
|
750
|
+
warn "Counter UNZ/UIZ, DE0036 does not match content: #{t} vs. #{h}"
|
751
|
+
err_count += 1
|
752
|
+
end
|
753
|
+
if (h=@header.cS001.d0001) != @charset
|
754
|
+
warn "Charset UNZ/UIZ, S001/0001 mismatch: #{h} vs. #@charset"
|
755
|
+
err_count += 1
|
756
|
+
end
|
757
|
+
if (h=@header.cS001.d0002) != @version
|
758
|
+
warn "Syntax version UNZ/UIZ, S001/0002 mismatch: #{h} vs. #@version"
|
759
|
+
err_count += 1
|
760
|
+
end
|
761
|
+
check_consistencies
|
762
|
+
|
763
|
+
if is_iedi?
|
764
|
+
if (t=@trailer.cS302.d0300) != (h=@header.cS302.d0300)
|
765
|
+
warn "UIB/UIZ mismatch in initiator ref (S302/0300): #{h} vs. #{t}"
|
766
|
+
err_count += 1
|
767
|
+
end
|
768
|
+
# FIXME: Add more I-EDI checks
|
769
|
+
else
|
770
|
+
if (t=@trailer.d0020) != (h=@header.d0020)
|
771
|
+
warn "UNB/UNZ mismatch in refno (DE0020): #{h} vs. #{t}"
|
772
|
+
err_count += 1
|
773
|
+
end
|
774
|
+
end
|
775
|
+
|
776
|
+
# FIXME: Check if messages/groups are uniquely numbered
|
777
|
+
|
778
|
+
super
|
779
|
+
end
|
780
|
+
|
781
|
+
private
|
782
|
+
|
783
|
+
#
|
784
|
+
# Private method: Loads EDIFACT norm database
|
785
|
+
#
|
786
|
+
def init_ndb(d0002, d0076 = nil)
|
787
|
+
@basedata = EDI::Dir::Directory.create(root.syntax,
|
788
|
+
:d0002 => @version,
|
789
|
+
:d0076 => d0076,
|
790
|
+
:is_iedi => is_iedi?)
|
791
|
+
end
|
792
|
+
|
793
|
+
#
|
794
|
+
# Private method: Check if basic UNB elements are set properly
|
795
|
+
#
|
796
|
+
def check_consistencies
|
797
|
+
# FIXME - @syntax should be completely avoided, use sub-module name
|
798
|
+
if not ['E'].include?(@syntax) # More anticipated here
|
799
|
+
raise "#{@syntax} - syntax not supported!"
|
800
|
+
end
|
801
|
+
case @version
|
802
|
+
when 1
|
803
|
+
if @charset != 'UNOA'
|
804
|
+
raise "Syntax version 1 permits only charset UNOA!"
|
805
|
+
end
|
806
|
+
when 2
|
807
|
+
if not @charset =~ /UNO[AB]/
|
808
|
+
raise "Syntax version 2 permits only charsets UNOA, UNOB!"
|
809
|
+
end
|
810
|
+
when 3
|
811
|
+
if not @charset =~ /UNO[A-F]/
|
812
|
+
raise "Syntax version 3 permits only charsets UNOA...UNOF!"
|
813
|
+
end
|
814
|
+
when 4
|
815
|
+
# A,B: ISO 646 subsets, C-K: ISO-8859-x, X: ISO 2022, Y: ISO 10646-1
|
816
|
+
if not @charset =~ /UNO[A-KXY]/
|
817
|
+
raise "Syntax version 4 permits only charsets UNOA...UNOZ!"
|
818
|
+
end
|
819
|
+
else
|
820
|
+
raise "#{@version} - no such syntax version!"
|
821
|
+
end
|
822
|
+
if @e_iedi and @version != 4
|
823
|
+
raise "Inconsistent parameters - I-EDI requires syntax version 4!"
|
824
|
+
end
|
825
|
+
@illegal_charset_pattern = Illegal_Charset_Patterns['@version']
|
826
|
+
# Add more rules ...
|
827
|
+
end
|
828
|
+
|
829
|
+
end
|
830
|
+
|
831
|
+
#########################################################################
|
832
|
+
#
|
833
|
+
# Class EDI::E::MsgGroup
|
834
|
+
#
|
835
|
+
# This class implements a group of business documents of the same type
|
836
|
+
# Its header unites features from UNB as well as from UNH.
|
837
|
+
#
|
838
|
+
class MsgGroup < EDI::MsgGroup
|
839
|
+
|
840
|
+
attr_reader :messages_created
|
841
|
+
|
842
|
+
@@msggroup_defaults = {
|
843
|
+
:msg_type => 'ORDERS', :version => 'D', :release => '96A',
|
844
|
+
:resp_agency => 'UN', :assigned_code => nil # e.g. 'EAN008'
|
845
|
+
}
|
846
|
+
@@msggroup_default_keys = @@msggroup_defaults.keys
|
847
|
+
|
848
|
+
# Creates an empty UN/EDIFACT message group
|
849
|
+
# Don't use directly - use +new_msggroup+ of class Interchange instead!
|
850
|
+
#
|
851
|
+
# == First parameter
|
852
|
+
#
|
853
|
+
# This is always the parent object (an interchange object).
|
854
|
+
# Use method +new_msggroup+ in the corresponding object instead
|
855
|
+
# of creating message groups unattended - the parent reference
|
856
|
+
# will be accounted for automatically.
|
857
|
+
#
|
858
|
+
# == Second parameter
|
859
|
+
#
|
860
|
+
# List of supported hash keys:
|
861
|
+
#
|
862
|
+
# === UNG presets for your convenience, may be changed later
|
863
|
+
#
|
864
|
+
# :msg_type :: Sets DE 0038, default = 'INVOIC'
|
865
|
+
# :resp_agency :: Sets DE 0051, default = 'UN'
|
866
|
+
# :version :: Sets S008.0052, default = 'D'
|
867
|
+
# :release :: Sets S008.0054, default = '96A'
|
868
|
+
#
|
869
|
+
# === Optional parameters, required depending upon use case
|
870
|
+
#
|
871
|
+
# :assigned_code :: Sets S008.0057 (subset), default = nil
|
872
|
+
# :sender :: Presets DE S006/0040, default = nil
|
873
|
+
# :recipient :: Presets DE S007/0044, default = nil
|
874
|
+
# :group_reference :: Presets DE 0048, auto-incremented
|
875
|
+
#
|
876
|
+
# == Notes
|
877
|
+
#
|
878
|
+
# * The functional group reference number in UNG and UNE (0048) is set
|
879
|
+
# automatically to a number that is unique for this message group and
|
880
|
+
# the running process (auto-increment).
|
881
|
+
# * The counter in UNG (0060) is set automatically to the number
|
882
|
+
# of included messages.
|
883
|
+
# * The trailer segment (UNE) is generated automatically.
|
884
|
+
# * Whenever possible, <b>avoid writing to the counters of
|
885
|
+
# the message header or trailer segments</b>!
|
886
|
+
|
887
|
+
def initialize( p, user_par={} )
|
888
|
+
super( p, user_par )
|
889
|
+
@messages_created = 0
|
890
|
+
|
891
|
+
if user_par.is_a? Hash
|
892
|
+
preset_group( user_par )
|
893
|
+
@header = new_segment('UNG')
|
894
|
+
@trailer = new_segment('UNE')
|
895
|
+
@trailer.d0060 = 0
|
896
|
+
|
897
|
+
@header.d0038 = @name
|
898
|
+
@header.d0051 = @resp_agency
|
899
|
+
cde = @header.cS008
|
900
|
+
cde.d0052 = @version
|
901
|
+
cde.d0054 = @release
|
902
|
+
cde.d0057 = @subset
|
903
|
+
|
904
|
+
@header.cS006.d0040 = user_par[:sender] || root.header.cS002.d0004
|
905
|
+
@header.cS007.d0044 = user_par[:recipient] || root.header.cS003.d0010
|
906
|
+
@header.d0048 = user_par[:group_reference] || p.groups_created
|
907
|
+
# @trailer.d0048 = @header.d0048
|
908
|
+
|
909
|
+
t = Time.now
|
910
|
+
@header.cS004.d0017 = t.strftime(p.version==4 ? '%Y%m%d':'%y%m%d')
|
911
|
+
@header.cS004.d0019 = t.strftime("%H%M")
|
912
|
+
|
913
|
+
elsif user_par.is_a? Segment
|
914
|
+
|
915
|
+
@header = user_par
|
916
|
+
raise "UNG expected, #{@header.name} found!" if @header.name != 'UNG'
|
917
|
+
@header.parent = self
|
918
|
+
@header.root = self.root
|
919
|
+
|
920
|
+
# Assign a temporary UNE segment
|
921
|
+
de_sep = root.una.de_sep
|
922
|
+
@trailer = Segment.parse(root, 'UNE' << de_sep << '0' << de_sep << '0')
|
923
|
+
|
924
|
+
s008 = @header.cS008
|
925
|
+
@name = @header.d0038
|
926
|
+
@version = s008.d0052
|
927
|
+
@release = s008.d0054
|
928
|
+
@resp_agency = @header.d0051
|
929
|
+
@subset = s008.d0057
|
930
|
+
else
|
931
|
+
raise "First parameter: Illegal type!"
|
932
|
+
end
|
933
|
+
|
934
|
+
end
|
935
|
+
|
936
|
+
|
937
|
+
# Internal use only!
|
938
|
+
|
939
|
+
def preset_group(user_par) # :nodoc:
|
940
|
+
if (illegal_keys = user_par.keys - @@msggroup_default_keys) != []
|
941
|
+
msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
|
942
|
+
msg += "Valid param keys (symbols): #{@@msggroup_default_keys.join(', ')}"
|
943
|
+
raise ArgumentError, msg
|
944
|
+
end
|
945
|
+
par = @@msggroup_defaults.merge( user_par )
|
946
|
+
|
947
|
+
@name = par[:msg_type]
|
948
|
+
@version = par[:version]
|
949
|
+
@release = par[:release]
|
950
|
+
@resp_agency = par[:resp_agency]
|
951
|
+
@subset = par[:assigned_code]
|
952
|
+
# FIXME: Eliminate use of @version, @release, @resp_agency, @subset
|
953
|
+
# They get outdated whenever their UNG counterparts are changed
|
954
|
+
# Try to keep @name updated, or pass it a generic name
|
955
|
+
end
|
956
|
+
|
957
|
+
|
958
|
+
def MsgGroup.parse (p, segment_list) # List of segments
|
959
|
+
grp = p.new_msggroup(:msg_type => 'DUMMY')
|
960
|
+
|
961
|
+
# We now expect a sequence of segments that comprises one group,
|
962
|
+
# starting with UNG and ending with UNE, and with messages in between.
|
963
|
+
# We process the UNG/UNE envelope separately, then work on the content.
|
964
|
+
|
965
|
+
header = grp.parse_segment(segment_list.shift, 'UNG')
|
966
|
+
trailer = grp.parse_segment(segment_list.pop, 'UNE')
|
967
|
+
|
968
|
+
init_seg = Regexp.new('^UNH')
|
969
|
+
exit_seg = Regexp.new('^UNT')
|
970
|
+
|
971
|
+
while segbuf = segment_list.shift
|
972
|
+
case segbuf
|
973
|
+
|
974
|
+
when init_seg
|
975
|
+
sub_list = Array.new
|
976
|
+
sub_list.push segbuf
|
977
|
+
|
978
|
+
when exit_seg
|
979
|
+
sub_list.push segbuf
|
980
|
+
grp.add grp.parse_message(sub_list)
|
981
|
+
|
982
|
+
else
|
983
|
+
sub_list.push segbuf
|
984
|
+
end
|
985
|
+
end
|
986
|
+
|
987
|
+
grp.header = header
|
988
|
+
grp.trailer = trailer
|
989
|
+
grp
|
990
|
+
end
|
991
|
+
|
992
|
+
|
993
|
+
def new_message(params={})
|
994
|
+
@messages_created += 1
|
995
|
+
Message.new(self, params)
|
996
|
+
end
|
997
|
+
|
998
|
+
def new_segment(tag) # :nodoc:
|
999
|
+
Segment.new(self, tag)
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
|
1003
|
+
def parse_message(list) # :nodoc:
|
1004
|
+
Message.parse(self, list)
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
def parse_segment(buf, tag) # :nodoc:
|
1008
|
+
Segment.parse(self, buf, tag)
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
|
1012
|
+
def add( msg )
|
1013
|
+
super
|
1014
|
+
@trailer.d0060 = @trailer.d0060.to_i if @trailer.d0060.is_a? String
|
1015
|
+
@trailer.d0060 += 1
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
|
1019
|
+
def to_s
|
1020
|
+
postfix = '' << root.una.seg_term << root.e_linebreak
|
1021
|
+
super( postfix )
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
|
1025
|
+
def validate( err_count=0 )
|
1026
|
+
|
1027
|
+
# Consistency checks
|
1028
|
+
|
1029
|
+
if (a=@trailer.d0060) != (b=self.size)
|
1030
|
+
warn "UNE: DE 0060 (#{a}) does not match number of messages (#{b})"
|
1031
|
+
err_count += 1
|
1032
|
+
end
|
1033
|
+
a, b = @trailer.d0048, @header.d0048
|
1034
|
+
if a != b
|
1035
|
+
warn "UNE: DE 0048 (#{a}) does not match reference in UNG (#{b})"
|
1036
|
+
err_count += 1
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
# FIXME: Check if messages are uniquely numbered
|
1040
|
+
|
1041
|
+
super
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
|
1047
|
+
#########################################################################
|
1048
|
+
#
|
1049
|
+
# Class EDI::E::Message
|
1050
|
+
#
|
1051
|
+
# This class implements a single business document according to UN/EDIFACT
|
1052
|
+
|
1053
|
+
class Message < EDI::Message
|
1054
|
+
# private_class_method :new
|
1055
|
+
|
1056
|
+
@@message_defaults = {
|
1057
|
+
:msg_type => 'ORDERS', :version => 'D', :release => '96A',
|
1058
|
+
:resp_agency => 'UN', :assigned_code => nil # e.g. 'EAN008'
|
1059
|
+
}
|
1060
|
+
@@message_default_keys = @@message_defaults.keys
|
1061
|
+
|
1062
|
+
# Creates an empty UN/EDIFACT message
|
1063
|
+
# Don't use directly - use +new_message+ of class Interchange or MsgGroup instead!
|
1064
|
+
#
|
1065
|
+
# == First parameter
|
1066
|
+
#
|
1067
|
+
# This is always the parent object, either a message group
|
1068
|
+
# or an interchange object.
|
1069
|
+
# Use method +new_message+ in the corresponding object instead
|
1070
|
+
# of creating messages unattended, and the parent reference
|
1071
|
+
# will be accounted for automatically.
|
1072
|
+
#
|
1073
|
+
# == Second parameter, case "Hash"
|
1074
|
+
#
|
1075
|
+
# List of supported hash keys:
|
1076
|
+
#
|
1077
|
+
# === Essentials, should not be changed later
|
1078
|
+
#
|
1079
|
+
# :msg_type :: Sets S009.0065, default = 'ORDERS'
|
1080
|
+
# :version :: Sets S009.0052, default = 'D'
|
1081
|
+
# :release :: Sets S009.0054, default = '96A'
|
1082
|
+
# :resp_agency :: Sets S009.0051, default = 'UN'
|
1083
|
+
#
|
1084
|
+
# === Optional parameters, required depending upon use case
|
1085
|
+
#
|
1086
|
+
# :assigned_code :: Sets S009.0057 (subset), default = nil
|
1087
|
+
#
|
1088
|
+
# == Second parameter, case "Segment"
|
1089
|
+
#
|
1090
|
+
# This mode is only used internally when parsing data.
|
1091
|
+
#
|
1092
|
+
# == Notes
|
1093
|
+
#
|
1094
|
+
# * The counter in UNH (0062) is set automatically to a
|
1095
|
+
# number that is unique for the running process.
|
1096
|
+
# * The trailer segment (usually UNT) is generated automatically.
|
1097
|
+
# * Whenever possible, <b>avoid write access to the
|
1098
|
+
# message header or trailer segments</b>!
|
1099
|
+
|
1100
|
+
def initialize( p, user_par={} )
|
1101
|
+
super( p, user_par )
|
1102
|
+
|
1103
|
+
# First param is either a hash or segment UNH
|
1104
|
+
# - If Hash: Build UNH from given parameters
|
1105
|
+
# - If Segment: Extract some crucial parameters
|
1106
|
+
if user_par.is_a? Hash
|
1107
|
+
preset_msg( user_par )
|
1108
|
+
par = {
|
1109
|
+
:d0065 => @name, :d0052=> @version, :d0054=> @release,
|
1110
|
+
:d0051 => @resp_agency, :d0057 => @subset, :is_iedi => root.is_iedi?
|
1111
|
+
}
|
1112
|
+
@maindata = EDI::Dir::Directory.create(root.syntax, par )
|
1113
|
+
|
1114
|
+
if root.is_iedi?
|
1115
|
+
@header = new_segment('UIH')
|
1116
|
+
@trailer = new_segment('UIT')
|
1117
|
+
cde = @header.cS306
|
1118
|
+
# cde.d0113 = @sub_id
|
1119
|
+
@header.d0340 = p.messages_created
|
1120
|
+
else
|
1121
|
+
@header = new_segment('UNH')
|
1122
|
+
@trailer = new_segment('UNT')
|
1123
|
+
cde = @header.cS009
|
1124
|
+
@header.d0062 = p.messages_created
|
1125
|
+
end
|
1126
|
+
cde.d0065 = @name
|
1127
|
+
cde.d0052 = @version
|
1128
|
+
cde.d0054 = @release
|
1129
|
+
cde.d0051 = @resp_agency
|
1130
|
+
cde.d0057 = @subset
|
1131
|
+
|
1132
|
+
elsif user_par.is_a? Segment
|
1133
|
+
@header = user_par
|
1134
|
+
raise "UNH expected, #{@header.name} found!" if @header.name != 'UNH'
|
1135
|
+
# I-EDI support to be added!
|
1136
|
+
@header.parent = self
|
1137
|
+
@header.root = self.root
|
1138
|
+
@trailer = Segment.new(root, 'UNT') # temporary
|
1139
|
+
s009 = @header.cS009
|
1140
|
+
@name = s009.d0065
|
1141
|
+
@version = s009.d0052
|
1142
|
+
@release = s009.d0054
|
1143
|
+
@resp_agency = s009.d0051
|
1144
|
+
@subset = s009.d0057
|
1145
|
+
par = {
|
1146
|
+
:d0065 => @name, :d0052=> @version, :d0054=> @release,
|
1147
|
+
:d0051 => @resp_agency, :d0057 => @subset, :is_iedi => root.is_iedi?
|
1148
|
+
}
|
1149
|
+
@maindata = EDI::Dir::Directory.create(root.syntax, par )
|
1150
|
+
else
|
1151
|
+
raise "First parameter: Illegal type!"
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
@trailer.d0074 = 2 if @trailer # Just UNH and UNT so far
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
#
|
1158
|
+
# Derive a new segment with the given name from this message context.
|
1159
|
+
# The call will fail if the message name is unknown to this message's
|
1160
|
+
# UN/TDID (not in EDMD/IDMD).
|
1161
|
+
#
|
1162
|
+
# == Example:
|
1163
|
+
# seg = msg.new_segment( 'BGM' )
|
1164
|
+
# seg.d1004 = '220'
|
1165
|
+
# # etc.
|
1166
|
+
# msg.add seg
|
1167
|
+
#
|
1168
|
+
def new_segment( tag )
|
1169
|
+
Segment.new(self, tag)
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
# Internal use only!
|
1173
|
+
|
1174
|
+
def parse_segment(buf, tag) # :nodoc:
|
1175
|
+
Segment.parse(self, buf, tag)
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
# Internal use only!
|
1179
|
+
|
1180
|
+
def preset_msg(user_par) # :nodoc:
|
1181
|
+
if (illegal_keys = user_par.keys - @@message_default_keys) != []
|
1182
|
+
msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
|
1183
|
+
msg += "Valid param keys (symbols): #{@@message_default_keys.join(', ')}"
|
1184
|
+
raise ArgumentError, msg
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
# Use UNG as source for defaults if present
|
1188
|
+
ung = parent.header
|
1189
|
+
if parent.is_a?(MsgGroup) && ung.d0038
|
1190
|
+
s008 = ung.cS008
|
1191
|
+
par = {
|
1192
|
+
:msg_type=> ung.d0038, :version=> s008.d0052, :release=> s008.d0054,
|
1193
|
+
:resp_agency => ung.d0051, :assigned_code => s008.d0057
|
1194
|
+
}.merge( user_par )
|
1195
|
+
else
|
1196
|
+
par = @@message_defaults.merge( user_par )
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
@name = par[:msg_type]
|
1200
|
+
@version = par[:version]
|
1201
|
+
@release = par[:release]
|
1202
|
+
@resp_agency = par[:resp_agency]
|
1203
|
+
@subset = par[:assigned_code]
|
1204
|
+
# FIXME: Eliminate use of @version, @release, @resp_agency, @subset
|
1205
|
+
# They get outdated whenever their UNH counterparts are changed
|
1206
|
+
# Try to keep @name updated, or pass it a generic name
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
|
1210
|
+
# Returns a new Message object that contains the data of the
|
1211
|
+
# strings passed in the +segment_list+ array. Uses the context
|
1212
|
+
# of the given +parent+ object and configures message as a child.
|
1213
|
+
|
1214
|
+
def Message.parse (parent, segment_list)
|
1215
|
+
|
1216
|
+
if parent.root.is_iedi?
|
1217
|
+
h, t, re_t = 'UIH', 'UIT', /^UIT/
|
1218
|
+
else
|
1219
|
+
h, t, re_t = 'UNH', 'UNT', /^UNT/
|
1220
|
+
end
|
1221
|
+
|
1222
|
+
# Segments comprise a single message
|
1223
|
+
# Temporarily assign a parent, or else service segment lookup fails
|
1224
|
+
header = parent.parse_segment(segment_list.shift, h)
|
1225
|
+
msg = parent.new_message(header)
|
1226
|
+
trailer = msg.parse_segment( segment_list.pop, t )
|
1227
|
+
|
1228
|
+
segment_list.each do |segbuf|
|
1229
|
+
seg = Segment.parse( msg, segbuf )
|
1230
|
+
if segbuf =~ re_t # FIXME: Should that case ever occur?
|
1231
|
+
msg.trailer = seg
|
1232
|
+
else
|
1233
|
+
msg.add(seg)
|
1234
|
+
end
|
1235
|
+
end
|
1236
|
+
msg.trailer = trailer
|
1237
|
+
msg
|
1238
|
+
end
|
1239
|
+
|
1240
|
+
|
1241
|
+
#
|
1242
|
+
# Add a previously derived segment to the end of this message (append)
|
1243
|
+
# Make sure that all mandatory elements have been supplied.
|
1244
|
+
#
|
1245
|
+
# == Notes
|
1246
|
+
#
|
1247
|
+
# * Strictly add segments in the sequence described by this message's
|
1248
|
+
# branching diagram!
|
1249
|
+
#
|
1250
|
+
# * Adding a segment will automatically increase the corresponding
|
1251
|
+
# counter in the message trailer.
|
1252
|
+
#
|
1253
|
+
# == Example:
|
1254
|
+
# seg = msg.new_segment( 'BGM' )
|
1255
|
+
# seg.d1004 = '220'
|
1256
|
+
# # etc.
|
1257
|
+
# msg.add seg
|
1258
|
+
#
|
1259
|
+
def add( seg )
|
1260
|
+
super
|
1261
|
+
@trailer.d0074 = @trailer.d0074.to_i if @trailer.d0074.is_a? String
|
1262
|
+
@trailer.d0074 += 1 # What if new segment is/remains empty??
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
|
1266
|
+
def validate( err_count=0 )
|
1267
|
+
# Check sequence of segments against library,
|
1268
|
+
# thereby adding location information to each segment
|
1269
|
+
|
1270
|
+
par = {
|
1271
|
+
:d0065 => @name, :d0052=> @version, :d0054=> @release,
|
1272
|
+
:d0051 => @resp_agency, :d0057 => @subset,
|
1273
|
+
:d0002 => root.version, :is_iedi => root.is_iedi?,
|
1274
|
+
:d0076 => nil # SV 4-1 support still missing here
|
1275
|
+
}
|
1276
|
+
diag = EDI::Diagram::Diagram.create( root.syntax, par )
|
1277
|
+
ni = EDI::Diagram::NodeInstance.new(diag)
|
1278
|
+
|
1279
|
+
ni.seek!( @header )
|
1280
|
+
@header.update_with( ni )
|
1281
|
+
each do |seg|
|
1282
|
+
if ni.seek!(seg)
|
1283
|
+
seg.update_with( ni )
|
1284
|
+
else
|
1285
|
+
# FIXME: Do we really have to fail here, or would a "warn" suffice?
|
1286
|
+
raise "seek! failed for #{seg.name} when starting at #{ni.name}"
|
1287
|
+
end
|
1288
|
+
end
|
1289
|
+
ni.seek!( @trailer )
|
1290
|
+
@trailer.update_with( ni )
|
1291
|
+
|
1292
|
+
|
1293
|
+
# Consistency checks
|
1294
|
+
|
1295
|
+
if (a=@trailer.d0074) != (b=self.size+2)
|
1296
|
+
warn "DE 0074 (#{a}) does not match number of segments (#{b})"
|
1297
|
+
err_count += 1
|
1298
|
+
end
|
1299
|
+
|
1300
|
+
if root.is_iedi?
|
1301
|
+
a, b = @trailer.d0340, @header.d0340
|
1302
|
+
else
|
1303
|
+
a, b = @trailer.d0062, @header.d0062
|
1304
|
+
end
|
1305
|
+
if a != b
|
1306
|
+
warn "Trailer reference (#{a}) does not match header reference (#{b})"
|
1307
|
+
err_count += 1
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
if parent.is_a? MsgGroup
|
1311
|
+
ung = parent.header; s008 = ung.cS008; s009 = header.cS009
|
1312
|
+
a, b = s009.d0065, ung.d0038
|
1313
|
+
if a != b
|
1314
|
+
warn "Message type (#{a}) does not match that of group (#{b})"
|
1315
|
+
err_count += 1
|
1316
|
+
end
|
1317
|
+
a, b = s009.d0052, s008.d0052
|
1318
|
+
if a != b
|
1319
|
+
warn "Message version (#{a}) does not match that of group (#{b})"
|
1320
|
+
err_count += 1
|
1321
|
+
end
|
1322
|
+
a, b = s009.d0054, s008.d0054
|
1323
|
+
if a != b
|
1324
|
+
warn "Message release (#{a}) does not match that of group (#{b})"
|
1325
|
+
err_count += 1
|
1326
|
+
end
|
1327
|
+
a, b = s009.d0051, ung.d0051
|
1328
|
+
if a != b
|
1329
|
+
warn "Message responsible agency (#{a}) does not match that of group (#{b})"
|
1330
|
+
err_count += 1
|
1331
|
+
end
|
1332
|
+
a, b = s009.d0057, s008.d0057
|
1333
|
+
if a != b
|
1334
|
+
warn "Message association assigned code (#{a}) does not match that of group (#{b})"
|
1335
|
+
err_count += 1
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
end
|
1339
|
+
|
1340
|
+
# Now check each segment
|
1341
|
+
super( err_count )
|
1342
|
+
end
|
1343
|
+
|
1344
|
+
|
1345
|
+
def to_s
|
1346
|
+
postfix = '' << root.una.seg_term << root.e_linebreak
|
1347
|
+
super( postfix )
|
1348
|
+
end
|
1349
|
+
|
1350
|
+
end
|
1351
|
+
|
1352
|
+
|
1353
|
+
#########################################################################
|
1354
|
+
#
|
1355
|
+
# Class EDI::E::Segment
|
1356
|
+
#
|
1357
|
+
# This class implements UN/EDIFACT segments like BGM, NAD etc.,
|
1358
|
+
# including the service segments UNB, UNH ...
|
1359
|
+
#
|
1360
|
+
|
1361
|
+
class Segment < EDI::Segment
|
1362
|
+
|
1363
|
+
# A new segment must have a parent (usually, a message). This is the
|
1364
|
+
# first parameter. The second is a string with the desired segment tag.
|
1365
|
+
#
|
1366
|
+
# Don't create segments without their context - use Message#new_segment()
|
1367
|
+
# instead.
|
1368
|
+
|
1369
|
+
def initialize(p, tag)
|
1370
|
+
super( p, tag )
|
1371
|
+
|
1372
|
+
each_BCDS(tag) do |entry|
|
1373
|
+
id = entry.name
|
1374
|
+
status = entry.status
|
1375
|
+
|
1376
|
+
# FIXME: Code redundancy in type detection - remove later!
|
1377
|
+
case id
|
1378
|
+
when /[CES]\d{3}/ # Composite
|
1379
|
+
add new_CDE(id, status)
|
1380
|
+
when /\d{4}/ # Simple DE
|
1381
|
+
add new_DE(id, status, fmt_of_DE(id))
|
1382
|
+
else # Should never occur
|
1383
|
+
raise "Not a legal DE or CDE id: #{id}"
|
1384
|
+
end
|
1385
|
+
end
|
1386
|
+
end
|
1387
|
+
|
1388
|
+
|
1389
|
+
def new_CDE(id, status)
|
1390
|
+
CDE.new(self, id, status)
|
1391
|
+
end
|
1392
|
+
|
1393
|
+
|
1394
|
+
def new_DE(id, status, fmt)
|
1395
|
+
DE.new(self, id, status, fmt)
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
|
1399
|
+
# Reserved for internal use
|
1400
|
+
|
1401
|
+
def Segment.parse (p, buf, tag_expected=nil)
|
1402
|
+
# Buffer contains a single segment
|
1403
|
+
obj_list = EDI::E::edi_split( buf, p.root.una.de_sep, p.root.una.esc_char )
|
1404
|
+
tag = obj_list.shift # First entry must be the segment tag
|
1405
|
+
|
1406
|
+
raise "Illegal tag: #{tag}" unless tag =~ /[A-Z]{3}/
|
1407
|
+
if tag_expected and tag_expected != tag
|
1408
|
+
raise "Wrong segment name! Expected: #{tag_expected}, found: #{tag}"
|
1409
|
+
end
|
1410
|
+
|
1411
|
+
seg = p.new_segment(tag)
|
1412
|
+
seg.each {|obj| obj.parse( obj_list.shift ) }
|
1413
|
+
seg
|
1414
|
+
# Error handling needed here if obj_list is not exhausted now!
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
|
1418
|
+
def to_s
|
1419
|
+
s = ''
|
1420
|
+
return s if empty?
|
1421
|
+
|
1422
|
+
rt = self.root
|
1423
|
+
|
1424
|
+
indent = rt.e_indent * (self.level || 0)
|
1425
|
+
s << indent << name << rt.una.de_sep
|
1426
|
+
skip_count = 0
|
1427
|
+
each {|obj|
|
1428
|
+
if obj.empty?
|
1429
|
+
skip_count += 1
|
1430
|
+
else
|
1431
|
+
if skip_count > 0
|
1432
|
+
s << rt.una.de_sep.chr * skip_count
|
1433
|
+
skip_count = 0
|
1434
|
+
end
|
1435
|
+
s << obj.to_s
|
1436
|
+
skip_count += 1
|
1437
|
+
end
|
1438
|
+
}
|
1439
|
+
s
|
1440
|
+
end
|
1441
|
+
|
1442
|
+
|
1443
|
+
# Some exceptional setters, required for data consistency
|
1444
|
+
|
1445
|
+
# Don't change DE 0001! d0001=() raises an exception when called.
|
1446
|
+
def d0001=( value ); fail "Charset not modifiable!"; end
|
1447
|
+
|
1448
|
+
# Don't change DE 0002! d0002=() raises an exception when called.
|
1449
|
+
def d0002=( value ); fail "EDIFACT Syntax version not modifiable!"; end
|
1450
|
+
|
1451
|
+
# Setter for DE 0020 in UNB & UNZ (interchange control reference)
|
1452
|
+
def d0020=( value )
|
1453
|
+
return super unless self.name=~/UN[BZ]/
|
1454
|
+
parent.header['0020'].first.value = value
|
1455
|
+
parent.trailer['0020'].first.value = value
|
1456
|
+
end
|
1457
|
+
|
1458
|
+
# Setter for DE 0048 in UNE & UNG (group reference)
|
1459
|
+
def d0048=( value )
|
1460
|
+
return super unless self.name=~/UN[GE]/
|
1461
|
+
parent.header['0048'].first.value = value
|
1462
|
+
parent.trailer['0048'].first.value = value
|
1463
|
+
end
|
1464
|
+
|
1465
|
+
# Setter for DE 0062 in UNH & UNT (message reference number)
|
1466
|
+
def d0062=( value ) # UNH
|
1467
|
+
return super unless self.name=~/UN[HT]/
|
1468
|
+
parent.header['0062'].first.value = value
|
1469
|
+
parent.trailer['0062'].first.value = value
|
1470
|
+
end
|
1471
|
+
|
1472
|
+
# Setter for DE 0340 in UIH & UIT (message reference number)
|
1473
|
+
def d0340=( value ) # UIH
|
1474
|
+
return super unless self.name=~/UI[HT]/
|
1475
|
+
parent.header['0340'].first.value = value
|
1476
|
+
parent.trailer['0340'].first.value = value
|
1477
|
+
end
|
1478
|
+
|
1479
|
+
end
|
1480
|
+
|
1481
|
+
|
1482
|
+
#########################################################################
|
1483
|
+
#
|
1484
|
+
# Class EDI::E::CDE
|
1485
|
+
#
|
1486
|
+
# This class implements UN/EDIFACT composite data elements C507 etc.,
|
1487
|
+
# including the service CDEs S001, S009 ...
|
1488
|
+
#
|
1489
|
+
# For internal use only.
|
1490
|
+
|
1491
|
+
class CDE < EDI::CDE
|
1492
|
+
|
1493
|
+
def initialize(p, name, status)
|
1494
|
+
super(p, name, status)
|
1495
|
+
|
1496
|
+
each_BCDS(name) do |entry|
|
1497
|
+
id = entry.name
|
1498
|
+
status = entry.status
|
1499
|
+
# FIXME: Code redundancy in type detection - remove later!
|
1500
|
+
if id =~ /\d{4}/
|
1501
|
+
add new_DE(id, status, fmt_of_DE(id))
|
1502
|
+
else # Should never occur
|
1503
|
+
raise "Not a legal DE: #{id}"
|
1504
|
+
end
|
1505
|
+
end
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
def new_DE(id, status, fmt)
|
1509
|
+
DE.new(self, id, status, fmt)
|
1510
|
+
end
|
1511
|
+
|
1512
|
+
|
1513
|
+
def parse (buf) # Buffer contains content of a single CDE
|
1514
|
+
return nil unless buf
|
1515
|
+
obj_list = EDI::E::edi_split( buf, root.una.ce_sep, root.una.esc_char )
|
1516
|
+
each {|obj| obj.parse( obj_list.shift ) }
|
1517
|
+
# FIXME: Error handling needed here if obj_list is not exhausted now!
|
1518
|
+
end
|
1519
|
+
|
1520
|
+
|
1521
|
+
def to_s
|
1522
|
+
rt = self.root
|
1523
|
+
s = ''; skip_count = 0
|
1524
|
+
ce_sep = rt.una.ce_sep.chr
|
1525
|
+
each {|de|
|
1526
|
+
if de.empty?
|
1527
|
+
skip_count += 1
|
1528
|
+
else
|
1529
|
+
if skip_count > 0
|
1530
|
+
s << ce_sep * skip_count
|
1531
|
+
skip_count = 0
|
1532
|
+
end
|
1533
|
+
s << de.to_s
|
1534
|
+
skip_count += 1
|
1535
|
+
end
|
1536
|
+
}
|
1537
|
+
s
|
1538
|
+
end
|
1539
|
+
|
1540
|
+
end
|
1541
|
+
|
1542
|
+
|
1543
|
+
#########################################################################
|
1544
|
+
#
|
1545
|
+
# Class EDI::E::DE
|
1546
|
+
#
|
1547
|
+
# This class implements UN/EDIFACT data elements 1004, 2005 etc.,
|
1548
|
+
# including the service DEs 0001, 0004, ...
|
1549
|
+
#
|
1550
|
+
# For internal use only.
|
1551
|
+
|
1552
|
+
class DE < EDI::DE
|
1553
|
+
|
1554
|
+
def initialize( p, name, status, fmt )
|
1555
|
+
super( p, name, status, fmt )
|
1556
|
+
raise "Illegal DE name: #{name}" unless name =~ /\d{4}/
|
1557
|
+
# check if supported format syntax
|
1558
|
+
# check if supported status value
|
1559
|
+
end
|
1560
|
+
|
1561
|
+
|
1562
|
+
# Generate the DE content from the given string representation.
|
1563
|
+
# +buf+ contains a single DE string, possibly escaped
|
1564
|
+
|
1565
|
+
def parse( buf, already_escaped=false )
|
1566
|
+
return nil unless buf
|
1567
|
+
return @value = nil if buf.empty?
|
1568
|
+
@value = already_escaped ? buf : unescape(buf)
|
1569
|
+
if format[0] == ?n
|
1570
|
+
# Normalize decimal sign
|
1571
|
+
@value.sub!(/,/, '.')
|
1572
|
+
# Select appropriate Numeric, FIXME: Also match exponents!
|
1573
|
+
self.value = @value=~/\d+\.\d+/ ? @value.to_f : @value.to_i
|
1574
|
+
end
|
1575
|
+
@value
|
1576
|
+
end
|
1577
|
+
|
1578
|
+
|
1579
|
+
def to_s( no_escape=false )
|
1580
|
+
return '' if empty?
|
1581
|
+
s = if @value.is_a? Numeric
|
1582
|
+
# Adjust decimal sign
|
1583
|
+
super().sub(/[.,]/, root.una.decimal_sign.chr)
|
1584
|
+
else
|
1585
|
+
super().to_s
|
1586
|
+
end
|
1587
|
+
no_escape ? s : escape(s)
|
1588
|
+
end
|
1589
|
+
|
1590
|
+
|
1591
|
+
# The proper method to assign values to a DE.
|
1592
|
+
# The passed value must respond to +to_i+ .
|
1593
|
+
|
1594
|
+
def value=( val )
|
1595
|
+
# Suppress trailing decimal part if Integer value
|
1596
|
+
ival = val.to_i
|
1597
|
+
val = ival if val.is_a? Float and val == ival
|
1598
|
+
super
|
1599
|
+
end
|
1600
|
+
|
1601
|
+
|
1602
|
+
private
|
1603
|
+
|
1604
|
+
def escape (str)
|
1605
|
+
rt = self.root
|
1606
|
+
raise "Must have a root to do this" if rt == nil
|
1607
|
+
|
1608
|
+
esc = rt.una.esc_char.chr
|
1609
|
+
esc << ?\\ if esc == '\\' # Special case if backslash!
|
1610
|
+
|
1611
|
+
if rt.charset == 'UNOA'
|
1612
|
+
# Implicit conversion to uppercase - convenient,
|
1613
|
+
# but could be argued against!
|
1614
|
+
str.upcase.gsub(rt.una.pattern_esc, esc+'\1')
|
1615
|
+
else
|
1616
|
+
str.gsub(rt.una.pattern_esc, esc+'\1')
|
1617
|
+
end
|
1618
|
+
end
|
1619
|
+
|
1620
|
+
def unescape (str)
|
1621
|
+
rt = self.root
|
1622
|
+
raise "Must have a root to do this" if rt == nil
|
1623
|
+
str.gsub(rt.una.pattern_unesc, '\1\2')
|
1624
|
+
end
|
1625
|
+
end
|
1626
|
+
|
1627
|
+
end # module EDI
|