saft 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,685 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+ require "tubby"
5
+
6
+ module SAFT::V2
7
+ module HTML
8
+ def self.css
9
+ File.read(css_path)
10
+ end
11
+
12
+ def self.css_path
13
+ Pathname.new(__dir__) + "html_dist.css"
14
+ end
15
+
16
+ def self.format_big_decimal(big_decimal)
17
+ integer, decimal = big_decimal.to_s("F").split(".")
18
+ integer = integer.reverse.scan(/.{1,3}/).join(" ").reverse
19
+
20
+ "#{integer},#{decimal.ljust(2, "0")}"
21
+ end
22
+
23
+ module DryStructRenderTubby
24
+ refine(String) do
25
+ def to_tubby
26
+ self
27
+ end
28
+ end
29
+
30
+ refine(Date) do
31
+ def to_tubby
32
+ to_s
33
+ end
34
+ end
35
+
36
+ refine(NilClass) do
37
+ def to_tubby
38
+ "-"
39
+ end
40
+ end
41
+
42
+ refine(DateTime) do
43
+ def to_tubby
44
+ to_s
45
+ end
46
+ end
47
+
48
+ refine(Integer) do
49
+ def to_tubby
50
+ to_s
51
+ end
52
+ end
53
+
54
+ refine(BigDecimal) do
55
+ def to_tubby
56
+ HTML.format_big_decimal(self)
57
+ end
58
+ end
59
+
60
+ refine(Array) do
61
+ def to_tubby
62
+ Tubby.new { |t| each { t << _1 } }
63
+ end
64
+ end
65
+
66
+ refine(Dry::Struct) do
67
+ def to_tubby
68
+ Tubby.new { |t| t.div(class: "mb-2 pl-2 border-l-2") { t << RenderHash.new(attributes) } }
69
+ end
70
+ end
71
+
72
+ refine(SAFT::V2::Types::AuditFile) do
73
+ def to_tubby
74
+ Tubby.new { |t|
75
+ t.div(class: "pl-2 border-l-2") {
76
+ t.div(class: "mb-2 border-b-2") { t.strong("Header") }
77
+ t << RenderHash.new(header.attributes)
78
+ }
79
+
80
+ if master_files
81
+ t.div(class: "pl-2 border-l-2") {
82
+ t.div(class: "mb-2 border-b-2") { t.strong("MasterFiles") }
83
+ t << master_files
84
+ }
85
+ end
86
+
87
+ if general_ledger_entries
88
+ t.div(class: "pl-2 border-l-2") {
89
+ t.div(class: "mb-2 border-b-2") { t.strong("GeneralLedgerEntries") }
90
+ t << general_ledger_entries
91
+ }
92
+ end
93
+ }
94
+ end
95
+ end
96
+
97
+ refine(SAFT::V2::Types::MasterFiles) do
98
+ def to_tubby
99
+ Tubby.new { |t|
100
+ if general_ledger_accounts
101
+ t.strong("General ledger accounts")
102
+ t.div(class: "pl-2 border-l-2") { t << RenderGeneralLedgerTable.new(general_ledger_accounts) }
103
+ end
104
+
105
+ if customers
106
+ t.strong("Customers")
107
+ t.div(class: "pl-2 border-l-2 flex flex-wrap") {
108
+ customers.each do |customer|
109
+ t << CompanyCard.new(customer)
110
+ end
111
+ }
112
+ end
113
+
114
+ if suppliers
115
+ t.strong("Suppliers")
116
+ t.div(class: "pl-2 border-l-2 flex flex-wrap") {
117
+ suppliers.each do |supplier|
118
+ t << CompanyCard.new(supplier)
119
+ end
120
+ }
121
+ end
122
+
123
+ if tax_table
124
+ t.strong("Tax able")
125
+ t.div(class: "pl-2 border-l-2") { t << TaxTable.new(tax_table) }
126
+ end
127
+
128
+ if analysis_type_table
129
+ t.strong("Analysis type table")
130
+ t.div(class: "pl-2 border-l-2") { t << AnalysisTypeTable.new(analysis_type_table) }
131
+ end
132
+
133
+ if owners
134
+ t.strong("Owners")
135
+ t.div(class: "pl-2 border-l-2") { owners.each { |owner| t << owner } }
136
+ end
137
+ }
138
+ end
139
+ end
140
+
141
+ refine(SAFT::V2::Types::Transaction) do
142
+ def to_tubby
143
+ Tubby.new { |t|
144
+ t.div(
145
+ id: "transaction-#{transaction_id}",
146
+ class: "mb-2 pl-2 border-l-2 flex",
147
+ ) {
148
+ t.div(class: "w-80") {
149
+ t
150
+ .a(
151
+ class: "whitespace-pre underline underline-offset-1 hover:underline-offset-2 visited:underline-decoration-2",
152
+ href: "#transaction-#{transaction_id}",
153
+ ) {
154
+ t.div {
155
+ t.strong("Transaction id ")
156
+ t << transaction_id
157
+ }
158
+ }
159
+ t.div { t << RenderHash.new(attributes.except(:transaction_id, :lines)) }
160
+ }
161
+ t.div {
162
+ t.strong("Lines ")
163
+ t << LinesTable.new(lines)
164
+ }
165
+ }
166
+ }
167
+ end
168
+ end
169
+ end
170
+
171
+ using DryStructRenderTubby
172
+
173
+ class RenderHash
174
+ def initialize(hash)
175
+ @hash = hash.select { |_, value| value }
176
+ end
177
+
178
+ def to_tubby
179
+ Tubby.new { |t|
180
+ @hash.each do |key, value|
181
+ t.div {
182
+ t.strong("#{key.to_s.tr("_", " ").capitalize} ")
183
+ t << value
184
+ }
185
+ end
186
+ }
187
+ end
188
+ end
189
+
190
+ class RenderGeneralLedgerTable
191
+ def initialize(accounts)
192
+ @accounts = accounts
193
+ end
194
+
195
+ def to_tubby
196
+ Tubby.new { |t|
197
+ t.table {
198
+ t.thead {
199
+ t.tr {
200
+ t.th("Id")
201
+ t.th("Description")
202
+ t.th("Std account")
203
+ t.th("Opening balance")
204
+ t.th("Closing balance")
205
+ t.th("Rest")
206
+ }
207
+ }
208
+ t.tbody {
209
+ @accounts.each do |account|
210
+ std_account = SAFT::V2::Norway.std_account(account.standard_account_id)
211
+ std_account_title = "Not found"
212
+ if std_account
213
+ std_account_title = <<~TEXT
214
+ Account no #{std_account.number}
215
+ #{std_account.description_en}
216
+ #{std_account.description_no}
217
+ TEXT
218
+ end
219
+
220
+ t.tr {
221
+ t.td(account.account_id)
222
+ t.td(account.account_description)
223
+ t.td(account.standard_account_id, title: std_account_title)
224
+ t.td {
225
+ t.div(class: "flex justify-between") {
226
+ if account.opening_debit_balance
227
+ t.span("Debit")
228
+ t.span(account.opening_debit_balance)
229
+ end
230
+
231
+ if account.opening_credit_balance
232
+ t.span("Credit")
233
+ t.span(-account.opening_credit_balance)
234
+ end
235
+ }
236
+ }
237
+ t.td {
238
+ t.div(class: "flex justify-between") {
239
+ if account.closing_debit_balance
240
+ t.span("Debit")
241
+ t.span(account.closing_debit_balance)
242
+ end
243
+
244
+ if account.closing_credit_balance
245
+ t.span("Credit")
246
+ t.span(-account.closing_credit_balance)
247
+ end
248
+ }
249
+ }
250
+ t.td(class: "pl-2") {
251
+ t.div(class: "pl-2 border-l-2") {
252
+ t <<
253
+ RenderHash.new(
254
+ account
255
+ .attributes
256
+ .except(
257
+ :account_id,
258
+ :account_description,
259
+ :standard_account_id,
260
+ :opening_debit_balance,
261
+ :opening_credit_balance,
262
+ :closing_debit_balance,
263
+ :closing_credit_balance,
264
+ ),
265
+ )
266
+ }
267
+ }
268
+ }
269
+ end
270
+ }
271
+ }
272
+ }
273
+ end
274
+ end
275
+
276
+ class CompanyCard
277
+ def initialize(company)
278
+ @company = company
279
+ end
280
+
281
+ def to_tubby
282
+ Tubby.new { |t|
283
+ t.div(class: "min-w-[20rem] max-w-[20rem] mr-8 mb-2") {
284
+ t.div(class: "pl-2 border-l-2") {
285
+ t.span("Supplier", class: "font-semibold") if @company.is_a?(Types::Supplier)
286
+ t.span("Customer", class: "font-semibold") if @company.is_a?(Types::Customer)
287
+ t << RenderHash.new(@company.attributes)
288
+ }
289
+ }
290
+ }
291
+ end
292
+ end
293
+
294
+ class TaxTable
295
+ def initialize(tax_table)
296
+ @tax_table = tax_table
297
+ end
298
+
299
+ def to_tubby
300
+ Tubby.new { |t|
301
+ t.table {
302
+ t.thead {
303
+ t.tr {
304
+ t.th("Tax code")
305
+ t.th("Description")
306
+ t.th("Country")
307
+ t.th("Std code")
308
+ t.th("Tax %", class: "text-right")
309
+ t.th("Base rate", class: "text-right")
310
+ t.th("rest")
311
+ }
312
+ }
313
+ t.tbody {
314
+ @tax_table.each { |table|
315
+ table.tax_code_details.each { |detail|
316
+ vat_code = SAFT::V2::Norway.vat_code(detail.standard_tax_code)
317
+ vat_code_title = "Not found"
318
+ if vat_code
319
+ vat_code_title = <<~TEXT
320
+ Vat Code #{vat_code.code}
321
+ #{vat_code.description_en}
322
+ #{vat_code.description_no}
323
+ #{vat_code.tax_rate}
324
+ #{"Can be used for compensation" if vat_code.compensation}
325
+ TEXT
326
+ end
327
+
328
+ t.tr {
329
+ t.td(detail.tax_code)
330
+ t.td(detail.description)
331
+ t.td(detail.country)
332
+ t.td(detail.standard_tax_code, title: vat_code_title)
333
+ t.td(detail.tax_percentage, class: "text-right")
334
+ t.td(class: "text-right") { detail.base_rates.each { t.div(_1) } }
335
+ t.td(class: "pl-2") {
336
+ t.div(class: "pl-2 border-l-2") {
337
+ t <<
338
+ RenderHash.new(
339
+ detail
340
+ .attributes
341
+ .except(
342
+ :tax_code,
343
+ :description,
344
+ :country,
345
+ :standard_tax_code,
346
+ :base_rate,
347
+ :tax_percentage,
348
+ ),
349
+ )
350
+ }
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ end
359
+ end
360
+
361
+ class AnalysisTypeTable
362
+ def initialize(analysis_type_table)
363
+ @analysis_type_table = analysis_type_table
364
+ end
365
+
366
+ def to_tubby
367
+ Tubby.new { |t|
368
+ t.table {
369
+ t.thead {
370
+ t.tr {
371
+ t.th("Type")
372
+ t.th("Type description")
373
+ t.th("ID")
374
+ t.th("ID Description")
375
+ t.th("Rest")
376
+ }
377
+ }
378
+ t.tbody {
379
+ @analysis_type_table.each { |entry|
380
+ html_analysis = t.get_analysis(entry.analysis_id, entry.analysis_type)
381
+ t.tr(id: html_analysis.html_id) {
382
+ t.td(entry.analysis_type)
383
+ t.td(entry.analysis_type_description)
384
+ t.td(entry.analysis_id)
385
+ t.td(entry.analysis_id_description)
386
+ t.td(class: "pl-2") {
387
+ t.div(class: "pl-2 border-l-2") {
388
+ t <<
389
+ RenderHash.new(
390
+ entry
391
+ .attributes
392
+ .except(
393
+ :analysis_type,
394
+ :analysis_type_description,
395
+ :analysis_id,
396
+ :analysis_id_description,
397
+ ),
398
+ )
399
+ }
400
+ }
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
406
+ end
407
+ end
408
+
409
+ class LinesTable
410
+ def initialize(lines)
411
+ @lines = lines
412
+ end
413
+
414
+ def to_tubby
415
+ Tubby.new { |t|
416
+ t.table {
417
+ t.thead {
418
+ t.tr {
419
+ t.th("RecordID")
420
+ t.th("AccountID")
421
+ t.th("Analysis")
422
+ t.th("ValueDate")
423
+ t.th("Description")
424
+ t.th("Dedit Amount", class: "text-right")
425
+ t.th("Credit Amount", class: "text-right")
426
+ t.th("Rest")
427
+ }
428
+ }
429
+
430
+ t.tbody {
431
+ @lines.each { |line|
432
+ t.tr {
433
+ t.td(line.record_id)
434
+ t.td {
435
+ account = t.get_account(line.account_id)
436
+ t.div(title: account.title) { t << line.account_id }
437
+ }
438
+
439
+ t.td {
440
+ line.analyses&.each do |line_analysis|
441
+ analysis = t.get_analysis(
442
+ line_analysis.analysis_id,
443
+ line_analysis.analysis_type,
444
+ )
445
+ t.div(title: analysis.title) { t << analysis.link { t << "#{line_analysis.analysis_type} #{line_analysis.analysis_id}" } }
446
+ end
447
+ }
448
+ t.td(line.value_date)
449
+ t.td(line.description)
450
+ t.td(line.debit_amount&.amount, class: "text-right")
451
+ t.td(line.credit_amount&.amount, class: "text-right")
452
+ t.td {
453
+ t.div(class: "mb-2 pl-2 border-l-2") {
454
+ if line.customer_id
455
+ customer = t.get_customer(line.customer_id)
456
+ t.div(title: customer.title) {
457
+ t.strong("Customer ")
458
+ t << line.customer_id
459
+ t << " #{customer.name}"
460
+ }
461
+ end
462
+
463
+ if line.supplier_id
464
+ supplier = t.get_supplier(line.supplier_id)
465
+ t.div(title: supplier.title) {
466
+ t.strong("Supplier ")
467
+ t << line.supplier_id
468
+ t << " #{supplier.name}"
469
+ }
470
+ end
471
+
472
+ t <<
473
+ RenderHash.new(
474
+ line
475
+ .attributes
476
+ .except(
477
+ :record_id,
478
+ :account_id,
479
+ :customer_id,
480
+ :supplier_id,
481
+ :analyses,
482
+ :value_date,
483
+ :description,
484
+ :debit_amount,
485
+ :credit_amount,
486
+ ),
487
+ )
488
+ }
489
+ }
490
+ }
491
+ }
492
+ }
493
+ }
494
+ }
495
+ end
496
+ end
497
+
498
+ class Analysis
499
+ def initialize(analysis)
500
+ @analysis = analysis
501
+ end
502
+
503
+ attr_reader :analysis
504
+
505
+ def title
506
+ <<~TEXT
507
+ #{analysis.analysis_type}(#{analysis.analysis_type_description})
508
+ #{analysis.analysis_id}(#{analysis.analysis_id_description})
509
+ TEXT
510
+ end
511
+
512
+ def html_id
513
+ "analysis-#{analysis.analysis_type}-#{analysis.analysis_id}"
514
+ end
515
+
516
+ def link
517
+ Tubby.new { |t| t.a(href: "##{html_id}") { yield } }
518
+ end
519
+ end
520
+
521
+ class NotFoundAnalysys
522
+ include Singleton
523
+
524
+ def title
525
+ "Could not find analysis"
526
+ end
527
+
528
+ def link
529
+ yield
530
+ nil
531
+ end
532
+ end
533
+
534
+ class Account
535
+ attr_reader(:account)
536
+
537
+ def initialize(account)
538
+ @account = account
539
+ end
540
+
541
+ def title
542
+ <<~TEXT
543
+ #{account.account_id} #{account.account_description}
544
+ Std account #{account.standard_account_id}
545
+ opening balance #{HTML.format_big_decimal(account.opening_debit_balance || -account.opening_credit_balance)}
546
+ closing balance #{HTML.format_big_decimal(account.closing_debit_balance || -account.closing_credit_balance)}
547
+ TEXT
548
+ end
549
+ end
550
+
551
+ class NotFoundAccount
552
+ include(Singleton)
553
+
554
+ def title
555
+ "Could not find account"
556
+ end
557
+ end
558
+
559
+ class Customer
560
+ attr_reader(:customer)
561
+
562
+ def initialize(customer)
563
+ @customer = customer
564
+ end
565
+
566
+ def title
567
+ <<~TEXT
568
+ #{customer.name} #{customer.registration_number}
569
+ opening balance #{HTML.format_big_decimal(customer.opening_debit_balance || -customer.opening_credit_balance)}
570
+ closing balance #{HTML.format_big_decimal(customer.closing_debit_balance || -customer.closing_credit_balance)}
571
+ TEXT
572
+ end
573
+
574
+ def name
575
+ customer.name
576
+ end
577
+ end
578
+
579
+ class NotFoundCustomer
580
+ include(Singleton)
581
+
582
+ def title
583
+ "Could not find customer"
584
+ end
585
+
586
+ def name
587
+ "Not found in Customers block"
588
+ end
589
+ end
590
+
591
+ class Supplier
592
+ attr_reader(:supplier)
593
+
594
+ def initialize(supplier)
595
+ @supplier = supplier
596
+ end
597
+
598
+ def title
599
+ <<~TEXT
600
+ #{supplier.name} #{supplier.registration_number}
601
+ opening balance #{HTML.format_big_decimal(supplier.opening_debit_balance || -supplier.opening_credit_balance)}
602
+ closing balance #{HTML.format_big_decimal(supplier.closing_debit_balance || -supplier.closing_credit_balance)}
603
+ TEXT
604
+ end
605
+
606
+ def name
607
+ supplier.name
608
+ end
609
+ end
610
+
611
+ class NotFoundSupplier
612
+ include(Singleton)
613
+
614
+ def title
615
+ "Could not find supplier"
616
+ end
617
+
618
+ def name
619
+ "Not found in Suppliers block"
620
+ end
621
+ end
622
+
623
+ class SaftRenderer < Tubby::Renderer
624
+ def <<(obj)
625
+ obj = obj.to_tubby if obj.respond_to?(:to_tubby)
626
+ if obj.is_a?(Tubby::Template)
627
+ obj.render_with(self)
628
+ else
629
+ @target << CGI.escape_html(obj.to_s)
630
+ end
631
+
632
+ self
633
+ end
634
+
635
+ def audit_file=(audit_file)
636
+ (audit_file.master_files&.analysis_type_table || [])
637
+ .each_with_object({}) { _2[[_1.analysis_id, _1.analysis_type]] = Analysis.new(_1) }
638
+ .tap { @analysis_lookup = _1 }
639
+
640
+ (audit_file.master_files&.customers || [])
641
+ .each_with_object({}) { _2[_1.customer_id] = Customer.new(_1) }
642
+ .tap { @customer_lookup = _1 }
643
+
644
+ (audit_file.master_files&.suppliers || [])
645
+ .each_with_object({}) { _2[_1.supplier_id] = Supplier.new(_1) }
646
+ .tap { @supplier_lookup = _1 }
647
+
648
+ (audit_file.master_files&.general_ledger_accounts || [])
649
+ .each_with_object({}) { _2[_1.account_id] = Account.new(_1) }
650
+ .tap { @account_lookup = _1 }
651
+ end
652
+
653
+ def get_analysis(id, type)
654
+ @analysis_lookup.fetch([id, type]) { NotFoundAnalysys.instance }
655
+ end
656
+
657
+ def get_account(id)
658
+ @account_lookup.fetch(id) { NotFoundAccount.instance }
659
+ end
660
+
661
+ def get_customer(id)
662
+ @customer_lookup.fetch(id) { NotFoundCustomer.instance }
663
+ end
664
+
665
+ def get_supplier(id)
666
+ @supplier_lookup.fetch(id) { NotFoundSupplier.instance }
667
+ end
668
+
669
+ def a(*args, **kwargs, &block)
670
+ kwargs[:class] ||= ""
671
+ kwargs[:class] += " whitespace-pre underline underline-offset-1 hover:underline-offset-2 visited:underline-decoration-2"
672
+ super(*args, **kwargs, &block)
673
+ end
674
+ end
675
+
676
+ def self.render(audit_file)
677
+ target = +""
678
+ renderer = SaftRenderer.new(target)
679
+ renderer.audit_file = audit_file
680
+ renderer << audit_file
681
+
682
+ Tubby.new { |t| t.raw!(target) }
683
+ end
684
+ end
685
+ end