secretariat 1.0.0 → 2.0.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: ea7afc077807afc9c37b543936f44c92dd47efde7b88afd9cd0189962a725582
4
- data.tar.gz: bf531f1c30ded4f468a0b7b4c8657dece3d9abb3f758b9d1fad167f0ddccdc4a
3
+ metadata.gz: 3574ab368be0c92f96a25ad98e00f9c17a96da895d1ce3046a1c387eecf43d75
4
+ data.tar.gz: 90630718af00d0a76af1d75173d58f6c94f7eaf2485dafd9a75a773c2e2f7a97
5
5
  SHA512:
6
- metadata.gz: b00aa871cdbe0610476e5689756db15ec8f5d584c658548513d2e22c895e1f393e74b42f33b92c7ebb516bf08ebea2b64d240ab58235ef89b0a47a3fa4a9503d
7
- data.tar.gz: d34cccd54819f93c2fdf6850a2ffee4306d42034ded2748961814c7159258aab5c06bda3251497ef85e0e417958ab4d044945c544c3e757e244c2df1d7d1a018
6
+ metadata.gz: c3d9e62d62321d142b502e32b01198256cb22e65b4f0502c071a525a96ae4dff5200d54ca2313a985bad0571c923ebb8c044f5b12b4fd3b1260a4799d2a743cc
7
+ data.tar.gz: 39ea3264258a3e058b24276caf8232d53db7516b1d10aa2cee66807ee25d601bb68413d61f18125021fcd0bad6bbced1aad14f4b7126392991984f5b8f6dc5db
@@ -16,6 +16,8 @@ limitations under the License.
16
16
 
17
17
  require_relative 'secretariat/version'
18
18
  require_relative 'secretariat/constants'
19
+ require_relative 'secretariat/helpers'
20
+ require_relative 'secretariat/versioner'
19
21
  require_relative 'secretariat/validation_error'
20
22
  require_relative 'secretariat/invoice'
21
23
  require_relative 'secretariat/trade_party'
@@ -22,12 +22,36 @@ module Secretariat
22
22
  :TAXEXEMPT => "E",
23
23
  :ZEROTAXPRODUCTS => "Z",
24
24
  :UNTAXEDSERVICE => "O",
25
- :INTRACOMMUNITY => "K"
25
+ :INTRACOMMUNITY => "K",
26
+ :EXPORT => 'G'
27
+ }
28
+
29
+ TAX_CATEGORY_CODES_1 = {
30
+ :STANDARDRATE => "S",
31
+ :REVERSECHARGE => "AE",
32
+ :TAXEXEMPT => "E",
33
+ :ZEROTAXPRODUCTS => "Z",
34
+ :UNTAXEDSERVICE => "O",
35
+ :INTRACOMMUNITY => "IC",
36
+ :EXPORT => 'E'
37
+ }
38
+
39
+ PAYMENT_CODES = {
40
+ :BANKACCOUNT => "42",
41
+ :NOTSPECIFIED => "1",
42
+ :AUTOMATICCLEARING => "3",
43
+ :CASH => "10",
44
+ :CHECK => "20",
45
+ :DEBITADVICE => "31",
46
+ :CREDITCARD => "48",
47
+ :DEBIT => "49",
48
+ :COMPENSATION => "97",
26
49
  }
27
50
 
28
51
  TAX_EXEMPTION_REASONS = {
29
52
  :REVERSECHARGE => 'Reverse Charge',
30
- :INTRACOMMUNITY => ''
53
+ :INTRACOMMUNITY => 'Intra-community transaction',
54
+ :EXPORT => 'Export outside the EU'
31
55
  }
32
56
 
33
57
  UNIT_CODES = {
@@ -0,0 +1,19 @@
1
+ module Secretariat
2
+ module Helpers
3
+ def self.format(something, round: nil, digits:2)
4
+ dec = BigDecimal(something, 10)
5
+ dec = dec.round(round, :down) if round
6
+ "%0.#{digits}f" % dec
7
+ end
8
+
9
+ def self.currency_element(xml, ns, name, amount, currency, add_currency: true, digits: 2)
10
+ attrs = {}
11
+ if add_currency
12
+ attrs[:currencyID] = currency
13
+ end
14
+ xml[ns].send(name, attrs) do
15
+ xml.text(format(amount, round: 4, digits: digits))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -24,7 +24,7 @@ module Secretariat
24
24
  :buyer,
25
25
  :line_items,
26
26
  :currency_code,
27
- :payment_code,
27
+ :payment_type,
28
28
  :payment_text,
29
29
  :tax_category,
30
30
  :tax_percent,
@@ -38,6 +38,8 @@ module Secretariat
38
38
  keyword_init: true
39
39
  ) do
