secretariat 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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