secretariat 3.6.0 → 3.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24767d1bf53e67440e5b012f83b19e45119e937fde0085d8197ff21dcf0c65e0
4
- data.tar.gz: c0cc0b22c3f8fa4612e08a6bab5af89f0fefd30a06c899976264305991d08846
3
+ metadata.gz: f6d311cbeb3472a1af82827bb21d95e1f789fb21fe486995e1be364213761e44
4
+ data.tar.gz: e52061bd2915e2ef348c33c714a1f6ae74ee5b2ab3961c5adce7ea0ceac1a804
5
5
  SHA512:
6
- metadata.gz: acaff7a169b01482c53491609cd0b91add3d528b74e84659369d6283463eae3787020d6a91153775778decf393353f2c8a88935e046eb8a3ab1bb151fd5dd31b
7
- data.tar.gz: c213647657402309c566461b15d11dc1a1cc5d72e8d158ab79fc0f51b2bde4eb6245c86e68cd976bc707f372f9d8ce803ef076453a7cfd222ae95f60e7257fac
6
+ metadata.gz: 7ebfa196f6d6c1710bbdb6768526321e9ca1a729d9d4ff8117914ab7d84bf1b919eea3c212cc3dfcc4574aa2f9272c415ec319cf941912ffccd2ddfef03f08bd
7
+ data.tar.gz: 6cb203137f2af48c89635b4bea7860b0aafb02feaebbecf649f8aabf0c43ac56afa3a3932439fa0d516f790d96dae80c800f46a31eac556afbbdc421a6ff7215
@@ -53,6 +53,10 @@ module Secretariat
53
53
  :DEBITADVICE => "31",
54
54
  :CREDITCARD => "48",
55
55
  :DEBIT => "49",
56
+ :CREDITTRANSFER => "54",
57
+ :DIRECTDEBIT => "55",
58
+ :SEPACREDITTRANSFER => "58",
59
+ :SEPADIRECTDEBIT => "59",
56
60
  :COMPENSATION => "97"
57
61
  }
58
62
 
@@ -69,6 +73,7 @@ module Secretariat
69
73
  TAX_CALCULATION_METHODS = %i[HORIZONTAL VERTICAL NONE UNKNOWN].freeze
70
74
 