40
40
 
41
+ include Versioner
42
+
41
43
  def errors
42
44
  @errors
43
45
  end
@@ -46,57 +48,93 @@ module Secretariat
46
48
  tax_reason || TAX_EXEMPTION_REASONS[tax_category]
47
49
  end
48
50
 
49
- def tax_category_code
51
+ def tax_category_code(version: 2)
52
+ if version == 1
53
+ return TAX_CATEGORY_CODES_1[tax_category] || 'S'
54
+ end
50
55
  TAX_CATEGORY_CODES[tax_category] || 'S'
51
56
  end
52
57
 
58
+ def payment_code
59
+ PAYMENT_CODES[payment_type] || '1'
60
+ end
61
+
53
62
  def valid?
63
+ @errors = []
54
64
  tax = BigDecimal(tax_amount)
55
65
  basis = BigDecimal(basis_amount)
56
66
  calc_tax = basis * BigDecimal(tax_percent) / BigDecimal(100)
67
+ calc_tax = calc_tax.round(2, :down)
57
68
  if tax != calc_tax
58
- @errors << "Tax amount and calculated tax amount deviate"
69
+ @errors << "Tax amount and calculated tax amount deviate: #{tax} / #{calc_tax}"
59
70
  return false
60
71
  end
61
72
  grand_total = BigDecimal(grand_total_amount)
62
73
  calc_grand_total = basis + tax
63
74
  if grand_total != calc_grand_total
64
- @errors << "Grand total amount and calculated grand total amount deviate"
75
+ @errors << "Grand total amount and calculated grand total amount deviate: #{grand_total} / #{calc_grand_total}"
65
76
  return false
66
77
  end
67
78
  line_item_sum = line_items.inject(BigDecimal(0)) do |m, item|
68
79
  m + BigDecimal(item.charge_amount)
69
80
  end
70
81
  if line_item_sum != basis
71
- @errors << "Line items do not add up to basis amount"
82
+ @errors << "Line items do not add up to basis amount #{line_item_sum} / #{basis}"
72
83
  return false
73
84
  end
74
85
  return true
75
86
  end
76
87
 
77
88
 
78
- def to_xml()
89
+ def namespaces(version: 1)
90
+ by_version(version,
91
+ {
92
+ 'xmlns:ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12',
93
+ 'xmlns:udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15',
94
+ 'xmlns:rsm' => 'urn:ferd:CrossIndustryDocument:invoice:1p0',
95
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
96
+ },
97
+ {
98
+ 'xmlns:qdt' => 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
99
+ 'xmlns:ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
100
+ 'xmlns:udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
101
+ 'xmlns:rsm' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
102
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
103
+ }
104
+ )
105
+ end
106
+
107
+ def to_xml(version: 1)
108
+ if version < 1 || version > 2
109
+ raise 'Unsupported Document Version'
110
+ end
79
111
 
80
112
  unless valid?
81
113
  raise ValidationError.new("Invoice is invalid", errors)
82
114
  end
83
115
 
84
116
  builder = Nokogiri::XML::Builder.new do |xml|
85
- xml.CrossIndustryInvoice({
86
- 'xmlns:qdt' => 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
87
- 'xmlns:ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
88
- 'xmlns:udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
89
- 'xmlns' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
90
- 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
91
- }) do
92
- xml.ExchangedDocumentContext do
117
+
118
+ root = by_version(version, 'CrossIndustryDocument', 'CrossIndustryInvoice')
119
+
120
+ xml['rsm'].send(root, namespaces(version: version)) do
121
+
122
+ context = by_version(version, 'SpecifiedExchangedDocumentContext', 'ExchangedDocumentContext')
123
+
124
+ xml['rsm'].send(context) do
93
125
  xml['ram'].GuidelineSpecifiedDocumentContextParameter do
94
- xml['ram'].ID 'urn:cen.eu:en16931:2017'
126
+ version_id = by_version(version, 'urn:ferd:CrossIndustryDocument:invoice:1p0:comfort', 'urn:cen.eu:en16931:2017')
127
+ xml['ram'].ID version_id
95
128
  end
96
129
  end
97
130
 
98
- xml.ExchangedDocument do
131
+ header = by_version(version, 'HeaderExchangedDocument', 'ExchangedDocument')
132
+
133
+ xml['rsm'].send(header) do
99
134
  xml['ram'].ID id
