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,34 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
# [BG-22] Totals for a document
|
|
4
|
+
class MonetaryTotal < Base
|
|
5
|
+
# [BT-106] Sum of all line items' net amounts. No tax.
|
|
6
|
+
# (quantity * unit price) - line-level discounts
|
|
7
|
+
has :line_items_tax_exclusive, Money
|
|
8
|
+
|
|
9
|
+
# [BT-107] Document-level discounts (aka allowances) e.g. "10% off everything"
|
|
10
|
+
has :discounts, Money
|
|
11
|
+
|
|
12
|
+
# [BT-108] Document-level charges e.g. shipping, processing
|
|
13
|
+
has :charges, Money
|
|
14
|
+
|
|
15
|
+
# [BT-109] aka. net amount
|
|
16
|
+
has :tax_exclusive, Money
|
|
17
|
+
|
|
18
|
+
has :tax_breakdown, TaxBreakdown
|
|
19
|
+
|
|
20
|
+
# [BT-112]
|
|
21
|
+
has :tax_inclusive, Money
|
|
22
|
+
|
|
23
|
+
# [BT-113] Money already paid by the buyer *before* this. e.g. deposits
|
|
24
|
+
has :prepaid, Money
|
|
25
|
+
|
|
26
|
+
# [BT-114] Adjustment of a few cents to reconcile discrepancies caused by rounding.
|
|
27
|
+
has :rounding, Money
|
|
28
|
+
|
|
29
|
+
# [BT-115] The actual total amount remaining to be paid by the buyer
|
|
30
|
+
# tax_exclusive - prepaid + rounding
|
|
31
|
+
has :payable, Money
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class PaymentMeans < Base
|
|
4
|
+
# [BT-81]
|
|
5
|
+
has :type_code, Code::PaymentMeansType
|
|
6
|
+
|
|
7
|
+
# Multiple formats call these "Payer" and "Payee", but it's too easy to
|
|
8
|
+
# miss that single character difference which obviously makes a huge difference.
|
|
9
|
+
# Opt for the obviously distinctive naming.
|
|
10
|
+
has :sending_account, FinancialAccount
|
|
11
|
+
|
|
12
|
+
# [BG-18]
|
|
13
|
+
has :receiving_account, FinancialAccount
|
|
14
|
+
|
|
15
|
+
# [BT-83]
|
|
16
|
+
has :payment_id, String
|
|
17
|
+
|
|
18
|
+
has :instruction_id, String
|
|
19
|
+
|
|
20
|
+
# Zero or many lines of messages that were sent to the bank about the payment.
|
|
21
|
+
# This is the text that the receiving party will typically see in their account
|
|
22
|
+
# with a label like "message" or "details".
|
|
23
|
+
# Banking Circle for example allows 4 lines of "remittance information".
|
|
24
|
+
has_many :messages, String
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
# [BG-29]
|
|
4
|
+
# The price of the invoiced item, including the net price, base quantity,
|
|
5
|
+
# and any price discount from the gross price.
|
|
6
|
+
class Price < Base
|
|
7
|
+
# [BT-146] gross - discount
|
|
8
|
+
has :net, Money
|
|
9
|
+
|
|
10
|
+
# [BT-147] Discount applied to the gross to arrive at the net
|
|
11
|
+
has :discount, Money
|
|
12
|
+
|
|
13
|
+
# [BT-148] Price before any discounts
|
|
14
|
+
has :gross, Money
|
|
15
|
+
|
|
16
|
+
# [BT-149] The quantity to which the price applies
|
|
17
|
+
# (e.g. price per 100 units). Defaults to 1 if not specified.
|
|
18
|
+
has :base_quantity, BigDecimal
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
# [BG-31]
|
|
4
|
+
# Known in UBL as an `Item`, this is any product or item that can be traded.
|
|
5
|
+
# Includes things like the description, colour, product code, catalogue code, weights, etc.
|
|
6
|
+
class Product < Base
|
|
7
|
+
# [BT-153] Required
|
|
8
|
+
has :name, String
|
|
9
|
+
|
|
10
|
+
# [BT-154]
|
|
11
|
+
has :description, String
|
|
12
|
+
|
|
13
|
+
has :seller_assigned_id, String
|
|
14
|
+
|
|
15
|
+
has :tax_category, TaxCategory
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class RemittanceAdvice < Base
|
|
4
|
+
is_document
|
|
5
|
+
|
|
6
|
+
has :id, String
|
|
7
|
+
has :uuid, String
|
|
8
|
+
has :issue_date, Date
|
|
9
|
+
has :note, String
|
|
10
|
+
has :buyer, TradeParty
|
|
11
|
+
has :supplier, TradeParty
|
|
12
|
+
has :total_payment_amount, Money
|
|
13
|
+
has :invoice_period, Period
|
|
14
|
+
has :payment_means, PaymentMeans
|
|
15
|
+
|
|
16
|
+
has_many :lines, RemittanceAdviceLine
|
|
17
|
+
alias_method :line_items, :lines
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
class RemittanceAdviceLine < Base
|
|
4
|
+
has :id, String
|
|
5
|
+
has :uuid, String
|
|
6
|
+
has :note, String
|
|
7
|
+
|
|
8
|
+
has :document_reference, DocumentReference
|
|
9
|
+
|
|
10
|
+
# Original amount due.
|
|
11
|
+
# Not supported by CII
|
|
12
|
+
has :debit_amount, Money
|
|
13
|
+
|
|
14
|
+
# If there's any offset/credit to apply. Usually 0.
|
|
15
|
+
# Not supported by CII
|
|
16
|
+
has :credit_amount, Money
|
|
17
|
+
|
|
18
|
+
# Actual amount being paid.
|
|
19
|
+
has :balance_amount, Money
|
|
20
|
+
|
|
21
|
+
has :exchange_rate, ExchangeRate
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
# [BG-30]
|
|
4
|
+
class TaxCategory < Base
|
|
5
|
+
# [BT-118] VAT category code
|
|
6
|
+
# https://unece.org/fileadmin/DAM/trade/untdid/d16b/tred/tred5305.htm
|
|
7
|
+
has :code, Code::TaxCategory
|
|
8
|
+
|
|
9
|
+
# [BT-119]
|
|
10
|
+
# Stores as the percentage, not as a fraction of 1. e.g. 4.5% = 4.50
|
|
11
|
+
has :rate_percent, BigDecimal
|
|
12
|
+
|
|
13
|
+
# [BT-120]
|
|
14
|
+
has :exemption_reason, String
|
|
15
|
+
|
|
16
|
+
# [BT-121]
|
|
17
|
+
# https://docs.peppol.eu/poacc/billing/3.0/codelist/vatex/
|
|
18
|
+
# https://ec.europa.eu/digital-building-blocks/sites/spaces/DIGITAL/pages/467108957/Code+lists
|
|
19
|
+
# Could also be CWA 15577 codes
|
|
20
|
+
has :exemption_reason_code, String
|
|
21
|
+
|
|
22
|
+
# https://service.unece.org/trade/untdid/d00a/tred/tred5153.htm
|
|
23
|
+
has :tax_scheme, String
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module Model
|
|
3
|
+
end
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
require_relative "model/base"
|
|
7
|
+
|
|
8
|
+
# no dependencies
|
|
9
|
+
require_relative "model/contact"
|
|
10
|
+
require_relative "model/country"
|
|
11
|
+
require_relative "model/country_subdivision"
|
|
12
|
+
require_relative "model/exchange_rate"
|
|
13
|
+
require_relative "model/financial_institution"
|
|
14
|
+
require_relative "model/period"
|
|
15
|
+
require_relative "model/price"
|
|
16
|
+
require_relative "model/tax_category"
|
|
17
|
+
require_relative "model/tax_subtotal"
|
|
18
|
+
|
|
19
|
+
# depends on others. order may matter
|
|
20
|
+
require_relative "model/address"
|
|
21
|
+
require_relative "model/financial_account"
|
|
22
|
+
require_relative "model/trade_party"
|
|
23
|
+
require_relative "model/document_reference"
|
|
24
|
+
require_relative "model/tax_breakdown"
|
|
25
|
+
require_relative "model/monetary_total"
|
|
26
|
+
require_relative "model/payment_means"
|
|
27
|
+
require_relative "model/product"
|
|
28
|
+
|
|
29
|
+
require_relative "model/remittance_advice_line"
|
|
30
|
+
require_relative "model/remittance_advice"
|
|
31
|
+
|
|
32
|
+
require_relative "model/invoice_line"
|
|
33
|
+
require_relative "model/invoice"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module XML
|
|
3
|
+
# Provides an XML reading experience similar to the `Nokogiri::XML::Builder`
|
|
4
|
+
# experience where you can traverse a document by selecting nodes without
|
|
5
|
+
# having to constantly do `nil` checks and assign ephemeral variables.
|
|
6
|
+
class Reader
|
|
7
|
+
attr_reader :node, :namespaces, :format
|
|
8
|
+
|
|
9
|
+
# @param node [Nokogiri::XML::Node]
|
|
10
|
+
# XML::Document is also considered a node
|
|
11
|
+
# @param namespaces [Hash]
|
|
12
|
+
# Namespaces *with aliases* to use when reading the document.
|
|
13
|
+
# You must not rely on namespace aliases defined in the source document
|
|
14
|
+
# since they can easily be different to what you expect.
|
|
15
|
+
# Define a list of all namespaces you'll read from with aliases.
|
|
16
|
+
#
|
|
17
|
+
# { "xmlns:foo" => "urn:example:Foo", "xmlns:bar" => "urn:example:Bar" }
|
|
18
|
+
# @param format [Module]
|
|
19
|
+
# The format to use for parsing.
|
|
20
|
+
# Must repond to `.coder_for(type)` to get a coder for a destination type.
|
|
21
|
+
def initialize(node, namespaces = {}, format:)
|
|
22
|
+
@node = node
|
|
23
|
+
@namespaces = namespaces
|
|
24
|
+
@format = format
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# If a node exists at the given xpath, yield a reader for that node.
|
|
28
|
+
# This avoids the pattern nil-checking every single read operation.
|
|
29
|
+
#
|
|
30
|
+
# This pattern of swapping-out the current node makes reading XML similar
|
|
31
|
+
# to building a document with Nokogiri.
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# reader = Reader.new(Nokogiri::XML.parse(str))
|
|
35
|
+
# obj = Person.new
|
|
36
|
+
#
|
|
37
|
+
# reader.with_node("Person") do
|
|
38
|
+
# obj.id = reader.at_xpath("ID").text
|
|
39
|
+
# reader.with_node("Address") do
|
|
40
|
+
# obj.address.city = reader.at_xpath("City").text
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @param input [String | Nokogiri::XML::Node] XPath or Node
|
|
45
|
+
def with_node(input)
|
|
46
|
+
previous_node = node
|
|
47
|
+
|
|
48
|
+
next_node = case input
|
|
49
|
+
in ::String => xpath
|
|
50
|
+
at_xpath(xpath)
|
|
51
|
+
in Nokogiri::XML::Node => n
|
|
52
|
+
n
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if next_node.nil?
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@node = next_node
|
|
60
|
+
yield
|
|
61
|
+
ensure
|
|
62
|
+
@node = previous_node
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Given an xpath that matches 0 or more nodes, maps each element using
|
|
66
|
+
# the given block. The block will reference the current node.
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# r.with_nodes("LineItem") do
|
|
70
|
+
# Model::LineItem.new(description: r.at_xpath("Description").text)
|
|
71
|
+
# end
|
|
72
|
+
# => [Model::LineItem, Model::LineItem...]
|
|
73
|
+
def with_nodes(xpath, &block)
|
|
74
|
+
previous_node = node
|
|
75
|
+
|
|
76
|
+
self.xpath(xpath).map do |current|
|
|
77
|
+
@node = current
|
|
78
|
+
yield
|
|
79
|
+
end
|
|
80
|
+
ensure
|
|
81
|
+
@node = previous_node
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Given an xpath and a type coder class, if that node exists, parse it into
|
|
85
|
+
# an instance of the "real" type we want out of this node.
|
|
86
|
+
# Yields the parsed value so that callers can do something with it if
|
|
87
|
+
# the given node is present
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# r.parse("udt:IssueDate", :Date) { doc.issue_date = it }
|
|
91
|
+
#
|
|
92
|
+
# @param xpath [String]
|
|
93
|
+
# @param coder_ref [Class | Symbol]
|
|
94
|
+
# Any class that responds to `.parse(r)` where r is this `Reader`
|
|
95
|
+
# @param opts [Hash] Options to pass to the underlying parser
|
|
96
|
+
# @return [Object | nil]
|
|
97
|
+
def parse(xpath, coder_ref, **opts)
|
|
98
|
+
coder_class = self.format.coder_for(coder_ref)
|
|
99
|
+
node_exists = false
|
|
100
|
+
value = with_node(xpath) do
|
|
101
|
+
node_exists = true
|
|
102
|
+
coder_class.parse(self, **opts)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Allows parse callers to do something with the parsed value e.g.
|
|
106
|
+
# parse("//node", Address) { some_obj.address = it }
|
|
107
|
+
if block_given? && node_exists
|
|
108
|
+
yield value
|
|
109
|
+
else
|
|
110
|
+
value
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_list(xpath, coder_ref, **opts)
|
|
115
|
+
coder_class = self.format.coder_for(coder_ref)
|
|
116
|
+
|
|
117
|
+
values = with_nodes(xpath) do
|
|
118
|
+
coder_class.parse(self, **opts)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if block_given?
|
|
122
|
+
yield values
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
values
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def text(strip: true, nilify: true)
|
|
129
|
+
v = node.text
|
|
130
|
+
|
|
131
|
+
return if v.nil?
|
|
132
|
+
|
|
133
|
+
if strip
|
|
134
|
+
v = v.strip
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if nilify && v.empty?
|
|
138
|
+
v = nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
v
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def at_xpath(path)
|
|
145
|
+
node.at_xpath(path, namespaces)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def xpath(path)
|
|
149
|
+
node.xpath(path, namespaces)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def attribute(name)
|
|
153
|
+
attribute!(name)
|
|
154
|
+
rescue KeyError
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def attribute!(name)
|
|
159
|
+
node.attributes.fetch(name).text
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Tradedoc
|
|
2
|
+
module XML
|
|
3
|
+
# Wrapper for a `Nokogiri::XML::Builder` that creates an interface for common
|
|
4
|
+
# build actions, namely rendering "components" easily.
|
|
5
|
+
class Writer
|
|
6
|
+
InvalidNodeNameError = Class.new(Error)
|
|
7
|
+
|
|
8
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
9
|
+
# @param format [Module]
|
|
10
|
+
# Any module responding to `.coder_for` and returning a coder.
|
|
11
|
+
# In reality, should be one of the `Tradedoc.coders`
|
|
12
|
+
def initialize(xml, format:)
|
|
13
|
+
@xml = xml
|
|
14
|
+
@format = format
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Directly adds an element to the document without any processing.
|
|
18
|
+
# Normally with a Nokogiri builder you'd do `xml["ns"].ElemName(text)`
|
|
19
|
+
# but with this you'd do `writer.add("ns:ElemName", value)`
|
|
20
|
+
def add(name, ...)
|
|
21
|
+
if name.include?("/")
|
|
22
|
+
raise InvalidNodeNameError, "element name #{name} includes `/`"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if (c = name.scan(":").count) && c > 1
|
|
26
|
+
raise InvalidNodeNameError, "element name #{name} includes `:` multiple times"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
xml.public_send(name, ...)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Given an object, figure out how to dump/render it into the XML builder.
|
|
33
|
+
#
|
|
34
|
+
# If the given object/value is `nil`, nothing is rendered. No empty element.
|
|
35
|
+
# Otherwise, a coder is looked-up and used for the given type.
|
|
36
|
+
# Assumes that the de-namespaced class name has a matching local coder.
|
|
37
|
+
# e.g. `Tradedoc::Model::Country` rendered by `Tradedoc::Format::UBL::Coder::Country`
|
|
38
|
+
def render(obj, coder_ref = nil, **opts)
|
|
39
|
+
return if obj.nil?
|
|
40
|
+
|
|
41
|
+
coder_class = self.format.coder_for(coder_ref || obj.class)
|
|
42
|
+
coder_class.dump(self, obj, **opts)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_list(objects, coder_ref = nil, **opts)
|
|
46
|
+
return if objects.nil? || objects.none?
|
|
47
|
+
|
|
48
|
+
objects.each { render(it, coder_ref, **opts) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :xml, :format
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/tradedoc/xml.rb
ADDED
data/lib/tradedoc.rb
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
require "money"
|
|
6
|
+
|
|
7
|
+
module Tradedoc
|
|
8
|
+
Error = Class.new(StandardError)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# no dependencies
|
|
12
|
+
require_relative "tradedoc/version"
|
|
13
|
+
require_relative "tradedoc/xml"
|
|
14
|
+
require_relative "tradedoc/code"
|
|
15
|
+
|
|
16
|
+
# may have dependencies and order may matter
|
|
17
|
+
require_relative "tradedoc/format"
|
|
18
|
+
require_relative "tradedoc/model"
|
|
19
|
+
|
|
20
|
+
module Tradedoc
|
|
21
|
+
# List of formats supports for reading and writing documents
|
|
22
|
+
#
|
|
23
|
+
# @return [Set<Module>]
|
|
24
|
+
def self.formats
|
|
25
|
+
Set[
|
|
26
|
+
Format::APEH,
|
|
27
|
+
Format::CII,
|
|
28
|
+
Format::FacturaE,
|
|
29
|
+
Format::FatturaPA,
|
|
30
|
+
Format::ISDOC,
|
|
31
|
+
Format::KSEF,
|
|
32
|
+
Format::MyData,
|
|
33
|
+
Format::NAV,
|
|
34
|
+
Format::UBL,
|
|
35
|
+
Format::ZugferdV1
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# List of all possible file extensions that may be parsable by a format in this library
|
|
40
|
+
#
|
|
41
|
+
# @return [Set<String>]
|
|
42
|
+
def self.file_extensions
|
|
43
|
+
formats.reduce(Set[]) { |memo, fmt| memo + fmt.file_extensions }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Detect the format and document type of a given source
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<Module, Class>]
|
|
49
|
+
def self.detect(source)
|
|
50
|
+
xml = xml_from(source)
|
|
51
|
+
formats.each do |fmt|
|
|
52
|
+
if (dt = fmt.detect_document_coder(xml))
|
|
53
|
+
return [fmt, dt]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Given an XML document or XML string, detect the format, document type, and
|
|
61
|
+
# parse it into a model.
|
|
62
|
+
#
|
|
63
|
+
# Returns `nil` when the format or document type isn't supported.
|
|
64
|
+
#
|
|
65
|
+
# @param source [Nokogiri::XML::Document | String]
|
|
66
|
+
# @return [Object | nil]
|
|
67
|
+
def self.parse(source)
|
|
68
|
+
xml = xml_from(source)
|
|
69
|
+
if detect(xml) in [fmt, coder]
|
|
70
|
+
fmt.parse(xml, coder:)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get a format by a flexible source selector
|
|
75
|
+
#
|
|
76
|
+
# @param source [Symbol | String | Module]
|
|
77
|
+
# @return [Module]
|
|
78
|
+
def self.format_from(source)
|
|
79
|
+
case source
|
|
80
|
+
in Module => m if formats.include?(m)
|
|
81
|
+
m
|
|
82
|
+
in String | Symbol => name
|
|
83
|
+
name = name.to_s
|
|
84
|
+
formats.detect { it.label.casecmp?(name) || it.name.split("::").last.casecmp?(name) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param source [String | Nokogiri::XML::Document]
|
|
89
|
+
# @return [Nokogiri::XML::Document]
|
|
90
|
+
def self.xml_from(source)
|
|
91
|
+
case source
|
|
92
|
+
in Nokogiri::XML::Document => doc
|
|
93
|
+
doc
|
|
94
|
+
in String => str
|
|
95
|
+
Nokogiri::XML.parse(str)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/sig/tradedoc.rbs
ADDED