zugpferd 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/lib/zugpferd/cii/mapping.rb +9 -0
- data/lib/zugpferd/cii/reader.rb +26 -7
- data/lib/zugpferd/cii/writer.rb +24 -0
- data/lib/zugpferd/model/billing_document.rb +3 -1
- data/lib/zugpferd/model/line_item.rb +4 -1
- data/lib/zugpferd/ubl/mapping.rb +10 -0
- data/lib/zugpferd/ubl/reader.rb +4 -0
- data/lib/zugpferd/ubl/writer.rb +15 -0
- data/lib/zugpferd.rb +4 -4
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 837410c3386f0fb24e0c903378bad0e4d797e24867b2a3b15b7fdc6d23f1edb3
|
|
4
|
+
data.tar.gz: e5a4808ba918693dcd393212064c3e6590038aa61406fcd0fccf37e3a91794d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f0fcf101222d3f841c8d1ea2601159509deafcc1c8344a04a70d4b63049d32dd78c180b4e9061f768f65b0675adfdc1393dbfbde9f40ef2efafdd37447e5478
|
|
7
|
+
data.tar.gz: 9bcf99a1530b14c1540551eab5534be30ffe6c6f1c1bf666f7875f2e7f442f0b929ebc08ff6351b477685520fe724dd7419292f2fce5f5a508a29b3b22cb162d
|
data/lib/zugpferd/cii/mapping.rb
CHANGED
|
@@ -41,6 +41,7 @@ module Zugpferd
|
|
|
41
41
|
INVOICE_SETTLEMENT = {
|
|
42
42
|
currency_code: "#{SETTLEMENT}/ram:InvoiceCurrencyCode",
|
|
43
43
|
buyer_reference: "#{AGREEMENT}/ram:BuyerReference",
|
|
44
|
+
purchase_order_reference: "#{AGREEMENT}/ram:BuyerOrderReferencedDocument/ram:IssuerAssignedID",
|
|
44
45
|
}.freeze
|
|
45
46
|
|
|
46
47
|
# Seller (BG-4)
|
|
@@ -93,6 +94,7 @@ module Zugpferd
|
|
|
93
94
|
|
|
94
95
|
# TaxTotal (BG-23)
|
|
95
96
|
TAX_SUBTOTAL = "ram:ApplicableTradeTax"
|
|
97
|
+
TAX_FALLBACK_CURRENCY = "ram:InvoiceCurrencyCode"
|
|
96
98
|
TAX = {
|
|
97
99
|
taxable_amount: "ram:BasisAmount",
|
|
98
100
|
tax_amount: "ram:CalculatedAmount",
|
|
@@ -139,6 +141,13 @@ module Zugpferd
|
|
|
139
141
|
line_extension_amount: "ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount",
|
|
140
142
|
}.freeze
|
|
141
143
|
|
|
144
|
+
# Invoice line period (BG-26)
|
|
145
|
+
LINE_PERIOD = "ram:SpecifiedLineTradeSettlement/ram:BillingSpecifiedPeriod"
|
|
146
|
+
LINE_PERIOD_FIELDS = {
|
|
147
|
+
start_date: "ram:StartDateTime/udt:DateTimeString",
|
|
148
|
+
end_date: "ram:EndDateTime/udt:DateTimeString",
|
|
149
|
+
}.freeze
|
|
150
|
+
|
|
142
151
|
# Item (BG-31)
|
|
143
152
|
ITEM = "ram:SpecifiedTradeProduct"
|
|
144
153
|
ITEM_FIELDS = {
|
data/lib/zugpferd/cii/reader.rb
CHANGED
|
@@ -12,6 +12,9 @@ module Zugpferd
|
|
|
12
12
|
class Reader
|
|
13
13
|
include Mapping
|
|
14
14
|
|
|
15
|
+
# Raised when BT-110 is missing and the VAT breakdown (BG-23) does not sum to 0.
|
|
16
|
+
class MissingTaxTotalError < Zugpferd::Error; end
|
|
17
|
+
|
|
15
18
|
TYPE_CODE_MAP = {
|
|
16
19
|
"381" => Model::CreditNote,
|
|
17
20
|
"384" => Model::CorrectedInvoice,
|
|
@@ -25,6 +28,7 @@ module Zugpferd
|
|
|
25
28
|
# @param xml_string [String] valid CII D16B XML
|
|
26
29
|
# @return [Model::BillingDocument]
|
|
27
30
|
# @raise [Nokogiri::XML::SyntaxError] if the XML is malformed
|
|
31
|
+
# @raise [MissingTaxTotalError] if BT-110 is missing and the VAT breakdown does not sum to 0
|
|
28
32
|
def read(xml_string)
|
|
29
33
|
doc = Nokogiri::XML(xml_string) { |config| config.strict }
|
|
30
34
|
root = doc.root
|
|
@@ -50,6 +54,7 @@ module Zugpferd
|
|
|
50
54
|
type_code: text(root, INVOICE[:type_code]),
|
|
51
55
|
currency_code: text(root, INVOICE_SETTLEMENT[:currency_code]),
|
|
52
56
|
buyer_reference: text(root, INVOICE_SETTLEMENT[:buyer_reference]),
|
|
57
|
+
purchase_order_reference: text(root, INVOICE_SETTLEMENT[:purchase_order_reference]),
|
|
53
58
|
customization_id: text(root, INVOICE[:customization_id]),
|
|
54
59
|
profile_id: text(root, INVOICE[:profile_id]),
|
|
55
60
|
note: text(root, INVOICE[:note]),
|
|
@@ -129,14 +134,9 @@ module Zugpferd
|
|
|
129
134
|
|
|
130
135
|
totals_node = settlement_node.at_xpath(MONETARY_TOTAL, NS)
|
|
131
136
|
tax_total_node = totals_node&.at_xpath(TOTALS[:tax_total_amount], NS)
|
|
132
|
-
currency = tax_total_node
|
|
133
|
-
|
|
134
|
-
breakdown = Model::TaxBreakdown.new(
|
|
135
|
-
tax_amount: tax_total_node&.text,
|
|
136
|
-
currency_code: currency,
|
|
137
|
-
)
|
|
137
|
+
currency = tax_total_node ? tax_total_node["currencyID"] : text(settlement_node, TAX_FALLBACK_CURRENCY)
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
subtotals = settlement_node.xpath(TAX_SUBTOTAL, NS).map do |sub|
|
|
140
140
|
Model::TaxSubtotal.new(
|
|
141
141
|
taxable_amount: text(sub, TAX[:taxable_amount]),
|
|
142
142
|
tax_amount: text(sub, TAX[:tax_amount]),
|
|
@@ -148,9 +148,25 @@ module Zugpferd
|
|
|
148
148
|
)
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
+
breakdown = Model::TaxBreakdown.new(
|
|
152
|
+
tax_amount: tax_total_node ? tax_total_node.text : zero_tax_total(subtotals),
|
|
153
|
+
currency_code: currency,
|
|
154
|
+
)
|
|
155
|
+
breakdown.subtotals = subtotals
|
|
151
156
|
breakdown
|
|
152
157
|
end
|
|
153
158
|
|
|
159
|
+
# BR-CO-15 lets CII omit BT-110 on invoices without VAT. A breakdown
|
|
160
|
+
# that shows VAT contradicts that, even where the Schematron passes it.
|
|
161
|
+
def zero_tax_total(subtotals)
|
|
162
|
+
sum = subtotals.sum(BigDecimal("0"), &:tax_amount)
|
|
163
|
+
return sum if subtotals.any? && sum.zero?
|
|
164
|
+
|
|
165
|
+
raise MissingTaxTotalError,
|
|
166
|
+
"Invoice total VAT amount (BT-110) is missing and the VAT breakdown (BG-23) " \
|
|
167
|
+
"#{subtotals.empty? ? "is empty" : "sums to #{sum.to_s("F")}"}"
|
|
168
|
+
end
|
|
169
|
+
|
|
154
170
|
def build_monetary_totals(node)
|
|
155
171
|
return nil unless node
|
|
156
172
|
|
|
@@ -170,6 +186,7 @@ module Zugpferd
|
|
|
170
186
|
item_node = node.at_xpath(ITEM, NS)
|
|
171
187
|
price_node = node.at_xpath(PRICE, NS)
|
|
172
188
|
tax_node = node.at_xpath(ITEM_TAX, NS)
|
|
189
|
+
period_node = node.at_xpath(LINE_PERIOD, NS)
|
|
173
190
|
|
|
174
191
|
Model::LineItem.new(
|
|
175
192
|
id: text(node, LINE[:id]),
|
|
@@ -177,6 +194,8 @@ module Zugpferd
|
|
|
177
194
|
unit_code: node.at_xpath(LINE[:unit_code], NS)&.text,
|
|
178
195
|
line_extension_amount: text(node, LINE[:line_extension_amount]),
|
|
179
196
|
note: text(node, LINE[:note]),
|
|
197
|
+
period_start_date: parse_cii_date(period_node ? text(period_node, LINE_PERIOD_FIELDS[:start_date]) : nil),
|
|
198
|
+
period_end_date: parse_cii_date(period_node ? text(period_node, LINE_PERIOD_FIELDS[:end_date]) : nil),
|
|
180
199
|
item: build_item(item_node, tax_node),
|
|
181
200
|
price: build_price(price_node),
|
|
182
201
|
)
|
data/lib/zugpferd/cii/writer.rb
CHANGED
|
@@ -76,6 +76,13 @@ module Zugpferd
|
|
|
76
76
|
xml["ram"].BuyerReference doc.buyer_reference if doc.buyer_reference
|
|
77
77
|
build_party(xml, "SellerTradeParty", doc.seller) if doc.seller
|
|
78
78
|
build_party(xml, "BuyerTradeParty", doc.buyer) if doc.buyer
|
|
79
|
+
build_buyer_order_reference(xml, doc) if doc.purchase_order_reference
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_buyer_order_reference(xml, doc)
|
|
84
|
+
xml["ram"].BuyerOrderReferencedDocument do
|
|
85
|
+
xml["ram"].IssuerAssignedID doc.purchase_order_reference
|
|
79
86
|
end
|
|
80
87
|
end
|
|
81
88
|
|
|
@@ -314,6 +321,8 @@ module Zugpferd
|
|
|
314
321
|
end
|
|
315
322
|
end
|
|
316
323
|
|
|
324
|
+
build_line_period(xml, line) if line.period_start_date || line.period_end_date
|
|
325
|
+
|
|
317
326
|
xml["ram"].SpecifiedTradeSettlementLineMonetarySummation do
|
|
318
327
|
xml["ram"].LineTotalAmount format_decimal(line.line_extension_amount)
|
|
319
328
|
end
|
|
@@ -321,6 +330,21 @@ module Zugpferd
|
|
|
321
330
|
end
|
|
322
331
|
end
|
|
323
332
|
|
|
333
|
+
def build_line_period(xml, line)
|
|
334
|
+
xml["ram"].BillingSpecifiedPeriod do
|
|
335
|
+
if line.period_start_date
|
|
336
|
+
xml["ram"].StartDateTime do
|
|
337
|
+
xml["udt"].DateTimeString(format_cii_date(line.period_start_date), format: "102")
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
if line.period_end_date
|
|
341
|
+
xml["ram"].EndDateTime do
|
|
342
|
+
xml["udt"].DateTimeString(format_cii_date(line.period_end_date), format: "102")
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
324
348
|
def build_item(xml, item)
|
|
325
349
|
xml["ram"].SpecifiedTradeProduct do
|
|
326
350
|
xml["ram"].SellerAssignedID item.sellers_identifier if item.sellers_identifier
|
|
@@ -22,6 +22,7 @@ module Zugpferd
|
|
|
22
22
|
# @return [Date, nil] BT-73 Invoicing period start date
|
|
23
23
|
# @return [Date, nil] BT-74 Invoicing period end date
|
|
24
24
|
# @return [String, nil] BT-10 Buyer reference
|
|
25
|
+
# @return [String, nil] BT-13 Purchase order reference
|
|
25
26
|
# @return [String, nil] BT-24 Specification identifier
|
|
26
27
|
# @return [String, nil] BT-23 Business process type
|
|
27
28
|
# @return [String, nil] BT-22 Invoice note
|
|
@@ -35,7 +36,8 @@ module Zugpferd
|
|
|
35
36
|
attr_accessor :number, :issue_date, :due_date, :type_code,
|
|
36
37
|
:currency_code, :delivery_date,
|
|
37
38
|
:invoicing_period_start_date, :invoicing_period_end_date,
|
|
38
|
-
:buyer_reference, :
|
|
39
|
+
:buyer_reference, :purchase_order_reference,
|
|
40
|
+
:customization_id, :profile_id, :note, :seller, :buyer,
|
|
39
41
|
:line_items, :tax_breakdown, :monetary_totals,
|
|
40
42
|
:payment_instructions, :allowance_charges
|
|
41
43
|
|
|
@@ -9,10 +9,13 @@ module Zugpferd
|
|
|
9
9
|
# @return [String] BT-130 Unit of measure code
|
|
10
10
|
# @return [BigDecimal] BT-131 Line extension amount
|
|
11
11
|
# @return [String, nil] BT-127 Invoice line note
|
|
12
|
+
# @return [Date, nil] BT-134 Invoice line period start date
|
|
13
|
+
# @return [Date, nil] BT-135 Invoice line period end date
|
|
12
14
|
# @return [Item, nil] BG-31 Item information
|
|
13
15
|
# @return [Price, nil] BG-29 Price details
|
|
14
16
|
attr_accessor :id, :invoiced_quantity, :unit_code,
|
|
15
|
-
:line_extension_amount, :note, :
|
|
17
|
+
:line_extension_amount, :note, :period_start_date,
|
|
18
|
+
:period_end_date, :item, :price
|
|
16
19
|
|
|
17
20
|
# @param id [String] BT-126 Line identifier
|
|
18
21
|
# @param invoiced_quantity [String, BigDecimal] BT-129 Quantity
|
data/lib/zugpferd/ubl/mapping.rb
CHANGED
|
@@ -37,6 +37,9 @@ module Zugpferd
|
|
|
37
37
|
end_date: "cbc:EndDate",
|
|
38
38
|
}.freeze
|
|
39
39
|
|
|
40
|
+
# Purchase order reference (BT-13)
|
|
41
|
+
ORDER_REFERENCE = "cac:OrderReference/cbc:ID"
|
|
42
|
+
|
|
40
43
|
# Seller (BG-4)
|
|
41
44
|
SELLER = "cac:AccountingSupplierParty/cac:Party"
|
|
42
45
|
# Buyer (BG-7)
|
|
@@ -132,6 +135,13 @@ module Zugpferd
|
|
|
132
135
|
note: "cbc:Note",
|
|
133
136
|
}.freeze
|
|
134
137
|
|
|
138
|
+
# Invoice line period (BG-26)
|
|
139
|
+
LINE_PERIOD = "cac:InvoicePeriod"
|
|
140
|
+
LINE_PERIOD_FIELDS = {
|
|
141
|
+
start_date: "cbc:StartDate",
|
|
142
|
+
end_date: "cbc:EndDate",
|
|
143
|
+
}.freeze
|
|
144
|
+
|
|
135
145
|
# Item (BG-31)
|
|
136
146
|
ITEM = "cac:Item"
|
|
137
147
|
ITEM_FIELDS = {
|
data/lib/zugpferd/ubl/reader.rb
CHANGED
|
@@ -46,6 +46,7 @@ module Zugpferd
|
|
|
46
46
|
type_code: text(root, type_code_element),
|
|
47
47
|
currency_code: text(root, INVOICE[:currency_code]),
|
|
48
48
|
buyer_reference: text(root, INVOICE[:buyer_reference]),
|
|
49
|
+
purchase_order_reference: text(root, ORDER_REFERENCE),
|
|
49
50
|
customization_id: text(root, INVOICE[:customization_id]),
|
|
50
51
|
profile_id: text(root, INVOICE[:profile_id]),
|
|
51
52
|
note: text(root, INVOICE[:note]),
|
|
@@ -168,6 +169,7 @@ module Zugpferd
|
|
|
168
169
|
def build_line_item(node)
|
|
169
170
|
item_node = node.at_xpath(ITEM, @ns)
|
|
170
171
|
price_node = node.at_xpath(PRICE, @ns)
|
|
172
|
+
period_node = node.at_xpath(LINE_PERIOD, @ns)
|
|
171
173
|
|
|
172
174
|
quantity_element = @credit_note ? "cbc:CreditedQuantity" : LINE[:invoiced_quantity]
|
|
173
175
|
unit_code_element = @credit_note ? "cbc:CreditedQuantity/@unitCode" : LINE[:unit_code]
|
|
@@ -178,6 +180,8 @@ module Zugpferd
|
|
|
178
180
|
unit_code: node.at_xpath(unit_code_element, @ns)&.text,
|
|
179
181
|
line_extension_amount: text(node, LINE[:line_extension_amount]),
|
|
180
182
|
note: text(node, LINE[:note]),
|
|
183
|
+
period_start_date: period_node ? parse_date(text(period_node, LINE_PERIOD_FIELDS[:start_date])) : nil,
|
|
184
|
+
period_end_date: period_node ? parse_date(text(period_node, LINE_PERIOD_FIELDS[:end_date])) : nil,
|
|
181
185
|
item: build_item(item_node),
|
|
182
186
|
price: build_price(price_node),
|
|
183
187
|
)
|
data/lib/zugpferd/ubl/writer.rb
CHANGED
|
@@ -51,6 +51,7 @@ module Zugpferd
|
|
|
51
51
|
xml["cbc"].DocumentCurrencyCode doc.currency_code
|
|
52
52
|
xml["cbc"].BuyerReference doc.buyer_reference if doc.buyer_reference
|
|
53
53
|
build_invoice_period(xml, doc) if doc.invoicing_period_start_date || doc.invoicing_period_end_date
|
|
54
|
+
build_order_reference(xml, doc) if doc.purchase_order_reference
|
|
54
55
|
|
|
55
56
|
build_supplier(xml, doc.seller, doc.payment_instructions) if doc.seller
|
|
56
57
|
build_customer(xml, doc.buyer) if doc.buyer
|
|
@@ -149,6 +150,12 @@ module Zugpferd
|
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
|
|
153
|
+
def build_order_reference(xml, doc)
|
|
154
|
+
xml["cac"].OrderReference do
|
|
155
|
+
xml["cbc"].ID doc.purchase_order_reference
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
152
159
|
def build_delivery(xml, doc)
|
|
153
160
|
xml["cac"].Delivery do
|
|
154
161
|
xml["cbc"].ActualDeliveryDate doc.delivery_date.to_s
|
|
@@ -274,11 +281,19 @@ module Zugpferd
|
|
|
274
281
|
unitCode: line.unit_code)
|
|
275
282
|
xml["cbc"].LineExtensionAmount(format_decimal(line.line_extension_amount),
|
|
276
283
|
currencyID: currency_code)
|
|
284
|
+
build_line_period(xml, line) if line.period_start_date || line.period_end_date
|
|
277
285
|
build_item(xml, line.item) if line.item
|
|
278
286
|
build_price(xml, line.price, currency_code) if line.price
|
|
279
287
|
end
|
|
280
288
|
end
|
|
281
289
|
|
|
290
|
+
def build_line_period(xml, line)
|
|
291
|
+
xml["cac"].InvoicePeriod do
|
|
292
|
+
xml["cbc"].StartDate line.period_start_date.to_s if line.period_start_date
|
|
293
|
+
xml["cbc"].EndDate line.period_end_date.to_s if line.period_end_date
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
282
297
|
def build_item(xml, item)
|
|
283
298
|
xml["cac"].Item do
|
|
284
299
|
xml["cbc"].Description item.description if item.description
|
data/lib/zugpferd.rb
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
module Zugpferd
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
end
|
|
4
|
+
|
|
1
5
|
require_relative "zugpferd/model/billing_document"
|
|
2
6
|
require_relative "zugpferd/model/invoice"
|
|
3
7
|
require_relative "zugpferd/model/credit_note"
|
|
@@ -20,7 +24,3 @@ require_relative "zugpferd/ubl/reader"
|
|
|
20
24
|
require_relative "zugpferd/ubl/writer"
|
|
21
25
|
require_relative "zugpferd/cii/reader"
|
|
22
26
|
require_relative "zugpferd/cii/writer"
|
|
23
|
-
|
|
24
|
-
module Zugpferd
|
|
25
|
-
class Error < StandardError; end
|
|
26
|
-
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zugpferd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Zeitler
|
|
@@ -27,14 +27,14 @@ dependencies:
|
|
|
27
27
|
name: bigdecimal
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '3.1'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '3.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|