135
+ if version == 1
136
+ xml['ram'].Name "RECHNUNG"
137
+ end
100
138
  xml['ram'].TypeCode '380' # TODO: make configurable
101
139
  xml['ram'].IssueDateTime do
102
140
  xml['udt'].DateTimeString(format: '102') do
@@ -104,21 +142,33 @@ module Secretariat
104
142
  end
105
143
  end
106
144
  end
107
- xml.SupplyChainTradeTransaction do
108
- line_items.each_with_index do |item, i|
109
- item.to_xml(xml, i + 1) # one indexed
145
+ transaction = by_version(version, 'SpecifiedSupplyChainTradeTransaction', 'SupplyChainTradeTransaction')
146
+ xml['rsm'].send(transaction) do
147
+
148
+ if version == 2
149
+ line_items.each_with_index do |item, i|
150
+ item.to_xml(xml, i + 1, version: version) # one indexed
151
+ end
110
152
  end
111
- xml['ram'].ApplicableHeaderTradeAgreement do
153
+
154
+ trade_agreement = by_version(version, 'ApplicableSupplyChainTradeAgreement', 'ApplicableHeaderTradeAgreement')
155
+
156
+ xml['ram'].send(trade_agreement) do
112
157
  xml['ram'].SellerTradeParty do
113
- seller.to_xml(xml)
158
+ seller.to_xml(xml, version: version)
114
159
  end
115
160
  xml['ram'].BuyerTradeParty do
116
- buyer.to_xml(xml)
161
+ buyer.to_xml(xml, version: version)
117
162
  end
118
163
  end
119
- xml['ram'].ApplicableHeaderTradeDelivery do
120
- xml['ram'].ShipToTradeParty do
121
- buyer.to_xml(xml, exclude_tax: true)
164
+
165
+ delivery = by_version(version, 'ApplicableSupplyChainTradeDelivery', 'ApplicableHeaderTradeDelivery')
166
+
167
+ xml['ram'].send(delivery) do
168
+ if version == 2
169
+ xml['ram'].ShipToTradeParty do
170
+ buyer.to_xml(xml, exclude_tax: true, version: version)
171
+ end
122
172
  end
123
173
  xml['ram'].ActualDeliverySupplyChainEvent do
124
174
  xml['ram'].OccurrenceDateTime do
@@ -128,34 +178,47 @@ module Secretariat
128
178
  end
129
179
  end
130
180
  end
131
- xml['ram'].ApplicableHeaderTradeSettlement do
181
+ trade_settlement = by_version(version, 'ApplicableSupplyChainTradeSettlement', 'ApplicableHeaderTradeSettlement')
182
+ xml['ram'].send(trade_settlement) do
132
183
  xml['ram'].InvoiceCurrencyCode currency_code
133
184
  xml['ram'].SpecifiedTradeSettlementPaymentMeans do
134
185
  xml['ram'].TypeCode payment_code
135
186
  xml['ram'].Information payment_text
136
187
  end
137
188
  xml['ram'].ApplicableTradeTax do
138
- xml['ram'].CalculatedAmount tax_amount
189
+
190
+ Helpers.currency_element(xml, 'ram', 'CalculatedAmount', tax_amount, currency_code, add_currency: version == 1)
139
191
  xml['ram'].TypeCode 'VAT'
140
192
  if tax_reason_text && tax_reason_text != ''
141
- xml['ram'].ExemptionReason tax_reason
193
+ xml['ram'].ExemptionReason tax_reason_text
142
194
  end
143
- xml['ram'].BasisAmount basis_amount
144
- xml['ram'].CategoryCode tax_category_code
145
- xml['ram'].RateApplicablePercent tax_percent
195
+ Helpers.currency_element(xml, 'ram', 'BasisAmount', basis_amount, currency_code, add_currency: version == 1)
196
+ xml['ram'].CategoryCode tax_category_code(version: version)
197
+
198
+ percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
199
+ xml['ram'].send(percent, Helpers.format(tax_percent))
146
200
  end
147
201
  xml['ram'].SpecifiedTradePaymentTerms do
148
202
  xml['ram'].Description "Paid"
149
203
  end
