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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 985b4ac96323c6efebddb2908c4346a25444370b4b930f052d79056586553695
4
- data.tar.gz: 80bbf8c1df7fd5ce7cc7357b1bd197b6c75926eea07354ae8ba65976af60885a
3
+ metadata.gz: 837410c3386f0fb24e0c903378bad0e4d797e24867b2a3b15b7fdc6d23f1edb3
4
+ data.tar.gz: e5a4808ba918693dcd393212064c3e6590038aa61406fcd0fccf37e3a91794d0
5
5
  SHA512:
6
- metadata.gz: 28d68c38b89914a8a9d3edd29e073cc1d40790fdd47a8be96d698daaf55cffff9c9b5c146a9720884644a3cab473fc6c6d996c3601819be27961e93cd75335fe
7
- data.tar.gz: '03648cd41bd7b8759d7159c1d9d9ba2d649d1d93721d6b11557b6fa060ebbb045cecaffc71b62047fe903b6f4c8b5ff6114a650de72136d37a8210ca5b2a0583'
6
+ metadata.gz: 1f0fcf101222d3f841c8d1ea2601159509deafcc1c8344a04a70d4b63049d32dd78c180b4e9061f768f65b0675adfdc1393dbfbde9f40ef2efafdd37447e5478
7
+ data.tar.gz: 9bcf99a1530b14c1540551eab5534be30ffe6c6f1c1bf666f7875f2e7f442f0b929ebc08ff6351b477685520fe724dd7419292f2fce5f5a508a29b3b22cb162d
@@ -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 = {
@@ -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&.[]("currencyID")
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
- breakdown.subtotals = settlement_node.xpath(TAX_SUBTOTAL, NS).map do |sub|
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
  )
@@ -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, :customization_id, :profile_id, :note, :seller, :buyer,
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, :item, :price
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
@@ -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 = {
@@ -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
  )
@@ -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.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