saft 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemspec +34 -0
- data/.rspec +4 -0
- data/.rubocop.yml +72 -0
- data/.rubocop_strict.yml +7 -0
- data/.ruby-gemset +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +26 -0
- data/Rakefile +18 -0
- data/docs/Norwegian SAF-T Financial data.pdf +0 -0
- data/docs/Norwegian SAF-T Standard VAT:Tax codes.pdf +0 -0
- data/docs/saf-t v2 xsd by oecd.pdf +0 -0
- data/lib/saft/v2/html.css +3 -0
- data/lib/saft/v2/html.rb +685 -0
- data/lib/saft/v2/html_dist.css +594 -0
- data/lib/saft/v2/norway.rb +133 -0
- data/lib/saft/v2/parse.rb +351 -0
- data/lib/saft/v2/scribe.rb +364 -0
- data/lib/saft/v2/types.rb +493 -0
- data/lib/saft/v2/xsd_validate.rb +28 -0
- data/lib/saft/v2.rb +36 -0
- data/lib/saft/version.rb +3 -0
- data/lib/saft.rb +26 -0
- data/package.json +15 -0
- data/pnpm-lock.yaml +443 -0
- data/readme.md +92 -0
- data/tailwind.config.js +10 -0
- data/vendor/SAF-T_Financial_Schema_NO_1.10.xsd +3343 -0
- data/vendor/norway/general_ledger_standard_accounts.xsd +54 -0
- data/vendor/norway/general_ledger_standard_accounts_2_character.csv +75 -0
- data/vendor/norway/general_ledger_standard_accounts_2_character.xml +373 -0
- data/vendor/norway/general_ledger_standard_accounts_4_character.csv +747 -0
- data/vendor/norway/general_ledger_standard_accounts_4_character.xml +3733 -0
- data/vendor/norway/standard_tax_codes.csv +31 -0
- data/vendor/norway/standard_tax_codes.xml +199 -0
- data/vendor/norway/standard_tax_codes.xsd +67 -0
- metadata +121 -0
@@ -0,0 +1,493 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-struct"
|
4
|
+
|
5
|
+
# This file contains the xml intermediate structure
|
6
|
+
# The node names are generated from the XSD located in
|
7
|
+
# ./vendor/SAF-T_Financial_Schema1.10.xsd. There are at least a couple of nodes
|
8
|
+
# which are ignored since it has the comment "Not in use.". I don't look at it as
|
9
|
+
# a bug since we don't need then but we can implement when/if thay are ever
|
10
|
+
# needed.
|
11
|
+
|
12
|
+
module SAFT::V2::Types
|
13
|
+
module Types
|
14
|
+
include Dry.Types
|
15
|
+
|
16
|
+
Date = Params::Date
|
17
|
+
DateTime = Params::DateTime
|
18
|
+
Integer = Coercible::Integer
|
19
|
+
Decimal = Coercible::Decimal
|
20
|
+
Bool = Params::Bool
|
21
|
+
String = Strict::String
|
22
|
+
PositiveInteger = Types::Integer.constrained(gteq: 0)
|
23
|
+
end
|
24
|
+
|
25
|
+
# not Ideal as we are extending the global scope
|
26
|
+
Dry::Logic::Predicates.predicate(:digits?) do |num, input|
|
27
|
+
_sign, significant_digits, _base, exponent = input.split
|
28
|
+
n_significant_digits = significant_digits.length
|
29
|
+
n_significant_digits += -exponent if exponent.negative?
|
30
|
+
n_significant_digits <= num
|
31
|
+
end
|
32
|
+
|
33
|
+
Dry::Logic::Predicates.predicate(:fraction_digitst?) do |num, input|
|
34
|
+
digits?(num, input.frac)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Aka base
|
38
|
+
module ObjectStrutures
|
39
|
+
def self.extended(base)
|
40
|
+
base.class_eval do
|
41
|
+
# not how I wanted to write this but this is how it turn out then I
|
42
|
+
# wanted to have different level of strictness for types like
|
43
|
+
# SAFmiddle1textType
|
44
|
+
|
45
|
+
klass = Class.new(Dry::Struct) do
|
46
|
+
attribute(:amount, base::SAFmonetaryType)
|
47
|
+
|
48
|
+
attribute(:currency_code, base::ISOCurrencyCode.optional.meta(omittable: true))
|
49
|
+
attribute(:currency_amount, base::SAFmonetaryType.optional.meta(omittable: true))
|
50
|
+
attribute(:exchange_rate, base::SAFexchangerateType.optional.meta(omittable: true))
|
51
|
+
end
|
52
|
+
|
53
|
+
base.const_set(:AmountStructure, klass)
|
54
|
+
|
55
|
+
klass = Class.new(Dry::Struct) do
|
56
|
+
attribute(:analysis_type, base::SAFcodeType)
|
57
|
+
attribute(:analysis_id, base::SAFlongtextType)
|
58
|
+
attribute(:analysis_amount, base::AmountStructure.optional.meta(omittable: true))
|
59
|
+
end
|
60
|
+
|
61
|
+
base.const_set(:AnalysisStructure, klass)
|
62
|
+
|
63
|
+
klass = Class.new(Dry::Struct) do
|
64
|
+
attribute(:analysis_type, base::SAFcodeType)
|
65
|
+
attribute(:analysis_id, base::SAFlongtextType)
|
66
|
+
end
|
67
|
+
|
68
|
+
base.const_set(:AnalysisPartyInfoStructure, klass)
|
69
|
+
|
70
|
+
klass = Class.new(Dry::Struct) do
|
71
|
+
attribute(:tax_type, base::SAFcodeType.optional.meta(omittable: true))
|
72
|
+
attribute(:tax_code, base::SAFmiddle1textType.optional.meta(omittable: true))
|
73
|
+
attribute(:tax_percentage, Types::Decimal.optional.meta(omittable: true))
|
74
|
+
attribute(:country, base::ISOCountryCode.optional.meta(omittable: true))
|
75
|
+
attribute(:tax_base, Types::Decimal.optional.meta(omittable: true))
|
76
|
+
attribute(:tax_base_description, base::SAFmiddle2textType.optional.meta(omittable: true))
|
77
|
+
attribute(:tax_amount, base::AmountStructure)
|
78
|
+
attribute(:tax_exemption_reason, base::SAFmiddle2textType.optional.meta(omittable: true))
|
79
|
+
attribute(:tax_declaration_period, base::SAFmiddle1textType.optional.meta(omittable: true))
|
80
|
+
end
|
81
|
+
|
82
|
+
base.const_set(:TaxInformationStructure, klass)
|
83
|
+
|
84
|
+
klass = Class.new(Dry::Struct) do
|
85
|
+
attribute(:tax_registration_number, base::SAFmiddle1textType)
|
86
|
+
# ignored since xsd states "Not in use."
|
87
|
+
# attribute :tax_type, base::SAFcodeType.optional.meta(omittable: true)
|
88
|
+
# attribute :tax_number, base::SAFmiddle1textType.optional.meta(omittable: true)
|
89
|
+
attribute(:tax_authority, base::SAFmiddle1textType.optional.meta(omittable: true))
|
90
|
+
attribute(:tax_verification_date, Types::Date.optional.meta(omittable: true))
|
91
|
+
end
|
92
|
+
|
93
|
+
base.const_set(:TaxIDStructure, klass)
|
94
|
+
|
95
|
+
klass = Class.new(Dry::Struct) do
|
96
|
+
attribute(:tax_reporting_jurisdiction, base::SAFmiddle1textType.optional.meta(omittable: true))
|
97
|
+
attribute(:company_entity, base::SAFmiddle2textType.optional.meta(omittable: true))
|
98
|
+
attribute(:selection_start_date, Types::Date.optional.meta(omittable: true))
|
99
|
+
attribute(:selection_end_date, Types::Date.optional.meta(omittable: true))
|
100
|
+
attribute(:period_start, Types::PositiveInteger.optional.meta(omittable: true))
|
101
|
+
attribute(:period_start_year, Types::Integer.constrained(gteq: 1970).constrained(lteq: 2100).optional.meta(omittable: true))
|
102
|
+
attribute(:period_end, Types::PositiveInteger.optional.meta(omittable: true))
|
103
|
+
attribute(:period_end_year, Types::Integer.constrained(gteq: 1970).constrained(lteq: 2100).optional.meta(omittable: true))
|
104
|
+
attribute(:document_type, base::SAFlongtextType.optional.meta(omittable: true))
|
105
|
+
attribute(:other_criterias, Types::Array.of(base::SAFlongtextType).optional.meta(omittable: true))
|
106
|
+
end
|
107
|
+
|
108
|
+
base.const_set(:SelectionCriteriaStructure, klass)
|
109
|
+
|
110
|
+
klass = Class.new(Dry::Struct) do
|
111
|
+
attribute(:title, base::SAFcodeType.optional.meta(omittable: true))
|
112
|
+
attribute(:first_name, base::SAFmiddle1textType)
|
113
|
+
attribute(:initials, base::SAFshorttextType.optional.meta(omittable: true))
|
114
|
+
attribute(:last_name_prefix, base::SAFshorttextType.optional.meta(omittable: true))
|
115
|
+
attribute(:last_name, base::SAFmiddle2textType)
|
116
|
+
attribute(:birth_name, base::SAFmiddle2textType.optional.meta(omittable: true))
|
117
|
+
attribute(:salutation, base::SAFshorttextType.optional.meta(omittable: true))
|
118
|
+
attribute(:other_titles, Types::Array.of(base::SAFshorttextType).optional.meta(omittable: true))
|
119
|
+
end
|
120
|
+
|
121
|
+
base.const_set(:PersonNameStructure, klass)
|
122
|
+
|
123
|
+
klass = Class.new(Dry::Struct) do
|
124
|
+
attribute(:days, Types::PositiveInteger.optional.meta(omittable: true))
|
125
|
+
attribute(:months, Types::PositiveInteger.optional.meta(omittable: true))
|
126
|
+
attribute(:cash_discount_days, Types::PositiveInteger.optional.meta(omittable: true))
|
127
|
+
attribute(:cash_discount_rate, Types::Decimal.constrained(gteq: 0).constrained(lteq: 100).optional.meta(omittable: true))
|
128
|
+
attribute(:free_billing_month, Types::Bool.optional.meta(omittable: true))
|
129
|
+
end
|
130
|
+
|
131
|
+
base.const_set(:PaymentTerms, klass)
|
132
|
+
|
133
|
+
klass = Class.new(Dry::Struct) do
|
134
|
+
attribute(:payment_terms, base::PaymentTerms.optional.meta(omittable: true))
|
135
|
+
attribute(:nace_code, base::SAFshorttextType.optional.meta(omittable: true))
|
136
|
+
attribute(:currency_code, base::ISOCurrencyCode.optional.meta(omittable: true))
|
137
|
+
attribute(:type, base::SAFmiddle1textType.optional.meta(omittable: true))
|
138
|
+
attribute(:status, base::SAFmiddle1textType.optional.meta(omittable: true))
|
139
|
+
attribute(:analyses, Types::Array.of(base::AnalysisPartyInfoStructure).optional.meta(omittable: true))
|
140
|
+
attribute(:notes, Types::String.optional.meta(omittable: true))
|
141
|
+
end
|
142
|
+
|
143
|
+
base.const_set(:PartyInfoStructure, klass)
|
144
|
+
|
145
|
+
klass = Class.new(Dry::Struct) do
|
146
|
+
attribute(:street_name, base::SAFmiddle2textType.optional.meta(omittable: true))
|
147
|
+
attribute(:number, base::SAFshorttextType.optional.meta(omittable: true))
|
148
|
+
attribute(:additional_address_detail, base::SAFmiddle2textType.optional.meta(omittable: true))
|
149
|
+
attribute(:building, base::SAFmiddle1textType.optional.meta(omittable: true))
|
150
|
+
attribute(:city, base::SAFmiddle1textType.optional.meta(omittable: true))
|
151
|
+
attribute(:postal_code, base::SAFshorttextType.optional.meta(omittable: true))
|
152
|
+
attribute(:region, base::SAFmiddle1textType.optional.meta(omittable: true))
|
153
|
+
attribute(:country, base::ISOCountryCode.optional.meta(omittable: true))
|
154
|
+
attribute(:address_type, base::AddressType.optional.meta(omittable: true))
|
155
|
+
end
|
156
|
+
|
157
|
+
base.const_set(:AddressStructure, klass)
|
158
|
+
|
159
|
+
klass = Class.new(Dry::Struct) do
|
160
|
+
attribute(:delivery_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
161
|
+
attribute(:delivery_date, Types::Date.optional.meta(omittable: true))
|
162
|
+
attribute(:warehouse_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
163
|
+
attribute(:location_id, base::SAFshorttextType.optional.meta(omittable: true))
|
164
|
+
attribute(:ucr, base::SAFmiddle1textType.optional.meta(omittable: true))
|
165
|
+
attribute(:address, base::AddressStructure.optional.meta(omittable: true))
|
166
|
+
end
|
167
|
+
|
168
|
+
base.const_set(:ShippingPointStructure, klass)
|
169
|
+
|
170
|
+
klass = Class.new(Dry::Struct) do
|
171
|
+
attribute(:contact_person, base::PersonNameStructure)
|
172
|
+
attribute(:telephone, base::SAFshorttextType.optional.meta(omittable: true))
|
173
|
+
attribute(:fax, base::SAFshorttextType.optional.meta(omittable: true))
|
174
|
+
attribute(:email, base::SAFmiddle2textType.optional.meta(omittable: true))
|
175
|
+
attribute(:website, Types::String.optional.meta(omittable: true))
|
176
|
+
attribute(:mobile_phone, base::SAFshorttextType.optional.meta(omittable: true))
|
177
|
+
end
|
178
|
+
|
179
|
+
base.const_set(:ContactInformationStructure, klass)
|
180
|
+
|
181
|
+
klass = Class.new(Dry::Struct) do
|
182
|
+
attribute(:iban_number, base::SAFmiddle1textType.optional.meta(omittable: true))
|
183
|
+
attribute(:bank_account_number, base::SAFmiddle1textType.optional.meta(omittable: true))
|
184
|
+
attribute(:bank_account_name, base::SAFmiddle2textType.optional.meta(omittable: true))
|
185
|
+
attribute(:sort_code, base::SAFshorttextType.optional.meta(omittable: true))
|
186
|
+
attribute(:bic, base::SAFshorttextType.optional.meta(omittable: true))
|
187
|
+
attribute(:currency_code, base::ISOCurrencyCode.optional.meta(omittable: true))
|
188
|
+
attribute(:general_ledger_account_id, base::SAFmiddle2textType.optional.meta(omittable: true))
|
189
|
+
end
|
190
|
+
|
191
|
+
base.const_set(:BankAccountStructure, klass)
|
192
|
+
|
193
|
+
klass = Class.new(Dry::Struct) do
|
194
|
+
attribute(:registration_number, base::SAFmiddle1textType.optional.meta(omittable: true))
|
195
|
+
attribute(:name, base::SAFmiddle2textType)
|
196
|
+
attribute(:addresses, Types::Array.of(base::AddressStructure))
|
197
|
+
attribute(:contacts, Types::Array.of(base::ContactInformationStructure).optional.meta(omittable: true))
|
198
|
+
attribute(:tax_registrations, Types::Array.of(base::TaxIDStructure).optional.meta(omittable: true))
|
199
|
+
attribute(:bank_accounts, Types::Array.of(base::BankAccountStructure).optional.meta(omittable: true))
|
200
|
+
end
|
201
|
+
|
202
|
+
base.const_set(:CompanyStructure, klass)
|
203
|
+
|
204
|
+
klass = Class.new(base::CompanyStructure) do
|
205
|
+
attribute(:registration_number, base::SAFmiddle1textType)
|
206
|
+
attribute(:name, base::SAFmiddle2textType)
|
207
|
+
attribute(:addresses, Types::Array.of(base::AddressStructure))
|
208
|
+
attribute(:contacts, Types::Array.of(base::ContactInformationStructure))
|
209
|
+
attribute(:tax_registrations, Types::Array.of(base::TaxIDStructure).optional.meta(omittable: true))
|
210
|
+
attribute(:bank_accounts, Types::Array.of(base::BankAccountStructure).optional.meta(omittable: true))
|
211
|
+
end
|
212
|
+
|
213
|
+
base.const_set(:CompanyHeaderStructure, klass)
|
214
|
+
|
215
|
+
klass = Class.new(Dry::Struct) do
|
216
|
+
attribute(:audit_file_version, base::SAFcodeType)
|
217
|
+
attribute(:audit_file_country, base::ISOCountryCode)
|
218
|
+
# ignored sine XSD said "not in use."
|
219
|
+
# attribute :audit_file_region, base::SAFcodeType.optional.meta(omittable: true)
|
220
|
+
attribute(:audit_file_date_created, Types::Date)
|
221
|
+
attribute(:software_company_name, base::SAFmiddle2textType)
|
222
|
+
attribute(:software_id, base::SAFlongtextType)
|
223
|
+
attribute(:software_version, base::SAFshorttextType)
|
224
|
+
attribute(:company, base::CompanyHeaderStructure)
|
225
|
+
attribute(:default_currency_code, base::ISOCurrencyCode)
|
226
|
+
attribute(:selection_criteria, base::SelectionCriteriaStructure.optional.meta(omittable: true))
|
227
|
+
attribute(:header_comment, base::SAFlongtextType.optional.meta(omittable: true))
|
228
|
+
end
|
229
|
+
|
230
|
+
base.const_set(:HeaderStructure, klass)
|
231
|
+
|
232
|
+
klass = Class.new(base::HeaderStructure) do
|
233
|
+
attribute(:tax_accounting_basis, base::SAFshorttextType)
|
234
|
+
attribute(:tax_entity, base::SAFmiddle2textType.optional.meta(omittable: true))
|
235
|
+
attribute(:user_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
236
|
+
attribute(:audit_file_sender, base::CompanyStructure.optional.meta(omittable: true))
|
237
|
+
end
|
238
|
+
|
239
|
+
base.const_set(:Header, klass)
|
240
|
+
|
241
|
+
klass = Class.new(Dry::Struct) do
|
242
|
+
attribute(:account_id, base::SAFmiddle2textType)
|
243
|
+
attribute(:account_description, base::SAFlongtextType)
|
244
|
+
attribute(:standard_account_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
245
|
+
attribute(:grouping_category, base::SAFmiddle1textType.optional.meta(omittable: true))
|
246
|
+
attribute(:grouping_code, base::SAFmiddle1textType.optional.meta(omittable: true))
|
247
|
+
attribute(:account_type, base::SAFshorttextType)
|
248
|
+
attribute(:account_creation_date, Types::Date.optional.meta(omittable: true))
|
249
|
+
attribute(:opening_debit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
250
|
+
attribute(:opening_credit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
251
|
+
attribute(:closing_debit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
252
|
+
attribute(:closing_credit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
253
|
+
end
|
254
|
+
|
255
|
+
base.const_set(:Account, klass)
|
256
|
+
|
257
|
+
klass = Class.new(base::CompanyStructure) do
|
258
|
+
attribute(:customer_id, base::SAFmiddle1textType)
|
259
|
+
attribute(:self_billing_indicator, base::SAFcodeType.optional.meta(omittable: true))
|
260
|
+
attribute(:account_id, base::SAFmiddle2textType.optional.meta(omittable: true))
|
261
|
+
attribute(:opening_debit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
262
|
+
attribute(:opening_credit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
263
|
+
attribute(:closing_debit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
264
|
+
attribute(:closing_credit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
265
|
+
attribute(:party_info, base::PartyInfoStructure.optional.meta(omittable: true))
|
266
|
+
end
|
267
|
+
|
268
|
+
base.const_set(:Customer, klass)
|
269
|
+
|
270
|
+
klass = Class.new(base::CompanyStructure) do
|
271
|
+
attribute(:supplier_id, base::SAFmiddle1textType)
|
272
|
+
attribute(:self_billing_indicator, base::SAFcodeType.optional.meta(omittable: true))
|
273
|
+
attribute(:account_id, base::SAFmiddle2textType.optional.meta(omittable: true))
|
274
|
+
attribute(:opening_debit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
275
|
+
attribute(:opening_credit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
276
|
+
attribute(:closing_debit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
277
|
+
attribute(:closing_credit_balance, base::SAFmonetaryType.optional.meta(omittable: true))
|
278
|
+
attribute(:party_info, base::PartyInfoStructure.optional.meta(omittable: true))
|
279
|
+
end
|
280
|
+
|
281
|
+
base.const_set(:Supplier, klass)
|
282
|
+
|
283
|
+
klass = Class.new(Dry::Struct) do
|
284
|
+
attribute(:tax_code, base::SAFmiddle1textType)
|
285
|
+
attribute(:effective_date, Types::Date.optional.meta(omittable: true))
|
286
|
+
attribute(:expiration_date, Types::Date.optional.meta(omittable: true))
|
287
|
+
attribute(:description, base::SAFlongtextType.optional.meta(omittable: true))
|
288
|
+
attribute(:tax_percentage, Types::Decimal.optional.meta(omittable: true))
|
289
|
+
# ignored because XSD states "Not in Use."
|
290
|
+
# attribute :flat_tax_rate, AmountStructure.optional.meta(omittable: true)
|
291
|
+
attribute(:country, base::ISOCountryCode)
|
292
|
+
# ignored because XSD states "Not in Use."
|
293
|
+
# attribute :region, base::SAFcodeType.optional.meta(omittable: true)
|
294
|
+
attribute(:standard_tax_code, base::SAFmiddle1textType)
|
295
|
+
attribute(:compensation, Types::Bool.optional.meta(omittable: true))
|
296
|
+
attribute(:base_rates, Types::Array.of(Types::Decimal))
|
297
|
+
end
|
298
|
+
|
299
|
+
base.const_set(:TaxCodeDetails, klass)
|
300
|
+
|
301
|
+
klass = Class.new(Dry::Struct) do
|
302
|
+
attribute(:tax_type, base::SAFcodeType)
|
303
|
+
attribute(:description, base::SAFlongtextType)
|
304
|
+
attribute(:tax_code_details, Types::Array.of(base::TaxCodeDetails).constrained(min_size: 1))
|
305
|
+
end
|
306
|
+
|
307
|
+
base.const_set(:TaxTableEntry, klass)
|
308
|
+
|
309
|
+
klass = Class.new(Dry::Struct) do
|
310
|
+
attribute(:analysis_type, base::SAFcodeType)
|
311
|
+
attribute(:analysis_type_description, base::SAFlongtextType)
|
312
|
+
attribute(:analysis_id, base::SAFmiddle1textType)
|
313
|
+
attribute(:analysis_id_description, base::SAFlongtextType)
|
314
|
+
attribute(:start_date, Types::Date.optional.meta(omittable: true))
|
315
|
+
attribute(:end_date, Types::Date.optional.meta(omittable: true))
|
316
|
+
attribute(:status, base::SAFmiddle1textType.optional.meta(omittable: true))
|
317
|
+
attribute(:analyses, Types::Array.of(base::AnalysisPartyInfoStructure).optional.meta(omittable: true))
|
318
|
+
end
|
319
|
+
|
320
|
+
base.const_set(:AnalysisTypeTableEntry, klass)
|
321
|
+
|
322
|
+
klass = Class.new(base::CompanyStructure) do
|
323
|
+
attribute(:owner_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
324
|
+
attribute(:account_id, base::SAFmiddle2textType.optional.meta(omittable: true))
|
325
|
+
end
|
326
|
+
|
327
|
+
base.const_set(:Owner, klass)
|
328
|
+
|
329
|
+
klass = Class.new(Dry::Struct) do
|
330
|
+
attribute(:general_ledger_accounts, Types::Array.of(base::Account).optional.meta(omittable: true))
|
331
|
+
# ignored since xsd said "Not in use."
|
332
|
+
# attribute :taxonomies, Taxonomies
|
333
|
+
attribute(:customers, Types::Array.of(base::Customer).optional.meta(omittable: true))
|
334
|
+
attribute(:suppliers, Types::Array.of(base::Supplier).optional.meta(omittable: true))
|
335
|
+
attribute(:tax_table, Types::Array.of(base::TaxTableEntry).optional.meta(omittable: true))
|
336
|
+
# ignored since xsd said "Not in use."
|
337
|
+
# attribute :uom_table, UOMTable
|
338
|
+
attribute(:analysis_type_table, Types::Array.of(base::AnalysisTypeTableEntry).optional.meta(omittable: true))
|
339
|
+
# ignored since xsd said "Not in use."
|
340
|
+
# attribute :movement_type_table, MovementTypeTable
|
341
|
+
# ignored since xsd said "Not in use."
|
342
|
+
# attribute :products, Products
|
343
|
+
# ignored since xsd said "Not in use."
|
344
|
+
# attribute :physical_stock, PhysicalStock
|
345
|
+
attribute(:owners, Types::Array.of(base::Owner).optional.meta(omittable: true))
|
346
|
+
# ignored since xsd said "Not in use."
|
347
|
+
# attribute :assets, Assets
|
348
|
+
end
|
349
|
+
|
350
|
+
base.const_set(:MasterFiles, klass)
|
351
|
+
|
352
|
+
klass = Class.new(Dry::Struct) do
|
353
|
+
attribute(:record_id, base::SAFshorttextType)
|
354
|
+
attribute(:account_id, base::SAFmiddle2textType)
|
355
|
+
attribute(:analyses, Types::Array.of(base::AnalysisStructure).optional.meta(omittable: true))
|
356
|
+
attribute(:value_date, Types::Date.optional.meta(omittable: true))
|
357
|
+
attribute(:source_document_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
358
|
+
attribute(:customer_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
359
|
+
attribute(:supplier_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
360
|
+
attribute(:description, base::SAFlongtextType)
|
361
|
+
attribute(:debit_amount, base::AmountStructure.optional.meta(omittable: true))
|
362
|
+
attribute(:credit_amount, base::AmountStructure.optional.meta(omittable: true))
|
363
|
+
attribute(:tax_information, Types::Array.of(base::TaxInformationStructure).optional.meta(omittable: true))
|
364
|
+
attribute(:reference_number, base::SAFmiddle1textType.optional.meta(omittable: true))
|
365
|
+
attribute(:cid, base::SAFmiddle1textType.optional.meta(omittable: true))
|
366
|
+
attribute(:due_date, Types::Date.optional.meta(omittable: true))
|
367
|
+
attribute(:quantity, base::SAFquantityType.optional.meta(omittable: true))
|
368
|
+
attribute(:cross_reference, base::SAFmiddle1textType.optional.meta(omittable: true))
|
369
|
+
attribute(:system_entry_time, Types::DateTime.optional.meta(omittable: true))
|
370
|
+
attribute(:owner_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
371
|
+
end
|
372
|
+
|
373
|
+
base.const_set(:Line, klass)
|
374
|
+
|
375
|
+
klass = Class.new(Dry::Struct) do
|
376
|
+
attribute(:transaction_id, base::SAFmiddle2textType)
|
377
|
+
attribute(:period, Types::Integer)
|
378
|
+
attribute(:period_year, Types::Integer)
|
379
|
+
attribute(:transaction_date, Types::Date)
|
380
|
+
attribute(:source_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
381
|
+
attribute(:transaction_type, base::SAFshorttextType.optional.meta(omittable: true))
|
382
|
+
attribute(:description, base::SAFlongtextType)
|
383
|
+
attribute(:batch_id, base::SAFmiddle1textType.optional.meta(omittable: true))
|
384
|
+
attribute(:system_entry_date, Types::Date)
|
385
|
+
attribute(:gl_posting_date, Types::Date)
|
386
|
+
# ignored since xsd said "Not in use."
|
387
|
+
# attribute :CustomerID, base::SAFmiddle1textType.optional.meta(omittable: true)
|
388
|
+
# ignored since xsd said "Not in use."
|
389
|
+
# attribute :SupplierID, base::SAFmiddle1textType.optional.meta(omittable: true)
|
390
|
+
attribute(:system_id, base::SAFshorttextType.optional.meta(omittable: true))
|
391
|
+
attribute(:lines, Types::Array.of(base::Line))
|
392
|
+
end
|
393
|
+
|
394
|
+
base.const_set(:Transaction, klass)
|
395
|
+
|
396
|
+
klass = Class.new(Dry::Struct) do
|
397
|
+
attribute(:journal_id, base::SAFshorttextType)
|
398
|
+
attribute(:description, base::SAFlongtextType)
|
399
|
+
attribute(:type, base::SAFcodeType)
|
400
|
+
attribute(:transactions, Types::Array.of(base::Transaction).optional.meta(omittable: true))
|
401
|
+
end
|
402
|
+
|
403
|
+
base.const_set(:Journal, klass)
|
404
|
+
|
405
|
+
klass = Class.new(Dry::Struct) do
|
406
|
+
attribute(:number_of_entries, Types::Integer)
|
407
|
+
attribute(:total_debit, base::SAFmonetaryType)
|
408
|
+
attribute(:total_credit, base::SAFmonetaryType)
|
409
|
+
attribute(:journals, Types::Array.of(base::Journal).optional.meta(omittable: true))
|
410
|
+
end
|
411
|
+
|
412
|
+
base.const_set(:GeneralLedgerEntries, klass)
|
413
|
+
|
414
|
+
klass = Class.new(Dry::Struct) do
|
415
|
+
attribute(:header, base::Header)
|
416
|
+
attribute(:master_files, base::MasterFiles.optional.meta(omittable: true))
|
417
|
+
attribute(:general_ledger_entries, base::GeneralLedgerEntries.optional.meta(omittable: true))
|
418
|
+
# ignored since xsd said "Not in use."
|
419
|
+
# attribute :source_documents, base::SourceDocuments
|
420
|
+
end
|
421
|
+
|
422
|
+
base.const_set(:AuditFile, klass)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
module Relaxed
|
428
|
+
ISOCurrencyCode = Types::String
|
429
|
+
ISOCountryCode = Types::String
|
430
|
+
SAFcodeType = Types::String
|
431
|
+
SAFshorttextType = Types::String
|
432
|
+
SAFmiddle1textType = Types::String
|
433
|
+
SAFmiddle2textType = Types::String
|
434
|
+
SAFlongtextType = Types::String
|
435
|
+
SAFmonetaryType = Types::Decimal
|
436
|
+
SAFexchangerateType = Types::Decimal
|
437
|
+
SAFquantityType = Types::Decimal
|
438
|
+
SAFweightType = Types::Decimal
|
439
|
+
AddressType = Types::String
|
440
|
+
|
441
|
+
extend ObjectStrutures
|
442
|
+
end
|
443
|
+
|
444
|
+
module Strict
|
445
|
+
ISOCurrencyCode = Types::String.constrained(size: 3)
|
446
|
+
ISOCountryCode = Types::String.constrained(size: 2)
|
447
|
+
SAFcodeType = Types::String.constrained(max_size: 9)
|
448
|
+
SAFshorttextType = Types::String.constrained(max_size: 18)
|
449
|
+
SAFmiddle1textType = Types::String.constrained(max_size: 35)
|
450
|
+
SAFmiddle2textType = Types::String.constrained(max_size: 70)
|
451
|
+
SAFlongtextType = Types::String.constrained(max_size: 256)
|
452
|
+
SAFmonetaryType = Types::Decimal.constrained(digits: 18, fraction_digitst: 2)
|
453
|
+
SAFexchangerateType = Types::Decimal.constrained(digits: 18, fraction_digitst: 8)
|
454
|
+
SAFquantityType = Types::Decimal.constrained(digits: 22, fraction_digitst: 6)
|
455
|
+
SAFweightType = Types::Decimal.constrained(digits: 14, fraction_digitst: 3)
|
456
|
+
AddressType = Types::String.enum("StreetAddress", "PostalAddress", "BillingAddress", "ShipToAddress", "ShipFromAddress")
|
457
|
+
|
458
|
+
extend ObjectStrutures
|
459
|
+
end
|
460
|
+
|
461
|
+
module Sliced
|
462
|
+
def self.slice_string(length)
|
463
|
+
proc do |value|
|
464
|
+
if value.is_a?(::String)
|
465
|
+
value.slice(0, length)
|
466
|
+
else
|
467
|
+
value
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
ISOCurrencyCode = Types.Constructor(Strict::ISOCurrencyCode, &slice_string(3))
|
473
|
+
ISOCountryCode = Types.Constructor(Strict::ISOCountryCode, &slice_string(2))
|
474
|
+
SAFcodeType = Types.Constructor(Strict::SAFcodeType, &slice_string(9))
|
475
|
+
SAFshorttextType = Types.Constructor(Strict::SAFshorttextType, &slice_string(18))
|
476
|
+
SAFmiddle1textType = Types.Constructor(Strict::SAFmiddle1textType, &slice_string(35))
|
477
|
+
SAFmiddle2textType = Types.Constructor(Strict::SAFmiddle2textType, &slice_string(70))
|
478
|
+
SAFlongtextType = Types.Constructor(Strict::SAFlongtextType, &slice_string(256))
|
479
|
+
SAFmonetaryType = Types::Decimal.constrained(digits: 18, fraction_digitst: 2)
|
480
|
+
SAFexchangerateType = Types::Decimal.constrained(digits: 18, fraction_digitst: 8)
|
481
|
+
SAFquantityType = Types::Decimal.constrained(digits: 22, fraction_digitst: 6)
|
482
|
+
SAFweightType = Types::Decimal.constrained(digits: 14, fraction_digitst: 3)
|
483
|
+
|
484
|
+
AddressType = Types::String.enum("StreetAddress", "PostalAddress", "BillingAddress", "ShipToAddress", "ShipFromAddress")
|
485
|
+
|
486
|
+
extend ObjectStrutures
|
487
|
+
end
|
488
|
+
|
489
|
+
# Want to keep developer experience where you can call
|
490
|
+
# SAFT::V2::Types::AuditFile and get a Audit file. We have three different
|
491
|
+
# AuditFile. We have Strict/Relaxed/Sliced.
|
492
|
+
include Strict
|
493
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
XSD_PATH = Pathname.new(__dir__) + ".." + ".." + ".." + "vendor" + "SAF-T_Financial_Schema_NO_1.10.xsd"
|
7
|
+
|
8
|
+
module SAFT::V2
|
9
|
+
class XsdValidate
|
10
|
+
def initialize(content)
|
11
|
+
@xml_errors = []
|
12
|
+
doc = Nokogiri::XML(content)
|
13
|
+
@xml_errors.push(*doc.errors)
|
14
|
+
xsd = Nokogiri::XML::Schema(XSD_PATH)
|
15
|
+
@xml_errors.push(*xsd.validate(doc))
|
16
|
+
# All error are of type Nokogiri::XML::SyntaxError
|
17
|
+
# https://nokogiri.org/rdoc/Nokogiri/XML/SyntaxError.html
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
@xml_errors.none?
|
22
|
+
end
|
23
|
+
|
24
|
+
def errors
|
25
|
+
@xml_errors
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/saft/v2.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
require "dry-struct"
|
5
|
+
|
6
|
+
module SAFT::V2
|
7
|
+
module Types
|
8
|
+
include Dry.Types
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.parse(xml_content)
|
12
|
+
doc = Nokogiri::XML(xml_content)
|
13
|
+
doc.remove_namespaces!
|
14
|
+
Parse.call(doc).then { Types::AuditFile.call(_1) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.scribe(audit_file)
|
18
|
+
rule = Types.Instance(Types::Relaxed::AuditFile) |
|
19
|
+
Types.Instance(Types::Strict::AuditFile) |
|
20
|
+
Types.Instance(Types::Sliced::AuditFile)
|
21
|
+
|
22
|
+
raise ArgumentError unless rule.valid?(audit_file)
|
23
|
+
|
24
|
+
Scribe.write_xml(audit_file)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.validate(xml_content)
|
28
|
+
XsdValidate.new(xml_content)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.to_html(audit_file)
|
32
|
+
raise ArgumentError unless audit_file.is_a?(Types::AuditFile)
|
33
|
+
|
34
|
+
HTML.render(audit_file)
|
35
|
+
end
|
36
|
+
end
|
data/lib/saft/version.rb
ADDED
data/lib/saft.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zeitwerk"
|
4
|
+
require "nokogiri"
|
5
|
+
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.inflector.inflect("saft" => "SAFT", "html" => "HTML")
|
8
|
+
loader.setup
|
9
|
+
|
10
|
+
loader.do_not_eager_load("#{__dir__}/saft/v2/html.rb")
|
11
|
+
|
12
|
+
module SAFT
|
13
|
+
def self.gem_root
|
14
|
+
Pathname(__dir__).parent
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.nokogiri_save_setting=(value)
|
18
|
+
@nokogiri_save_setting = value
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.nokogiri_save_setting
|
22
|
+
return @nokogiri_save_setting if defined?(@nokogiri_save_setting)
|
23
|
+
|
24
|
+
@nokogiri_save_setting = Nokogiri::XML::Node::SaveOptions::AS_XML
|
25
|
+
end
|
26
|
+
end
|
data/package.json
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
{
|
2
|
+
"name": "saft_gem",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "",
|
5
|
+
"main": "index.js",
|
6
|
+
"scripts": {
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
8
|
+
},
|
9
|
+
"keywords": [],
|
10
|
+
"author": "",
|
11
|
+
"license": "ISC",
|
12
|
+
"dependencies": {
|
13
|
+
"tailwindcss": "^3.2.4"
|
14
|
+
}
|
15
|
+
}
|