150
- xml['ram'].SpecifiedTradeSettlementHeaderMonetarySummation do
151
- xml['ram'].LineTotalAmount basis_amount
152
- xml['ram'].TaxBasisTotalAmount basis_amount
153
- xml['ram'].TaxTotalAmount(currencyID: currency_code) do
154
- xml.text(tax_amount)
155
- end
156
- xml['ram'].GrandTotalAmount grand_total_amount
157
- xml['ram'].TotalPrepaidAmount paid_amount
158
- xml['ram'].DuePayableAmount due_amount
204
+
205
+ monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementHeaderMonetarySummation')
206
+
207
+ xml['ram'].send(monetary_summation) do
208
+ Helpers.currency_element(xml, 'ram', 'LineTotalAmount', basis_amount, currency_code, add_currency: version == 1)
209
+ # TODO: Fix this!
210
+ Helpers.currency_element(xml, 'ram', 'ChargeTotalAmount', BigDecimal(0), currency_code, add_currency: version == 1)
211
+ Helpers.currency_element(xml, 'ram', 'AllowanceTotalAmount', BigDecimal(0), currency_code, add_currency: version == 1)
212
+ Helpers.currency_element(xml, 'ram', 'TaxBasisTotalAmount', basis_amount, currency_code, add_currency: version == 1)
213
+ Helpers.currency_element(xml, 'ram', 'TaxTotalAmount', tax_amount, currency_code, add_currency: true)
214
+ Helpers.currency_element(xml, 'ram', 'GrandTotalAmount', grand_total_amount, currency_code, add_currency: version == 1)
215
+ Helpers.currency_element(xml, 'ram', 'TotalPrepaidAmount', paid_amount, currency_code, add_currency: version == 1)
216
+ Helpers.currency_element(xml, 'ram', 'DuePayableAmount', due_amount, currency_code, add_currency: version == 1)
217
+ end
218
+ end
219
+ if version == 1
220
+ line_items.each_with_index do |item, i|
221
+ item.to_xml(xml, i + 1, version: version) # one indexed
159
222
  end
160
223
  end
161
224
  end
@@ -164,5 +227,4 @@ module Secretariat
164
227
  builder.to_xml
165
228
  end
166
229
  end
167
-
168
230
  end
@@ -25,32 +25,49 @@ module Secretariat
25
25
  :name,
26
26
  :quantity,
27
27
  :unit,
28
- :unit_amount,
29
- :charge_amount,
28
+ :gross_amount,
29
+ :net_amount,
30
30
  :tax_category,
31
31
  :tax_percent,
32
32
  :tax_amount,
33
+ :discount_amount,
34
+ :discount_reason,
35
+ :charge_amount,
33
36
  :origin_country_code,
37
+ :currency_code,
34
38
  keyword_init: true
35
39
  ) do
36
40
 
41
+ include Versioner
42
+
37
43
  def errors
38
44
  @errors
39
45
  end
40
46
 
41
47
  def valid?
42
- unit_price = BigDecimal(unit_amount)
48
+ @errors = []
49
+ net_price = BigDecimal(net_amount)
50
+ gross_price = BigDecimal(gross_amount)
43
51
  charge_price = BigDecimal(charge_amount)
44
52
  tax = BigDecimal(tax_amount)
53
+ unit_price = net_price * BigDecimal(quantity)
45
54
 
46
- if charge_price != unit_price * BigDecimal(quantity)
47
- @errors << "charge price and unit price times quantity deviate"
55
+ if charge_price != unit_price
56
+ @errors << "charge price and gross price times quantity deviate: #{charge_price} / #{unit_price}"
48
57
  return false
49
58
  end
59
+ if discount_amount
60
+ discount = BigDecimal(discount_amount)
61
+ calculated_net_price = (gross_price - discount).round(2, :down)
62
+ if calculated_net_price != net_price
63
+ @errors = "Calculated net price and net price deviate: #{calculated_net_price} / #{net_price}"
64
+ return false
65
+ end
66
+ end
50
67
 
51
68
  calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
52
69
  if calculated_tax != tax
53
- @errors << "Tax and calculated tax deviate"
70
+ @errors << "Tax and calculated tax deviate: #{tax} / #{calculated_tax}"
54
71
  return false
55
72
  end
56
73
  return true
@@ -60,12 +77,16 @@ module Secretariat
60
77
  UNIT_CODES[unit] || 'C62'
61
78
  end
62
79
 
63
- def tax_category_code
80
+ def tax_category_code(version: 2)
81
+ if version == 1
82
+ return TAX_CATEGORY_CODES_1[tax_category] || 'S'
83
+ end
64
84
  TAX_CATEGORY_CODES[tax_category] || 'S'
65
85
  end
66
86
 
67
- def to_xml(xml, line_item_index)
87
+ def to_xml(xml, line_item_index, version: 2)
68
88
  if !valid?
89
+ pp errors
69
90
  raise ValidationError.new("LineItem #{line_item_index} is invalid", errors)
70
91
  end
71
92
 
