tradedoc 0.1.0
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.
- checksums.yaml +7 -0
- data/.editorconfig +13 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +53 -0
- data/Rakefile +10 -0
- data/lib/tradedoc/code/agency.rb +8 -0
- data/lib/tradedoc/code/document_type.rb +95 -0
- data/lib/tradedoc/code/national_clearing_system.rb +97 -0
- data/lib/tradedoc/code/payment_means_type.rb +107 -0
- data/lib/tradedoc/code/tax_category.rb +97 -0
- data/lib/tradedoc/code.rb +12 -0
- data/lib/tradedoc/format/apeh.rb +36 -0
- data/lib/tradedoc/format/cii/coder/address.rb +55 -0
- data/lib/tradedoc/format/cii/coder/big_decimal.rb +21 -0
- data/lib/tradedoc/format/cii/coder/contact.rb +43 -0
- data/lib/tradedoc/format/cii/coder/date.rb +87 -0
- data/lib/tradedoc/format/cii/coder/document_reference.rb +33 -0
- data/lib/tradedoc/format/cii/coder/exchange_rate.rb +33 -0
- data/lib/tradedoc/format/cii/coder/financial_account.rb +45 -0
- data/lib/tradedoc/format/cii/coder/financial_institution.rb +39 -0
- data/lib/tradedoc/format/cii/coder/invoice.rb +127 -0
- data/lib/tradedoc/format/cii/coder/invoice_line.rb +88 -0
- data/lib/tradedoc/format/cii/coder/monetary_total.rb +36 -0
- data/lib/tradedoc/format/cii/coder/money.rb +27 -0
- data/lib/tradedoc/format/cii/coder/payment_means.rb +49 -0
- data/lib/tradedoc/format/cii/coder/period.rb +27 -0
- data/lib/tradedoc/format/cii/coder/remittance_advice.rb +130 -0
- data/lib/tradedoc/format/cii/coder/string.rb +21 -0
- data/lib/tradedoc/format/cii/coder/tax_subtotal.rb +35 -0
- data/lib/tradedoc/format/cii/coder/trade_party.rb +29 -0
- data/lib/tradedoc/format/cii/coder.rb +27 -0
- data/lib/tradedoc/format/cii.rb +12 -0
- data/lib/tradedoc/format/facturae.rb +51 -0
- data/lib/tradedoc/format/fatturapa/coder/invoice.rb +31 -0
- data/lib/tradedoc/format/fatturapa/coder.rb +8 -0
- data/lib/tradedoc/format/fatturapa.rb +13 -0
- data/lib/tradedoc/format/isdoc.rb +40 -0
- data/lib/tradedoc/format/ksef/coder/invoice.rb +32 -0
- data/lib/tradedoc/format/ksef/coder.rb +10 -0
- data/lib/tradedoc/format/ksef.rb +17 -0
- data/lib/tradedoc/format/mydata.rb +42 -0
- data/lib/tradedoc/format/nav.rb +38 -0
- data/lib/tradedoc/format/ubl/coder/address.rb +48 -0
- data/lib/tradedoc/format/ubl/coder/big_decimal.rb +21 -0
- data/lib/tradedoc/format/ubl/coder/contact.rb +29 -0
- data/lib/tradedoc/format/ubl/coder/country.rb +27 -0
- data/lib/tradedoc/format/ubl/coder/date.rb +21 -0
- data/lib/tradedoc/format/ubl/coder/document_reference.rb +55 -0
- data/lib/tradedoc/format/ubl/coder/exchange_rate.rb +33 -0
- data/lib/tradedoc/format/ubl/coder/financial_account.rb +32 -0
- data/lib/tradedoc/format/ubl/coder/financial_institution.rb +42 -0
- data/lib/tradedoc/format/ubl/coder/invoice.rb +92 -0
- data/lib/tradedoc/format/ubl/coder/invoice_line.rb +53 -0
- data/lib/tradedoc/format/ubl/coder/monetary_total.rb +39 -0
- data/lib/tradedoc/format/ubl/coder/money.rb +22 -0
- data/lib/tradedoc/format/ubl/coder/payment_means.rb +39 -0
- data/lib/tradedoc/format/ubl/coder/period.rb +27 -0
- data/lib/tradedoc/format/ubl/coder/product.rb +37 -0
- data/lib/tradedoc/format/ubl/coder/remittance_advice.rb +91 -0
- data/lib/tradedoc/format/ubl/coder/string.rb +21 -0
- data/lib/tradedoc/format/ubl/coder/tax_breakdown.rb +27 -0
- data/lib/tradedoc/format/ubl/coder/tax_category.rb +37 -0
- data/lib/tradedoc/format/ubl/coder/tax_subtotal.rb +29 -0
- data/lib/tradedoc/format/ubl/coder/trade_party.rb +37 -0
- data/lib/tradedoc/format/ubl/coder.rb +29 -0
- data/lib/tradedoc/format/ubl.rb +13 -0
- data/lib/tradedoc/format/zugferdv1/coder/invoice.rb +33 -0
- data/lib/tradedoc/format/zugferdv1/coder.rb +8 -0
- data/lib/tradedoc/format/zugferdv1.rb +21 -0
- data/lib/tradedoc/format.rb +110 -0
- data/lib/tradedoc/model/address.rb +21 -0
- data/lib/tradedoc/model/base.rb +153 -0
- data/lib/tradedoc/model/contact.rb +9 -0
- data/lib/tradedoc/model/country.rb +8 -0
- data/lib/tradedoc/model/country_subdivision.rb +8 -0
- data/lib/tradedoc/model/document.rb +9 -0
- data/lib/tradedoc/model/document_reference.rb +11 -0
- data/lib/tradedoc/model/exchange_rate.rb +17 -0
- data/lib/tradedoc/model/financial_account.rb +19 -0
- data/lib/tradedoc/model/financial_institution.rb +11 -0
- data/lib/tradedoc/model/invoice.rb +57 -0
- data/lib/tradedoc/model/invoice_line.rb +46 -0
- data/lib/tradedoc/model/monetary_total.rb +34 -0
- data/lib/tradedoc/model/payment_means.rb +27 -0
- data/lib/tradedoc/model/period.rb +8 -0
- data/lib/tradedoc/model/price.rb +21 -0
- data/lib/tradedoc/model/product.rb +18 -0
- data/lib/tradedoc/model/remittance_advice.rb +20 -0
- data/lib/tradedoc/model/remittance_advice_line.rb +24 -0
- data/lib/tradedoc/model/tax_breakdown.rb +11 -0
- data/lib/tradedoc/model/tax_category.rb +26 -0
- data/lib/tradedoc/model/tax_subtotal.rb +13 -0
- data/lib/tradedoc/model/trade_party.rb +9 -0
- data/lib/tradedoc/model.rb +33 -0
- data/lib/tradedoc/version.rb +5 -0
- data/lib/tradedoc/xml/reader.rb +163 -0
- data/lib/tradedoc/xml/writer.rb +56 -0
- data/lib/tradedoc/xml.rb +8 -0
- data/lib/tradedoc.rb +98 -0
- data/sig/tradedoc.rbs +4 -0
- metadata +187 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Format
|
|
3
|
+
module UBL
|
|
4
|
+
module Coder
|
|
5
|
+
class TaxSubtotal
|
|
6
|
+
def self.ruby_type
|
|
7
|
+
Model::TaxSubtotal
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.dump(w, obj, as: "cac:TaxSubtotal")
|
|
11
|
+
w.add(as) do
|
|
12
|
+
w.render(obj.taxable_amount, as: "TaxableAmount")
|
|
13
|
+
w.render(obj.tax_amount, as: "TaxAmount")
|
|
14
|
+
w.render(obj.tax_category, as: "cac:TaxCategory")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.parse(r)
|
|
19
|
+
ruby_type.new.tap do |subtotal|
|
|
20
|
+
r.parse("cbc:TaxableAmount", :Money) { subtotal.taxable_amount = it }
|
|
21
|
+
r.parse("cbc:TaxAmount", :Money) { subtotal.tax_amount = it }
|
|
22
|
+
r.parse("cac:TaxCategory", :TaxCategory) { subtotal.tax_category = it }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Format
|
|
3
|
+
module UBL
|
|
4
|
+
module Coder
|
|
5
|
+
class TradeParty
|
|
6
|
+
def self.ruby_type
|
|
7
|
+
Model::TradeParty
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.dump(w, obj, as: "Party")
|
|
11
|
+
w.add(as) do
|
|
12
|
+
w.add("cac:PartyName") do
|
|
13
|
+
w.add("cbc:Name", obj.name)
|
|
14
|
+
end
|
|
15
|
+
w.render(obj.address, as: "PostalAddress")
|
|
16
|
+
w.render(obj.contact, as: "cac:Contact")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.parse(r)
|
|
21
|
+
obj = ruby_type.new
|
|
22
|
+
|
|
23
|
+
r.with_node("cac:Party") do
|
|
24
|
+
r.with_node("cac:PartyName") do
|
|
25
|
+
r.parse("cbc:Name", :String) { obj.name = it }
|
|
26
|
+
end
|
|
27
|
+
r.parse("cac:PostalAddress", :Address) { obj.address = it }
|
|
28
|
+
r.parse("cac:Contact", :Contact) { obj.contact = it }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
obj
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Format
|
|
3
|
+
module Coder
|
|
4
|
+
end
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative "coder/address"
|
|
9
|
+
require_relative "coder/big_decimal"
|
|
10
|
+
require_relative "coder/contact"
|
|
11
|
+
require_relative "coder/country"
|
|
12
|
+
require_relative "coder/date"
|
|
13
|
+
require_relative "coder/document_reference"
|
|
14
|
+
require_relative "coder/exchange_rate"
|
|
15
|
+
require_relative "coder/financial_account"
|
|
16
|
+
require_relative "coder/financial_institution"
|
|
17
|
+
require_relative "coder/invoice"
|
|
18
|
+
require_relative "coder/invoice_line"
|
|
19
|
+
require_relative "coder/monetary_total"
|
|
20
|
+
require_relative "coder/money"
|
|
21
|
+
require_relative "coder/payment_means"
|
|
22
|
+
require_relative "coder/period"
|
|
23
|
+
require_relative "coder/product"
|
|
24
|
+
require_relative "coder/remittance_advice"
|
|
25
|
+
require_relative "coder/string"
|
|
26
|
+
require_relative "coder/tax_breakdown"
|
|
27
|
+
require_relative "coder/tax_category"
|
|
28
|
+
require_relative "coder/tax_subtotal"
|
|
29
|
+
require_relative "coder/trade_party"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Format
|
|
3
|
+
module ZugferdV1
|
|
4
|
+
module Coder
|
|
5
|
+
class Invoice
|
|
6
|
+
NS = {
|
|
7
|
+
"xmlns:ram" => "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12",
|
|
8
|
+
"xmlns:rsm" => "urn:ferd:CrossIndustryDocument:invoice:1p0",
|
|
9
|
+
"xmlns:udt" => "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15"
|
|
10
|
+
}
|
|
11
|
+
private_constant :NS
|
|
12
|
+
|
|
13
|
+
def self.namespaces
|
|
14
|
+
NS
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param xmldoc [Nokogiri::XML::Document]
|
|
18
|
+
def self.can_parse?(xmldoc)
|
|
19
|
+
xmldoc.root.namespace&.href == namespaces.fetch("xmlns:rsm")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.dump(w, obj)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.parse(r)
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Format
|
|
3
|
+
# ZUGFeRDv1 is a German standard that is the predecessor of CII and its
|
|
4
|
+
# data structures are quite similar and sometimes identical.
|
|
5
|
+
#
|
|
6
|
+
# Not officially obsolete (as of June 2026) but its website says:
|
|
7
|
+
# ...has never been officially deprecated but is likely to be discontinued
|
|
8
|
+
# and it's not compliant to EN16931
|
|
9
|
+
module ZugferdV1
|
|
10
|
+
extend Accessors
|
|
11
|
+
extend Finders
|
|
12
|
+
extend XMLSerialization
|
|
13
|
+
|
|
14
|
+
def self.label
|
|
15
|
+
"ZUGFeRDv1"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require_relative "zugferdv1/coder"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Format
|
|
3
|
+
module Accessors
|
|
4
|
+
# User-friendly label for this format.
|
|
5
|
+
# @return [String]
|
|
6
|
+
def label
|
|
7
|
+
name.split("::").last
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# List of classes that can dump and parse Ruby objects to/from XML
|
|
11
|
+
# @return [Array<Class>]
|
|
12
|
+
def coders
|
|
13
|
+
ns = self::Coder
|
|
14
|
+
|
|
15
|
+
ns.constants.filter_map do |const_name|
|
|
16
|
+
val = ns.const_get(const_name)
|
|
17
|
+
if val.respond_to?(:dump) && val.respond_to?(:parse)
|
|
18
|
+
val
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# For faster detection of what files may be parsable by this format
|
|
24
|
+
def file_extensions
|
|
25
|
+
Set[".xml"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# List of coders that are document types.
|
|
29
|
+
# These are the only ones that can be read/written to/from XML properly.
|
|
30
|
+
#
|
|
31
|
+
# @return [Array<Class>]
|
|
32
|
+
def document_coders
|
|
33
|
+
coders.select { it.respond_to?(:namespaces) && it.respond_to?(:can_parse?) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module Finders
|
|
38
|
+
# Given a Ruby type, get the coder class for it.
|
|
39
|
+
#
|
|
40
|
+
# @param coder_ref [Symbol | Class]
|
|
41
|
+
# symbol: "bare" name of a type like :Date, :BigDecimal, :TradeParty
|
|
42
|
+
# class: one of:
|
|
43
|
+
# - Ruby type like `::Date` or `Model::TradeParty` to encode
|
|
44
|
+
# - A valid coder class (responds to .dump and .parse)
|
|
45
|
+
# @return [Class]
|
|
46
|
+
def coder_for(coder_ref)
|
|
47
|
+
matches = case coder_ref
|
|
48
|
+
in Class => coder if coders.include?(coder)
|
|
49
|
+
[coder]
|
|
50
|
+
in Class => ruby_type
|
|
51
|
+
coders.select { it.ruby_type == ruby_type }
|
|
52
|
+
in Symbol => type_sym
|
|
53
|
+
coders.select { it.ruby_type.name.split("::").last == type_sym.to_s }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if matches.none?
|
|
57
|
+
raise "couldn't find a coder for '#{coder_ref}'"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# This shouldn't happen unless there's a misconfigure in this library itself.
|
|
61
|
+
if matches.count > 1
|
|
62
|
+
raise "found multiple coders for #{coder_ref}: #{matches}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
matches[0]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def detect_document_coder(xmldoc)
|
|
69
|
+
document_coders.detect { it.can_parse?(xmldoc) }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
module XMLSerialization
|
|
74
|
+
# @param obj [Class] A document-level model to serialize
|
|
75
|
+
# @return [Nokogiri::XML::Document]
|
|
76
|
+
def dump(obj)
|
|
77
|
+
encoding = Encoding::UTF_8.to_s
|
|
78
|
+
builder = Nokogiri::XML::Builder.new(encoding:) do |xml|
|
|
79
|
+
writer = XML::Writer.new(xml, format: self)
|
|
80
|
+
writer.render(obj)
|
|
81
|
+
end
|
|
82
|
+
builder.doc
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Given an XML document (as an object or string), detect the type
|
|
86
|
+
# and parse it into a model
|
|
87
|
+
#
|
|
88
|
+
# @param source [Nokogiri::XML::Document | String]
|
|
89
|
+
# @param coder [Class] If the coder is already known
|
|
90
|
+
# @return [Object] Model instance
|
|
91
|
+
def parse(source, coder: nil)
|
|
92
|
+
xml = Tradedoc.xml_from(source)
|
|
93
|
+
coder ||= detect_document_coder(xml)
|
|
94
|
+
reader = XML::Reader.new(xml, coder.namespaces, format: self)
|
|
95
|
+
coder.parse(reader)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
require_relative "format/apeh"
|
|
102
|
+
require_relative "format/cii"
|
|
103
|
+
require_relative "format/facturae"
|
|
104
|
+
require_relative "format/fatturapa"
|
|
105
|
+
require_relative "format/isdoc"
|
|
106
|
+
require_relative "format/ksef"
|
|
107
|
+
require_relative "format/mydata"
|
|
108
|
+
require_relative "format/nav"
|
|
109
|
+
require_relative "format/ubl"
|
|
110
|
+
require_relative "format/zugferdv1"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class Address < Base
|
|
4
|
+
has :city, String
|
|
5
|
+
has :postal_code, String
|
|
6
|
+
has :country, Country
|
|
7
|
+
has :subdivision, CountrySubdivision
|
|
8
|
+
|
|
9
|
+
# According to documentation and examples from UBL and CII, address lines
|
|
10
|
+
# are used when you can't or don't want to break-down the parts of an address
|
|
11
|
+
# to specific components like building number, suite/apartment number, PO Box,
|
|
12
|
+
# attention of, care of, department code, etc. Free-form lines are easier to work with.
|
|
13
|
+
has_many :lines, String
|
|
14
|
+
|
|
15
|
+
# Optionally, the specific components of the address lines.
|
|
16
|
+
has :street_name, String
|
|
17
|
+
has :building_number, String
|
|
18
|
+
has :additional_street_name, String
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
# Provides a simple interface for models where you can define the attributes it has
|
|
4
|
+
# and the correct types for each, and providing some simple coercion.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# class DocumentLine < Base
|
|
8
|
+
# has :id, String
|
|
9
|
+
# has :amount, Money
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# class Document < Base
|
|
13
|
+
# has :id, String
|
|
14
|
+
# has :issue_date, Date
|
|
15
|
+
# has_many :lines, Line
|
|
16
|
+
# end
|
|
17
|
+
class Base
|
|
18
|
+
Element = Data.define(:name, :type, :many)
|
|
19
|
+
|
|
20
|
+
module Serialization
|
|
21
|
+
def dump(to_format)
|
|
22
|
+
fmt = Tradedoc.format_from(to_format)
|
|
23
|
+
fmt.dump(self)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(**attrs)
|
|
28
|
+
attrs.each { |key, value| public_send(:"#{key}=", value) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def is_document
|
|
33
|
+
include(Serialization)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def has(name, type)
|
|
37
|
+
def_element(name, type, false)
|
|
38
|
+
|
|
39
|
+
ivar_name = :"@#{name}"
|
|
40
|
+
|
|
41
|
+
define_method(name) { instance_variable_get(ivar_name) }
|
|
42
|
+
|
|
43
|
+
define_method("#{name}=") do |value|
|
|
44
|
+
value_to_write = self.class.coerce_for(name, value)
|
|
45
|
+
instance_variable_set(ivar_name, value_to_write)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def has_many(name, type, default: -> { [] })
|
|
50
|
+
def_element(name, type, true)
|
|
51
|
+
|
|
52
|
+
ivar_name = :"@#{name}"
|
|
53
|
+
|
|
54
|
+
define_method(name) do
|
|
55
|
+
unless instance_variable_defined?(ivar_name)
|
|
56
|
+
instance_variable_set(ivar_name, default.call)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
instance_variable_get(ivar_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
define_method("#{name}=") do |values|
|
|
63
|
+
value_to_write = values.map { |value| self.class.coerce_for(name, value) }
|
|
64
|
+
instance_variable_set(ivar_name, value_to_write)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def inherited(subclass)
|
|
69
|
+
subclass.inherit_elements(elements)
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def elements
|
|
74
|
+
@elements ||= {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def inherit_elements(other)
|
|
78
|
+
@elements ||= {}
|
|
79
|
+
@elements = @elements.merge(other)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def coerce_for(element_name, value)
|
|
83
|
+
proper_type = elements.fetch(element_name).type
|
|
84
|
+
|
|
85
|
+
case value
|
|
86
|
+
in ^proper_type | nil => as_is
|
|
87
|
+
as_is
|
|
88
|
+
in other
|
|
89
|
+
if proper_type < Base
|
|
90
|
+
proper_type.new(**other)
|
|
91
|
+
elsif proper_type.respond_to?(:parse)
|
|
92
|
+
proper_type.parse(other)
|
|
93
|
+
else
|
|
94
|
+
proper_type.new(other)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def def_element(name, type, many)
|
|
102
|
+
elements[name] = Element.new(name:, type:, many:)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def merge(other)
|
|
107
|
+
to_h.deep_merge(other.to_h)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_h
|
|
111
|
+
self.class.elements.keys.each_with_object({}) do |k, h|
|
|
112
|
+
h[k] = case send(k)
|
|
113
|
+
in nil
|
|
114
|
+
nil
|
|
115
|
+
in Array => arr
|
|
116
|
+
arr.map do |item|
|
|
117
|
+
item.respond_to?(:to_h) ? item.to_h : item
|
|
118
|
+
end
|
|
119
|
+
in obj if obj.respond_to?(:to_h)
|
|
120
|
+
obj.to_h
|
|
121
|
+
in other
|
|
122
|
+
other
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def ==(other)
|
|
128
|
+
case other
|
|
129
|
+
in nil
|
|
130
|
+
false
|
|
131
|
+
in Hash
|
|
132
|
+
to_h == other
|
|
133
|
+
in obj if obj.respond_to?(:to_h)
|
|
134
|
+
to_h == other.to_h
|
|
135
|
+
in unmatched
|
|
136
|
+
raise "can't compare #{self.class.name} to #{unmatched}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def hash
|
|
141
|
+
to_h.hash
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def eql?(other)
|
|
145
|
+
to_h == other
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def deconstruct_keys(keys)
|
|
149
|
+
to_h.slice(*keys)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class ExchangeRate < Base
|
|
4
|
+
has :source_currency_code, String
|
|
5
|
+
has :target_currency_code, String
|
|
6
|
+
|
|
7
|
+
# e.g. 1 USD = 0.92 EU, calculation rate is 0.92
|
|
8
|
+
has :rate, BigDecimal
|
|
9
|
+
|
|
10
|
+
# FX date
|
|
11
|
+
has :date, Date
|
|
12
|
+
|
|
13
|
+
# Where did the exchange rate come from?
|
|
14
|
+
has :market_id, String
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class FinancialAccount < Base
|
|
4
|
+
# Identifies the kind of account numbering scheme
|
|
5
|
+
# Documentation for this is hard to come by. "IBAN" is for sure one of them,
|
|
6
|
+
# and "SWIFT" and "SWIFTBIC" have both been observed in samples and docs
|
|
7
|
+
has :scheme_name, String
|
|
8
|
+
|
|
9
|
+
# [BT-84]
|
|
10
|
+
has :account_number, String
|
|
11
|
+
|
|
12
|
+
# [BT-85]
|
|
13
|
+
has :name, String
|
|
14
|
+
|
|
15
|
+
# Only necessary for non-IBAN
|
|
16
|
+
has :financial_institution, FinancialInstitution
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class FinancialInstitution < Base
|
|
4
|
+
# For SWIFT, this is the BIC
|
|
5
|
+
# For national clearing systems, this is the member ID e.g. US Routing, UK Sort
|
|
6
|
+
has :id, String
|
|
7
|
+
|
|
8
|
+
has :national_clearing_system, Code::NationalClearingSystem
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class Invoice < Base
|
|
4
|
+
is_document
|
|
5
|
+
|
|
6
|
+
# [BT-1]
|
|
7
|
+
has :invoice_number, String
|
|
8
|
+
|
|
9
|
+
# [BT-2]
|
|
10
|
+
has :issue_date, Date
|
|
11
|
+
|
|
12
|
+
# [BT-3]
|
|
13
|
+
has :invoice_type_code, String
|
|
14
|
+
|
|
15
|
+
# [BT-5]
|
|
16
|
+
has :currency_code, String
|
|
17
|
+
|
|
18
|
+
# [BT-9]
|
|
19
|
+
has :due_date, Date
|
|
20
|
+
|
|
21
|
+
# [BT-10] Identifier created by the buyer for internal routing e.g. cost centre
|
|
22
|
+
# If you're looking for a PO number, see BT-13
|
|
23
|
+
has :buyer_reference, String
|
|
24
|
+
|
|
25
|
+
# [BT-13] PO number created by the buyer for three-way matching.
|
|
26
|
+
has :purchase_order_number, String
|
|
27
|
+
|
|
28
|
+
# [BT-22]
|
|
29
|
+
has :note, String
|
|
30
|
+
has :payment_means, PaymentMeans
|
|
31
|
+
|
|
32
|
+
# [BT-24]
|
|
33
|
+
# Identifier for the set of rules, content, conformity, etc.
|
|
34
|
+
# The standard seen for EU invoice is `urn:cen.eu:en16931:2017`
|
|
35
|
+
has :specification_id, String
|
|
36
|
+
|
|
37
|
+
# [BG-5]
|
|
38
|
+
has :invoice_period, Period
|
|
39
|
+
|
|
40
|
+
# [BG-7]
|
|
41
|
+
has :supplier, TradeParty
|
|
42
|
+
|
|
43
|
+
# [BG-10]
|
|
44
|
+
has :buyer, TradeParty
|
|
45
|
+
|
|
46
|
+
# [BG-17]
|
|
47
|
+
has_many :payment_means, PaymentMeans
|
|
48
|
+
|
|
49
|
+
# [BG-22]
|
|
50
|
+
has :monetary_total, MonetaryTotal
|
|
51
|
+
|
|
52
|
+
# [BG-25]
|
|
53
|
+
has_many :lines, InvoiceLine
|
|
54
|
+
alias_method :line_items, :lines
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class InvoiceLine < Base
|
|
4
|
+
# [BT-126] Unique ID for the invoice line within the invoice itself
|
|
5
|
+
has :id, String
|
|
6
|
+
|
|
7
|
+
# [BT-127]
|
|
8
|
+
has :note, String
|
|
9
|
+
|
|
10
|
+
# [BT-129]
|
|
11
|
+
has :invoiced_quantity, BigDecimal
|
|
12
|
+
|
|
13
|
+
# [BT-131] Total after discounts and charges, excluding vat
|
|
14
|
+
has :total_excluding_tax, Money
|
|
15
|
+
|
|
16
|
+
has :product, Product
|
|
17
|
+
|
|
18
|
+
# [BG-29] Price breakdown / details
|
|
19
|
+
has :price, Price
|
|
20
|
+
|
|
21
|
+
# UBL and CII deal with line-level tax differently.
|
|
22
|
+
# UBL has a `TaxTotal` at the line level with the amount under it,
|
|
23
|
+
# then under `Item` has the `ClassifiedTaxCategory`, so they're totally separate.
|
|
24
|
+
#
|
|
25
|
+
# CII by contrast uses one `ApplicableTradeTax` element for `TaxSubtotal` which
|
|
26
|
+
# combines the total tax and tax category into one.
|
|
27
|
+
# To make access easier, we'll store the amount and category separately and then
|
|
28
|
+
# have accessors for reading and writing as a `TaxSubtotal`.
|
|
29
|
+
has :total_tax, Money
|
|
30
|
+
|
|
31
|
+
def tax_subtotal
|
|
32
|
+
TaxSubtotal.new(tax_amount: total_tax, tax_category: product.tax_category).freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tax_subtotal=(ts)
|
|
36
|
+
if (total_tax = ts.tax_amount)
|
|
37
|
+
self.total_tax = total_tax
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if (tax_category = ts.tax_category)
|
|
41
|
+
product.tax_category = tax_category
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|