71
75
  UNIT_CODES = {
76
+ :YEAR => "ANN",
72
77
  :PIECE => "C62",
73
78
  :DAY => "DAY",
74
79
  :HECTARE => "HAR",
@@ -17,13 +17,16 @@ limitations under the License.
17
17
  require 'bigdecimal'
18
18
 
19
19
  module Secretariat
20
+ using ObjectExtensions
21
+
20
22
  Invoice = Struct.new("Invoice",
21
23
  :id,
22
24
  :issue_date,
23
25
  :service_period_start,
24
26
  :service_period_end,
25
- :seller,
26
- :buyer,
27
+ :seller, # TradeParty
28
+ :buyer, # TradeParty
29
+ :ship_to, # TradeParty or nil (buyer) or false (no ShipTo)
27
30
  :buyer_reference,
28
31
  :line_items,
29
32
  :currency_code,
@@ -33,6 +36,8 @@ module Secretariat
33
36
  :payment_terms_text,
34
37
  :payment_due_date,
35
38
  :payment_iban,
39
+ :payment_bic,
40
+ :payment_payee_account_name,
36
41
  :tax_category,
37
42
  :tax_percent,
38
43
  :tax_amount,
@@ -44,6 +49,9 @@ module Secretariat
44
49
  :tax_calculation_method,
45
50
  :notes,
46
51
  :attachments,
52
+ :direct_debit_mandate_reference_id, # BT-89
53
+ :direct_debit_creditor_id, # BT-90
54
+ :direct_debit_iban, # BT-91
47
55
  keyword_init: true
48
56
  ) do
49
57
 
@@ -53,8 +61,16 @@ module Secretariat
53
61
  @errors
54
62
  end
55
63
 
56
- def tax_reason_text
57
- tax_reason || TAX_EXEMPTION_REASONS[tax_category]
64
+ def tax_reason_text(tax)
65
+ tax_reason || TAX_EXEMPTION_REASONS[tax.tax_category || tax_category]
66
+ end
67
+
68
+ # ship_to: nil => use buyer (backwards compatibility)
69
+ # ship_to: false => ignore
70
+ def ship_to_or_buyer
71
+ return buyer if ship_to.nil?
72
+
73
+ ship_to
58
74
  end
59
75
 
60
76
  def tax_category_code(tax, version: 2)
@@ -78,11 +94,11 @@ module Secretariat
78
94
  line_items.each do |line_item|
79
95
  if line_item.tax_percent.nil?
80
96
  taxes['0'] = Tax.new(tax_percent: BigDecimal(0), tax_category: line_item.tax_category, tax_amount: BigDecimal(0)) if taxes['0'].nil?
81
- taxes['0'].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
97
+ taxes['0'].base_amount += BigDecimal(line_item.net_amount) * line_item.billed_quantity
82
98
  else
83
99
  taxes[line_item.tax_percent] = Tax.new(tax_percent: BigDecimal(line_item.tax_percent), tax_category: line_item.tax_category) if taxes[line_item.tax_percent].nil?
84
100
  taxes[line_item.tax_percent].tax_amount += BigDecimal(line_item.tax_amount)
85
- taxes[line_item.tax_percent].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
101
+ taxes[line_item.tax_percent].base_amount += BigDecimal(line_item.net_amount) * line_item.billed_quantity
86
102
  end
87
103
  end
88
104
 
@@ -136,7 +152,7 @@ module Secretariat
136
152
  return false
137
153
  end
138
154
  line_item_sum = line_items.inject(BigDecimal(0)) do |m, item|
139
- m + BigDecimal(item.quantity.negative? ? -item.charge_amount : item.charge_amount)
155
+ m + BigDecimal(item.billed_quantity.negative? ? -item.charge_amount : item.charge_amount)
140
156
  end
141
157
  if line_item_sum != basis
142
158
  @errors << "Line items do not add up to basis amount #{line_item_sum} / #{basis}"
@@ -241,9 +257,9 @@ module Secretariat
241
257
  delivery = by_version(version, 'ApplicableSupplyChainTradeDelivery', 'ApplicableHeaderTradeDelivery')
242
258
 
243
259
  xml['ram'].send(delivery) do
244
- if version == 2
260
+ if version == 2 && ship_to_or_buyer
245
261
  xml['ram'].ShipToTradeParty do
246
- buyer.to_xml(xml, exclude_tax: true, version: version)
262
+ ship_to_or_buyer.to_xml(xml, exclude_tax: true, version: version)
247
263
  end
248
264
  end
249
265
  xml['ram'].ActualDeliverySupplyChainEvent do
@@ -256,16 +272,30 @@ module Secretariat
256
272
  end
257
273
  trade_settlement = by_version(version, 'ApplicableSupplyChainTradeSettlement', 'ApplicableHeaderTradeSettlement')
258
274
  xml['ram'].send(trade_settlement) do
259
- if payment_reference && payment_reference != ''
275
+ if direct_debit_creditor_id
276
+ xml['ram'].CreditorReferenceID direct_debit_creditor_id # BT-90
277
+ end
278
+ if payment_reference.present?
260
279
  xml['ram'].PaymentReference payment_reference
261
280
  end
262
281
  xml['ram'].InvoiceCurrencyCode currency_code
263
282
  xml['ram'].SpecifiedTradeSettlementPaymentMeans do
264
283
  xml['ram'].TypeCode payment_code
265
284
  xml['ram'].Information payment_text
266
- if payment_iban
285
+ if payment_iban || payment_payee_account_name
267
286
  xml['ram'].PayeePartyCreditorFinancialAccount do
268
- xml['ram'].IBANID payment_iban
287
+ xml['ram'].IBANID payment_iban if payment_iban
288
+ xml['ram'].AccountName payment_payee_account_name if payment_payee_account_name
289
+ end
290
+ end
291
+ if payment_bic
292
+ xml['ram'].PayeeSpecifiedCreditorFinancialInstitution do
293
+ xml['ram'].BICID payment_bic
294
+ end
295
+ end
296
+ if direct_debit_iban
297
+ xml['ram'].PayerPartyDebtorFinancialAccount do
298
+ xml['ram'].IBANID direct_debit_iban
269
299
  end
270
300
  end
271
301
  end
@@ -273,8 +303,8 @@ module Secretariat
273
303
  xml['ram'].ApplicableTradeTax do
274
304
  Helpers.currency_element(xml, 'ram', 'CalculatedAmount', tax.tax_amount, currency_code, add_currency: version == 1)
275
305
  xml['ram'].TypeCode 'VAT'
276
- if tax_reason_text && tax_reason_text != ''
277
- xml['ram'].ExemptionReason tax_reason_text
306
+ if tax_reason_text(tax).present?
307
+ xml['ram'].ExemptionReason tax_reason_text(tax)
278
308
  end
279
309
  Helpers.currency_element(xml, 'ram', 'BasisAmount', tax.base_amount, currency_code, add_currency: version == 1)
280
310
  xml['ram'].CategoryCode tax_category_code(tax, version: version)
@@ -301,6 +331,9 @@ module Secretariat
301
331
  Helpers.date_element(xml, payment_due_date)
302
332
  end
303
333
  end
334
+ if direct_debit_mandate_reference_id
335
+ xml['ram'].DirectDebitMandateID direct_debit_mandate_reference_id
336
+ end
304
337
  end
305
338
 
306
339
  monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementHeaderMonetarySummation')
@@ -20,7 +20,7 @@ module Secretariat
20
20
 
21
21
  LineItem = Struct.new('LineItem',
22
22
  :name,
23
- :quantity,
23
+ :billed_quantity,
24
24
  :unit,
25
25
  :gross_amount,
26
26
  :net_amount,
@@ -32,11 +32,38 @@ module Secretariat
32
32
  :charge_amount,
33
33
  :origin_country_code,
34
34
  :currency_code,
35
+ :service_period_start, # if start present start & end are required
36
+ :service_period_end, # end has to be on or after start (secretariat does not validate this)
37
+ :basis_quantity,
35
38
  keyword_init: true
36
39
  ) do
37
40
 
38
41
  include Versioner
39
42
 
43
+ def initialize(**kwargs)
44
+ if kwargs.key?(:quantity) && !kwargs.key?(:billed_quantity)
45
+ kwargs[:billed_quantity] = kwargs.delete(:quantity)
46
+ end
47
+ super(**kwargs)
48
+ end
49
+
50
+ def quantity
51
+ warn_once_quantity
52
+ billed_quantity
53
+ end
54
+
55
+ def quantity=(val)
56
+ warn_once_quantity
57
+ self.billed_quantity = val
58
+ end
59
+
60
+ def warn_once_quantity
61
+ unless @__warned_quantity
62
+ Kernel.warn("[secretariat] LineItem#quantity is deprecated; use #billed_quantity")
63
+ @__warned_quantity = true
64
+ end
65
+ end
66
+
40
67
  def errors
41
68
  @errors
42
69
  end
@@ -47,7 +74,7 @@ module Secretariat
47
74
  gross_price = BigDecimal(gross_amount)
48
75
  charge_price = BigDecimal(charge_amount)
49
76
  tax = BigDecimal(tax_amount)
50
- unit_price = net_price * BigDecimal(quantity.abs)
77
+ unit_price = net_price * BigDecimal(billed_quantity.abs)
51
78
 
52
79
  if charge_price != unit_price
53
80
  @errors << "charge price and gross price times quantity deviate: #{charge_price} / #{unit_price}"
@@ -65,7 +92,7 @@ module Secretariat
65
92
  self.tax_percent ||= BigDecimal(0)
66
93
  calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
67
94
  calculated_tax = calculated_tax.round(2)
68
- calculated_tax = -calculated_tax if quantity.negative?
95
+ calculated_tax = -calculated_tax if billed_quantity.negative?
69
96
  if calculated_tax != tax
70
97
  @errors << "Tax and calculated tax deviate: #{tax} / #{calculated_tax}"
71
98
  return false
@@ -78,6 +105,13 @@ module Secretariat
78
105
  UNIT_CODES[unit] || 'C62'
79
106
  end
80
107
 
108
+ # If not provided, BasisQuantity should be 1.0 (price per 1 unit)
109
+ def effective_basis_quantity
110
+ q = basis_quantity.nil? ? BigDecimal("1.0") : BigDecimal(basis_quantity.to_s)
111
+ raise ArgumentError, "basis_quantity must be > 0" if q <= 0
112
+ q
113
+ end
114
+
81
115
  def tax_category_code(version: 2)
82
116
  if version == 1
83
117
  return TAX_CATEGORY_CODES_1[tax_category] || 'S'
@@ -103,7 +137,7 @@ module Secretariat
103
137
  if net_price&.negative?
104
138
  # Zugferd doesn't allow negative amounts at the item level.
105
139
  # Instead, a negative quantity is used.
106
- self.quantity = -quantity
140
+ self.billed_quantity = -billed_quantity
107
141
  self.gross_amount = gross_price&.abs
108
142
  self.net_amount = net_price&.abs
109
143
  self.charge_amount = charge_price&.abs
@@ -132,7 +166,7 @@ module Secretariat
132
166
  Helpers.currency_element(xml, 'ram', 'ChargeAmount', gross_amount, currency_code, add_currency: version == 1, digits: 4)
133
167
  if version == 2 && discount_amount
134
168
  xml['ram'].BasisQuantity(unitCode: unit_code) do
135
- xml.text(Helpers.format(quantity, digits: 4))
169
+ xml.text(Helpers.format(effective_basis_quantity, digits: 4))
136
170
  end
137
171
  xml['ram'].AppliedTradeAllowanceCharge do
138
172
  xml['ram'].ChargeIndicator do
@@ -156,7 +190,7 @@ module Secretariat
156
190
  Helpers.currency_element(xml, 'ram', 'ChargeAmount', net_amount, currency_code, add_currency: version == 1, digits: 4)
157
191
  if version == 2
158
192
  xml['ram'].BasisQuantity(unitCode: unit_code) do
159
- xml.text(Helpers.format(quantity, digits: 4))
193
+ xml.text(Helpers.format(effective_basis_quantity, digits: 4))
160
194
  end
161
195
  end
162
196
  end
@@ -166,7 +200,7 @@ module Secretariat
166
200
 
167
201
  xml['ram'].send(delivery) do
168
202
  xml['ram'].BilledQuantity(unitCode: unit_code) do
169
- xml.text(Helpers.format(quantity, digits: 4))
203
+ xml.text(Helpers.format(billed_quantity, digits: 4))
170
204
  end
171
205
  end
172
206
 
@@ -181,9 +215,25 @@ module Secretariat
181
215
  xml['ram'].send(percent,Helpers.format(tax_percent))
182
216
  end
183
217
  end
218
+
219
+ if version == 2 && self.service_period_start && self.service_period_end
220
+ xml['ram'].BillingSpecifiedPeriod do
221
+ xml['ram'].StartDateTime do
222
+ xml['udt'].DateTimeString(format: '102') do
223
+ xml.text(service_period_start.strftime("%Y%m%d"))
224
+ end
225
+ end
226
+ xml['ram'].EndDateTime do
227
+ xml['udt'].DateTimeString(format: '102') do
228
+ xml.text(service_period_end.strftime("%Y%m%d"))
229
+ end
230
+ end
231
+ end
232
+ end
233
+
184
234
  monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementLineMonetarySummation')
185
235
  xml['ram'].send(monetary_summation) do
186
- Helpers.currency_element(xml, 'ram', 'LineTotalAmount', (quantity.negative? ? -charge_amount : charge_amount), currency_code, add_currency: version == 1)
236
+ Helpers.currency_element(xml, 'ram', 'LineTotalAmount', (billed_quantity.negative? ? -charge_amount : charge_amount), currency_code, add_currency: version == 1)
187
237
  end
188
238
  end
189
239
 
@@ -0,0 +1,14 @@
1
+ module Secretariat
2
+ module ObjectExtensions
3
+ refine Object do
4
+ # Copied from activesupport/lib/active_support/core_ext/object/blank.rb, line 18
5
+ def blank?
6
+ respond_to?(:empty?) ? !!empty? : !self
7
+ end
8
+
9
+ def present?
10
+ !blank?
11
+ end
12
+ end
13
+ end
14
+ end
@@ -15,33 +15,45 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  module Secretariat
18
+ using ObjectExtensions
19
+
18
20
  TradeParty = Struct.new('TradeParty',
21
+ :id,
19
22
  :name, :street1, :street2, :city, :postal_code, :country_id, :vat_id, :global_id, :global_id_scheme_id, :tax_id,
23
+ :person_name,
20
24
  keyword_init: true,
21
25
  ) do
22
26
  def to_xml(xml, exclude_tax: false, version: 2)
23
- if global_id && global_id != '' && global_id_scheme_id && global_id_scheme_id != ''
27
+ if id && !exclude_tax
28
+ xml['ram'].ID id # BT-46
29
+ end
30
+ if global_id.present? && global_id_scheme_id.present?
24
31
  xml['ram'].GlobalID(schemeID: global_id_scheme_id) do
25
32
  xml.text(global_id)
26
33
  end
27
34
  end
28
35
  xml['ram'].Name name
36
+ if person_name
37
+ xml['ram'].DefinedTradeContact do
38
+ xml['ram'].PersonName person_name
39
+ end
40
+ end
29
41
  xml['ram'].PostalTradeAddress do
30
42
  xml['ram'].PostcodeCode postal_code
31
43
  xml['ram'].LineOne street1
32
- if street2 && street2 != ''
44
+ if street2.present?
33
45
  xml['ram'].LineTwo street2
34
46
  end
35
47
  xml['ram'].CityName city
36
48
  xml['ram'].CountryID country_id
37
49
  end
38
- if !exclude_tax && vat_id && vat_id != ''
50
+ if !exclude_tax && vat_id.present?
39
51
  xml['ram'].SpecifiedTaxRegistration do
40
52
  xml['ram'].ID(schemeID: 'VA') do
41
53
  xml.text(vat_id)
42
54
  end
43
55
  end
44
- elsif tax_id && tax_id != ''
56
+ elsif tax_id.present?
45
57
  xml['ram'].SpecifiedTaxRegistration do
46
58
  xml['ram'].ID(schemeID: 'FC') do
47
59
  xml.text(tax_id)
@@ -51,3 +63,5 @@ module Secretariat
51
63
  end
52
64
  end
53
65
  end
66
+
67
+ # assert_match(%r{<ram:DefinedTradeContact>\s*<ram:PersonName>Max Mustermann</ram:PersonName>\s*</ram:DefinedTradeContact>}, xml)
@@ -15,5 +15,5 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  module Secretariat
18
- VERSION = '3.6.0'
18
+ VERSION = "3.8.0"
19
19
  end
data/lib/secretariat.rb CHANGED
@@ -15,6 +15,7 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  require_relative 'secretariat/version'
18
+ require_relative 'secretariat/object_extensions'
18
19
  require_relative 'secretariat/constants'
19
20
  require_relative 'secretariat/helpers'
20
21
  require_relative 'secretariat/versioner'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secretariat
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.0
4
+ version: 3.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Krutisch
@@ -107,6 +107,7 @@ files:
107
107
  - lib/secretariat/helpers.rb
108
108
  - lib/secretariat/invoice.rb
109
109
  - lib/secretariat/line_item.rb
110
+ - lib/secretariat/object_extensions.rb
110
111
  - lib/secretariat/tax.rb
111
112
  - lib/secretariat/trade_party.rb
112
113
  - lib/secretariat/validation_error.rb