@@ -73,40 +94,79 @@ module Secretariat
73
94
  xml['ram'].AssociatedDocumentLineDocument do
74
95
  xml['ram'].LineID line_item_index
75
96
  end
76
- xml['ram'].SpecifiedTradeProduct do
77
- xml['ram'].Name name
78
- xml['ram'].OriginTradeCountry do
79
- xml['ram'].ID origin_country_code
97
+ if (version == 2)
98
+ xml['ram'].SpecifiedTradeProduct do
99
+ xml['ram'].Name name
100
+ xml['ram'].OriginTradeCountry do
101
+ xml['ram'].ID origin_country_code
102
+ end
80
103
  end
81
104
  end
82
- xml['ram'].SpecifiedLineTradeAgreement do
105
+ agreement = by_version(version, 'SpecifiedSupplyChainTradeAgreement', 'SpecifiedLineTradeAgreement')
106
+
107
+ xml['ram'].send(agreement) do
83
108
  xml['ram'].GrossPriceProductTradePrice do
84
- xml['ram'].ChargeAmount unit_amount
85
- xml['ram'].BasisQuantity(unitCode: unit_code) do
86
- xml.text(quantity)
109
+ Helpers.currency_element(xml, 'ram', 'ChargeAmount', gross_amount, currency_code, add_currency: version == 1, digits: 4)
110
+ if version == 2 && discount_amount
111
+ xml['ram'].BasisQuantity(unitCode: unit_code) do
112
+ xml.text(Helpers.format(quantity, digits: 4))
113
+ end
114
+ xml['ram'].AppliedTradeAllowanceCharge do
115
+ xml['ram'].ChargeIndicator do
116
+ xml['udt'].Indicator 'false'
117
+ end
118
+ Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1)
119
+ xml['ram'].Reason discount_reason
120
+ end
121
+ end
122
+ if version == 1 && discount_amount
123
+ xml['ram'].AppliedTradeAllowanceCharge do
124
+ xml['ram'].ChargeIndicator do
125
+ xml['udt'].Indicator 'false'
126
+ end
127
+ Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1)
128
+ xml['ram'].Reason discount_reason
129
+ end
87
130
  end
88
131
  end
89
132
  xml['ram'].NetPriceProductTradePrice do
90
- xml['ram'].ChargeAmount unit_amount
91
- xml['ram'].BasisQuantity(unitCode: unit_code) do
92
- xml.text(quantity)
133
+ Helpers.currency_element(xml, 'ram', 'ChargeAmount', net_amount, currency_code, add_currency: version == 1, digits: 4)
134
+ if version == 2
135
+ xml['ram'].BasisQuantity(unitCode: unit_code) do
136
+ xml.text(Helpers.format(quantity, digits: 4))
137
+ end
93
138
  end
94
139
  end
95
140
  end
96
- xml['ram'].SpecifiedLineTradeDelivery do
141
+
142
+ delivery = by_version(version, 'SpecifiedSupplyChainTradeDelivery', 'SpecifiedLineTradeDelivery')
143
+
144
+ xml['ram'].send(delivery) do
97
145
  xml['ram'].BilledQuantity(unitCode: unit_code) do
98
- xml.text(quantity)
146
+ xml.text(Helpers.format(quantity, digits: 4))
99
147
  end
100
148
  end
101
- xml['ram'].SpecifiedLineTradeSettlement do
149
+
150
+ settlement = by_version(version, 'SpecifiedSupplyChainTradeSettlement', 'SpecifiedLineTradeSettlement')
151
+
152
+ xml['ram'].send(settlement) do
102
153
  xml['ram'].ApplicableTradeTax do
103
154
  xml['ram'].TypeCode 'VAT'
104
- xml['ram'].CategoryCode tax_category_code
105
- xml['ram'].RateApplicablePercent tax_percent
155
+ xml['ram'].CategoryCode tax_category_code(version: version)
106
156
 
157
+ percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
158
+ xml['ram'].send(percent,Helpers.format(tax_percent))
159
+
160
+ end
161
+ monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementLineMonetarySummation')
162
+ xml['ram'].send(monetary_summation) do
163
+ Helpers.currency_element(xml, 'ram', 'LineTotalAmount', charge_amount, currency_code, add_currency: version == 1)
107
164
  end
108
- xml['ram'].SpecifiedTradeSettlementLineMonetarySummation do
109
- xml['ram'].LineTotalAmount charge_amount
165
+ end
166
+
167
+ if version == 1
168
+ xml['ram'].SpecifiedTradeProduct do
169
+ xml['ram'].Name name
110
170
  end
111
171
  end
112
172
  end