africompta 1.9.8

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.
@@ -0,0 +1,995 @@
1
+ require 'prawn'
2
+ require 'prawn/measurement_extensions'
3
+
4
+ class AccountRoot
5
+
6
+ def self.actual
7
+ self.accounts.find { |a| a.name == 'Root' }
8
+ end
9
+
10
+ def self.current
11
+ AccountRoot.actual
12
+ end
13
+
14
+ def self.archive
15
+ self.accounts.find { |a| a.name == 'Archive' }
16
+ end
17
+
18
+ def self.accounts
19
+ Accounts.matches_by_account_id(0)
20
+ end
21
+
22
+ def self.clean
23
+ count_mov, count_acc = 0, 0
24
+ bad_mov, bad_acc = 0, 0
25
+ log_msg 'Account.clean', 'starting to clean up'
26
+ Movements.search_all_.each { |m|
27
+ dputs(4) { "Testing movement #{m.inspect}" }
28
+ if not m or not m.date or not m.desc or not m.value or
29
+ not m.rev_index or not m.account_src or not m.account_dst
30
+ if m and m.desc
31
+ log_msg 'Account.clean', "Bad movement: #{m.inspect}"
32
+ end
33
+ m.delete
34
+ bad_mov += 1
35
+ end
36
+ if m.rev_index
37
+ count_mov = [count_mov, m.rev_index].max
38
+ end
39
+ }
40
+ Accounts.search_all_.each { |a|
41
+ if (a.account_id and (a.account_id > 0)) and (not a.account)
42
+ log_msg 'Account.clean', "Account has unexistent parent: #{a.inspect}"
43
+ a.delete
44
+ bad_acc += 1
45
+ end
46
+ if !(a.account_id or a.deleted)
47
+ log_msg 'Account.clean', "Account has undefined parent: #{a.inspect}"
48
+ a.delete
49
+ bad_acc += 1
50
+ end
51
+ if a.account_id == 0
52
+ if !((a.name =~ /(Root|Archive)/) or a.deleted)
53
+ log_msg 'Account.clean', 'Account is in root but neither ' +
54
+ "'Root' nor 'Archive': #{a.inspect}"
55
+ a.delete
56
+ bad_acc += 1
57
+ end
58
+ end
59
+ if !a.rev_index
60
+ log_msg 'Account.clean', "Didn't find rev_index for #{a.inspect}"
61
+ a.new_index
62
+ bad_acc += 1
63
+ end
64
+ count_acc = [count_acc, a.rev_index].max
65
+ }
66
+
67
+ # Check also whether our counters are OK
68
+ u_l = Users.match_by_name('local')
69
+ dputs(1) { "Movements-index: #{count_mov} - #{u_l.movement_index}" }
70
+ dputs(1) { "Accounts-index: #{count_acc} - #{u_l.account_index}" }
71
+ @ul_mov, @ul_acc = u_l.movement_index, u_l.account_index
72
+ if count_mov > u_l.movement_index
73
+ log_msg 'Account.clean', 'Error, there is a bigger movement! Fixing'
74
+ u_l.movement_index = count_mov + 1
75
+ end
76
+ if count_acc > u_l.account_index
77
+ log_msg 'Account.clean', 'Error, there is a bigger account! Fixing'
78
+ u_l.account_index = count_acc + 1
79
+ end
80
+ return [count_mov, bad_mov, count_acc, bad_acc]
81
+ end
82
+
83
+ def self.path_id
84
+ return ''
85
+ end
86
+
87
+ def self.id
88
+ 0
89
+ end
90
+
91
+ def self.mult
92
+ 1
93
+ end
94
+
95
+ def self.keep_total
96
+ false
97
+ end
98
+
99
+ def self.multiplier
100
+ 1
101
+ end
102
+ end
103
+
104
+ class Accounts < Entities
105
+ self.needs %w(Users Movements)
106
+
107
+ attr_reader :check_state, :check_progress
108
+
109
+ def setup_data
110
+ @default_type = :SQLiteAC
111
+ @data_field_id = :id
112
+ value_int :index
113
+
114
+ value_str :name
115
+ value_str :desc
116
+ value_str :global_id
117
+ value_float :total
118
+ value_int :multiplier
119
+ value_int :rev_index
120
+ value_bool :deleted
121
+ value_bool :keep_total
122
+ # This is the ID of the parent account
123
+ value_int :account_id
124
+ end
125
+
126
+ def self.create(name, desc = 'Too lazy', parent = nil, global_id = '', mult = nil)
127
+ dputs(5) { "Parent is #{parent.inspect}" }
128
+ if parent
129
+ if parent.class != Account and parent != AccountRoot
130
+ parent = Accounts.matches_by_id(parent).first
131
+ end
132
+ mult ||= parent.multiplier
133
+ a = super(:name => name, :desc => desc, :account_id => parent.id,
134
+ :global_id => global_id.to_s, :multiplier => mult,
135
+ :deleted => false, :keep_total => parent.keep_total)
136
+ else
137
+ mult ||= 1
138
+ a = super(:name => name, :desc => desc, :account_id => 0,
139
+ :global_id => global_id.to_s, :multiplier => mult,
140
+ :deleted => false, :keep_total => false)
141
+ end
142
+ a.total = 0
143
+ if global_id == ''
144
+ a.global_id = Users.match_by_name('local').full + '-' + a.id.to_s
145
+ end
146
+ a.new_index
147
+ dputs(2) { "Created account #{a.path_id} - #{a.inspect}" }
148
+ a
149
+ end
150
+
151
+ def self.create_path(path, desc = '', double_last = false, mult = 1,
152
+ keep_total = false)
153
+ dputs(3) { "Path: #{path.inspect}, mult: #{mult}" }
154
+ elements = path.split('::')
155
+ parent = AccountRoot
156
+ while elements.size > 0
157
+ name = elements.shift
158
+ dputs(4) { "Working on element #{name} with base of #{parent.path_id}" }
159
+ child = parent.accounts.find { |a|
160
+ dputs(5) { "Searching child #{a.name} - #{a.path_id}" }
161
+ a.name == name
162
+ }
163
+ child and dputs(4) { "Found existing child #{child.path_id}" }
164
+ if (not child) or (elements.size == 0 and double_last)
165
+ dputs(4) { "Creating child #{name}" }
166
+ child = Accounts.create(name, desc, parent)
167
+ end
168
+ parent = child
169
+ end
170
+ parent.set_nochildmult(name, desc, nil, mult, [], keep_total)
171
+ end
172
+
173
+ # Gets an account from a string, if it doesn't exist yet, creates it.
174
+ # It will update it anyway.
175
+ def self.from_s(str)
176
+ str.force_encoding(Encoding::UTF_8)
177
+ desc, str = str.split("\r")
178
+ if not str
179
+ dputs(0) { "Error: Invalid account found: #{desc}" }
180
+ return [-1, nil]
181
+ end
182
+ global_id, total, name, multiplier, par,
183
+ deleted_s, keep_total_s = str.split("\t")
184
+ total, multiplier = total.to_f, multiplier.to_f
185
+ deleted = deleted_s == 'true'
186
+ keep_total = keep_total_s == 'true'
187
+ dputs(3) { [global_id, total, name, multiplier].inspect }
188
+ dputs(3) { [par, deleted_s, keep_total_s].inspect }
189
+ dputs(5) { "deleted, keep_total is #{deleted.inspect}, #{keep_total.inspect}" }
190
+ dputs(3) { 'Here comes the account: ' + global_id.to_s }
191
+ dputs(3) { "global_id: #{global_id}" }
192
+
193
+ if par.to_s.length > 0
194
+ parent = Accounts.match_by_global_id(par)
195
+ parent_id = parent.id
196
+ dputs(3) { "Parent is #{parent.inspect}" }
197
+ else
198
+ parent = nil
199
+ parent_id = 0
200
+ end
201
+
202
+ # Does the account already exist?
203
+ our_a = nil
204
+ if not (our_a = Accounts.match_by_global_id(global_id))
205
+ # Create it
206
+ dputs(3) { "Creating account #{name} - #{desc} - #{parent} - #{global_id}" }
207
+ our_a = Accounts.create(name, desc, parent, global_id)
208
+ end
209
+ # And update it
210
+ our_a.deleted = deleted
211
+ our_a.set_nochildmult(name, desc, parent_id, multiplier, [], keep_total)
212
+ our_a.global_id = global_id
213
+ dputs(2) { "Saved account #{name} with index #{our_a.rev_index} and global_id #{our_a.global_id}" }
214
+ dputs(4) { "Account is now #{our_a.inspect}" }
215
+ return our_a
216
+ end
217
+
218
+ def self.get_by_path_or_create(p, desc = '', last = false, mult = 1, keep = false)
219
+ get_by_path(p) or
220
+ create_path(p, desc, last, mult, keep)
221
+ end
222
+
223
+ def self.get_by_path(parent, elements = nil)
224
+ if not elements
225
+ if parent
226
+ return get_by_path(AccountRoot, parent.split('::'))
227
+ else
228
+ return nil
229
+ end
230
+ end
231
+
232
+ child = elements.shift
233
+ parent.accounts.each { |a|
234
+ if a.name == child
235
+ if elements.length > 0
236
+ return get_by_path(a, elements)
237
+ else
238
+ return a
239
+ end
240
+ end
241
+ }
242
+ return nil
243
+ end
244
+
245
+ def self.find_by_path(parent)
246
+ return Accounts.get_by_path(parent)
247
+ end
248
+
249
+ def self.get_id_by_path(p)
250
+ if a = get_by_path(p)
251
+ return a.id.to_s
252
+ else
253
+ return nil
254
+ end
255
+ end
256
+
257
+ def archive_parent(acc, years_archived, year)
258
+ dputs(3) { "years_archived is #{years_archived.inspect}" }
259
+ if not years_archived.has_key? year
260
+ dputs(2) { "Adding #{year}" }
261
+ years_archived[year] =
262
+ Accounts.create_path("Archive::#{year}", 'New archive')
263
+ end
264
+ # This means we're more than one level below root, so we can't
265
+ # just copy easily
266
+ if acc.path.split('::').count > 2
267
+ dputs(3) { "Creating archive #{acc.path} with mult #{acc.multiplier}" }
268
+ return Accounts.create_path("Archive::#{year}::"+
269
+ "#{acc.parent.path.gsub(/^Root::/, '')}", 'New archive', false,
270
+ acc.multiplier, acc.keep_total)
271
+ else
272
+ return years_archived[year]
273
+ end
274
+ end
275
+
276
+ def search_account(acc, month_start)
277
+ years = Hash.new(0)
278
+ acc.movements.each { |mov|
279
+ if not mov.desc =~ /^-- Sum of/
280
+ y, m, d = mov.date.to_s.split('-').collect { |d| d.to_i }
281
+ dputs(5) { "Date of #{mov.desc} is #{mov.date}" }
282
+ m < month_start and y -= 1
283
+ years[y] += 1
284
+ end
285
+ }
286
+ dputs(3) { "years is #{years.inspect}" }
287
+ years
288
+ end
289
+
290
+ def create_accounts(acc, years, years_archived, this_year)
291
+ years.keys.each { |y|
292
+ if y == this_year
293
+ dputs(3) { "Creating path #{acc.path} with mult #{acc.multiplier}" }
294
+ years[y] = Accounts.create_path(acc.path, acc.desc,
295
+ true, acc.multiplier, acc.keep_total)
296
+ else
297
+ path = "#{archive_parent(acc, years_archived, y).path}::" +
298
+ acc.name
299
+ dputs(3) { "Creating other path #{path} with mult #{acc.multiplier}" }
300
+ years[y] = Accounts.create_path(path, acc.desc, false,
301
+ acc.multiplier, acc.keep_total)
302
+ end
303
+ dputs(3) { "years[y] is #{years[y].path_id}" }
304
+ }
305
+ end
306
+
307
+ def move_movements(acc, years, month_start)
308
+ acc.movements.each { |mov|
309
+ dputs(5) { 'Start of each' }
310
+ y, m, d = mov.date.to_s.split('-').collect { |d| d.to_i }
311
+ dputs(5) { "Date of #{mov.desc} is #{mov.date}" }
312
+ m < month_start and y -= 1
313
+ if years.has_key? y
314
+ value = mov.value
315
+ mov.value = 0
316
+ dputs(5) { "Moving to #{years[y].inspect}: " +
317
+ "#{mov.account_src.id} - #{mov.account_dst.id} - #{acc.id}" }
318
+ if mov.account_src.id == acc.id
319
+ dputs(5) { 'Moving src' }
320
+ mov.account_src_id = years[y]
321
+ else
322
+ dputs(5) { 'Moving dst' }
323
+ mov.account_dst_id = years[y]
324
+ end
325
+ mov.value = value
326
+ end
327
+ }
328
+ dputs(5) { "Movements left in account #{acc.path}:" }
329
+ acc.movements.each { |m|
330
+ dputs(5) { m.desc }
331
+ }
332
+ end
333
+
334
+ def sum_up_total(acc_path, years_archived, month_start)
335
+ a_path = acc_path.sub(/[^:]*::/, '')
336
+ dputs(2) { "Summing up account #{a_path}" }
337
+ acc_sum = []
338
+ years_archived.each { |y, a|
339
+ dputs(5) { "Found archived year #{y.inspect} which is #{y.class.name}" }
340
+ aacc = Accounts.get_by_path(a.get_path + '::' + a_path)
341
+ acc_sum.push [y, a, aacc]
342
+ }
343
+ dputs(5) { 'Trying to add current year' }
344
+ if curr_acc = Accounts.get_by_path(acc_path)
345
+ dputs(4) { 'Adding current year' }
346
+ acc_sum.push [9999, nil, curr_acc]
347
+ end
348
+
349
+ last_total = 0
350
+ last_year_acc = nil
351
+ last_year_acc_parent = nil
352
+ last_year = 0
353
+ dputs(5) { "Sorting account_sums #{acc_sum.length}" }
354
+ acc_sum.sort { |a, b| a[0] <=> b[0] }.each { |y, a, aacc|
355
+ dputs(5) { "y, a, aacc: #{y}, #{a.to_json}, #{aacc.to_json}" }
356
+ if aacc
357
+ last_year_acc_parent and last_year_acc_parent.dump true
358
+ dputs(4) { "Found archived account #{aacc.get_path} for year #{y}" +
359
+ " with last_total #{last_total}" }
360
+ dputs(5) { 'And has movements' }
361
+ aacc.movements.each { |m|
362
+ dputs(5) { m.to_json }
363
+ }
364
+ if last_total != 0
365
+ desc = "-- Sum of #{last_year} of #{last_year_acc.path}"
366
+ date = "#{last_year + 1}-#{month_start.to_s.rjust(2, '0')}-01"
367
+ dputs(3) { "Deleting old sums with date #{date.inspect}" }
368
+ Movements.matches_by_desc("^#{desc}$").each { |m|
369
+ dputs(3) { "Testing movement with date #{m.date.to_s.inspect}: #{m.to_json}" }
370
+ if m.date.to_s == date.to_s
371
+ dputs(3) { 'Deleting it' }
372
+ m.delete
373
+ end
374
+ }
375
+ dputs(3) { "Creating movement for the sum of last year: #{last_total}" }
376
+ mov = Movements.create(desc, date, last_total,
377
+ last_year_acc_parent, aacc)
378
+ last_year_acc_parent.dump true
379
+ dputs(3) { "Movement is: #{mov.to_json}" }
380
+ end
381
+ aacc.update_total
382
+ dputs(5) { "#{aacc.total} - #{aacc.multiplier}" }
383
+ last_total = aacc.total * aacc.multiplier
384
+ else
385
+ dputs(4) { "Didn't find archived account for #{y}" }
386
+ last_total = 0
387
+ end
388
+ last_year, last_year_acc, last_year_acc_parent = y, aacc, a
389
+ }
390
+ end
391
+
392
+ def self.archive(month_start = 1, this_year = nil, only_account = nil)
393
+ if not this_year
394
+ now = Time.now
395
+ this_year = now.year
396
+ now.month < month_start and this_year -= 1
397
+ end
398
+
399
+ root = AccountRoot.actual
400
+ if only_account
401
+ root = only_account
402
+ elsif not root
403
+ dputs(0) { 'Error: Root-account not available!' }
404
+ return false
405
+ elsif (root.account_id > 0)
406
+ dputs(0) { "Error: Can't archive with Root is not in root: #{root.account_id.inspect}!" }
407
+ return false
408
+ end
409
+
410
+ archive = AccountRoot.archive
411
+ if not archive
412
+ archive = self.create('Archive')
413
+ end
414
+
415
+ years_archived = {}
416
+ archive.accounts.each { |a| years_archived[a.name.to_i] = a }
417
+
418
+ dputs(2) { 'Got root and archive' }
419
+ # For every account we search the most-used year, so
420
+ # that we can move the account to that archive. This way
421
+ # we omit as many as possible updates for the clients, as
422
+ # every displacement of a movement will have to be updated,
423
+ # while the displacement of an account is much simpler
424
+ root.get_tree_depth { |acc|
425
+ dputs(2) { "Looking at account #{acc.path}" }
426
+ years = search_account(acc, month_start)
427
+
428
+ if years.size > 0
429
+ most_used = last_used = this_year
430
+ if acc.accounts.count == 0
431
+ most_used = years.key(years.values.max)
432
+ last_used = years.keys.max
433
+ years.delete most_used
434
+ end
435
+ acc_path = acc.path
436
+
437
+ dputs(3) { "most_used: #{most_used} - last_used: #{last_used}" +
438
+ "- acc_path: #{acc_path}" }
439
+
440
+ # First move all other movements around
441
+ if years.keys.size > 0
442
+ create_accounts(acc, years, years_archived, this_year)
443
+
444
+ move_movements(acc, years, month_start)
445
+ end
446
+
447
+ if most_used != this_year
448
+ # Now move account to archive-year of most movements
449
+ parent = archive_parent(acc, years_archived, most_used)
450
+ if double = Accounts.get_by_path("#{parent.get_path}::#{acc.name}")
451
+ dputs(3) { "Account #{acc.path_id} already exists in #{parent.path_id}" }
452
+ # Move all movements
453
+ acc.movements.each { |m|
454
+ dputs(4) { "Moving movement #{m.to_json}" }
455
+ value = m.value
456
+ m.value = 0
457
+ if m.account_src == acc
458
+ m.account_src_id = double
459
+ else
460
+ m.account_dst_id = double
461
+ end
462
+ m.value = value
463
+ }
464
+ # Delete acc
465
+ acc.delete
466
+ else
467
+ dputs(3) { "Moving account #{acc.path_id} to #{parent.path_id}" }
468
+ acc.parent = parent
469
+ end
470
+ end
471
+
472
+ # Check whether we need to add the account to the current year
473
+ if (last_used >= this_year - 1) and
474
+ (most_used != this_year)
475
+ dputs(3) { "Adding #{acc_path} to this year with mult #{acc.multiplier}" }
476
+ Accounts.create_path(acc_path, 'Copied from archive', false,
477
+ acc.multiplier, acc.keep_total)
478
+ end
479
+
480
+ if acc.keep_total
481
+ dputs(2) { "Keeping total for #{acc_path}" }
482
+ # And create a trail so that every year contains the previous
483
+ # years worth of "total"
484
+ sum_up_total(acc_path, years_archived, month_start)
485
+ dputs(5) { "acc_path is now #{acc_path}" }
486
+ else
487
+ dputs(2) { "Not keeping for #{acc_path}" }
488
+ end
489
+ else
490
+ dputs(3) { "Empty account #{acc.movements.count} - #{acc.accounts.count}" }
491
+ end
492
+
493
+ if acc.accounts.count == 0
494
+ movs = acc.movements
495
+ case movs.count
496
+ when 0
497
+ dputs(3) { "Deleting empty account #{acc.path}" }
498
+ if acc.path != 'Root'
499
+ acc.delete
500
+ else
501
+ dputs(2) { 'Not deleting root!' }
502
+ end
503
+ when 1
504
+ dputs(3) { "Found only one movement for #{acc.path}" }
505
+ if movs.first.desc =~ /^-- Sum of/
506
+ dputs(3) { 'Deleting account which has only a sum' }
507
+ movs.first.delete
508
+ if acc.path != 'Root'
509
+ acc.delete
510
+ else
511
+ dputs(2) { 'Not deleting root!' }
512
+ end
513
+ end
514
+ end
515
+ end
516
+ }
517
+ if DEBUG_LVL >= 3
518
+ self.dump
519
+ end
520
+ end
521
+
522
+ def self.dump_raw(mov = false)
523
+ Accounts.search_all.each { |a|
524
+ a.dump(mov)
525
+ }
526
+ end
527
+
528
+ def self.dump(mov = false)
529
+ dputs(1) { 'Root-tree is now' }
530
+ AccountRoot.actual.dump_rec(mov)
531
+ if archive = AccountRoot.archive
532
+ dputs(1) { 'Archive-tree is now' }
533
+ archive.dump_rec(mov)
534
+ else
535
+ dputs(1) { 'No archive-tree' }
536
+ end
537
+ AccountRoot.accounts.each { |a|
538
+ dputs(1) { "Root-Account: #{a.inspect}" }
539
+ }
540
+ end
541
+
542
+ def load
543
+ super
544
+ if Accounts.search_by_name('Root').count == 0
545
+ dputs(1) { "Didn't find 'Root' in database - creating base" }
546
+ root = Accounts.create('Root', 'Initialisation')
547
+ %w( Income Outcome Lending Cash ).each { |a|
548
+ Accounts.create(a, 'Initialisation', root)
549
+ }
550
+ %w( Lending Cash ).each { |a|
551
+ acc = Accounts.match_by_name(a)
552
+ acc.multiplier = -1
553
+ acc.keep_total = true
554
+ }
555
+ Accounts.save
556
+ end
557
+ end
558
+
559
+ def migration_1(a)
560
+ dputs(4) { Accounts.storage[:SQLiteAC].db_class.inspect }
561
+ a.deleted = false
562
+ # As most of the accounts in Cash have -1 and shall be kept, this
563
+ # gives a good first initialisation
564
+ a.keep_total = (a.multiplier == -1.0) || (a.multiplier == -1)
565
+ dputs(4) { "#{a.name}: #{a.deleted.inspect} - #{a.keep_total.inspect}" }
566
+ end
567
+
568
+ def migration_2(a)
569
+ a.rev_index = a.id
570
+ end
571
+
572
+ def listp_path
573
+ dputs(3) { 'Being called' }
574
+ Accounts.search_all.select { |a| !a.deleted }.collect { |a| [a.id, a.path] }.
575
+ sort { |a, b|
576
+ a[1] <=> b[1]
577
+ }
578
+ end
579
+
580
+ def bool_to_s(b)
581
+ (b && b != 'f') ? 'true' : 'false'
582
+ end
583
+
584
+ def check_against_db(file)
585
+ # First build
586
+ # in_db - content of 'file' in .to_s format
587
+ # in_local - content available locally in .to_s format
588
+ in_db, diff, in_local = [], [], []
589
+ dputs(3) { 'Searching all accounts' }
590
+ @check_state = 'Collect local'
591
+ @check_progress = 0.0
592
+ in_local = Accounts.search_all_
593
+ progress_step = 1.0 / (in_local.size + 1)
594
+ dputs(3) { "Found #{in_local.size} accounts" }
595
+
596
+ @check_state = 'Collect local'
597
+ in_local = in_local.collect { |a|
598
+ @check_progress += progress_step
599
+ a.to_s
600
+ }
601
+
602
+ dputs(3) { 'Loading file-db' }
603
+ @check_state = 'Collect file-DB'
604
+ @check_progress = 0.0
605
+ SQLite3::Database.new(file) do |db|
606
+ db.execute('select id, account_id, name, desc, global_id, total, '+
607
+ 'multiplier, "index", rev_index, deleted, keep_total '+
608
+ 'from compta_accounts').sort_by { |a| a[4] }.each do |row|
609
+ #dputs(3) { "Looking at #{row}" }
610
+ @check_progress += progress_step
611
+
612
+ id_, acc_id_, name_, desc_, gid_, tot_, mult_, ind_, revind_, del_, keep_, = row
613
+ parent = if acc_id_
614
+ acc_id_ == 0 ? '' :
615
+ db.execute("select * from compta_accounts where id=#{acc_id_}").first[4]
616
+ else
617
+ ''
618
+ end
619
+ in_db.push "#{desc_}\r#{gid_}\t" +
620
+ "#{sprintf('%.3f', tot_.to_f.round(3))}\t#{name_.to_s}\t"+
621
+ "#{mult_.to_i.to_s}\t#{parent}" +
622
+ "\t#{bool_to_s(del_)}" + "\t#{bool_to_s(keep_)}"
623
+ end
624
+ end
625
+
626
+ # Now compare what is available only in db and what is available only locally
627
+ dputs(3) { 'Comparing local accounts with file-db accounts' }
628
+ @check_state = 'On one side'
629
+ @check_progress = 0.0
630
+ in_db.delete_if { |a|
631
+ @check_progress += progress_step
632
+ in_local.delete(a)
633
+ }
634
+
635
+ # And search for accounts with same global-id but different content
636
+ dputs(3) { 'Seaching mix-ups' }
637
+ @check_state = 'Mixed-up'
638
+ @check_progress = 0.0
639
+ progress_step = 1.0 / (in_db.size + 1)
640
+ (in_db + in_local).sort_by { |a| a.match(/\r(.*?)\t/)[1] }
641
+ in_db.delete_if { |a|
642
+ @check_progress += progress_step
643
+ gid = a.match(/\r(.*?)\t/)[1]
644
+ if c = in_local.find { |b| b =~ /\r#{gid}\t/ }
645
+ diff.push [a, c]
646
+ in_local.delete c
647
+ end
648
+ }
649
+
650
+ @check_state = 'Done'
651
+ [in_db, diff, in_local]
652
+ end
653
+ end
654
+
655
+ class Account < Entity
656
+
657
+ def data_set(f, v)
658
+ if !@proxy.loading
659
+ if !%w( _total _rev_index ).index(f.to_s)
660
+ dputs(4) { "Updating index for field #{f.inspect} - #{@pre_init} - #{@proxy.loading} - #{caller}" }
661
+ new_index
662
+ end
663
+ end
664
+ super(f, v)
665
+ end
666
+
667
+ # This gets the tree under that account, breadth-first
668
+ def get_tree(depth = -1)
669
+ yield self, depth
670
+ return if depth == 0
671
+ accounts.sort { |a, b| a.name <=> b.name }.each { |a|
672
+ a.get_tree(depth - 1) { |b| yield b, depth - 1 }
673
+ }
674
+ end
675
+
676
+ # This gets the tree under that account, depth-first
677
+ def get_tree_depth
678
+ accounts.sort { |a, b| a.name <=> b.name }.each { |a|
679
+ a.get_tree_depth { |b| yield b }
680
+ }
681
+ yield self
682
+ end
683
+
684
+ def get_tree_debug(ind = '')
685
+ yield self
686
+ dputs(1) { "get_tree_ #{ind}#{self.name}" }
687
+ accounts.sort { |a, b| a.name <=> b.name }.each { |a|
688
+ a.get_tree_debug("#{ind} ") { |b| yield b }
689
+ }
690
+ end
691
+
692
+ def path(sep = '::', p='', first=true)
693
+ if (acc = self.account)
694
+ return acc.path(sep, p, false) + sep + self.name
695
+ else
696
+ return self.name
697
+ end
698
+ end
699
+
700
+ def path_id(sep = '::', p='', first=true)
701
+ (self.account ?
702
+ "#{self.account.path_id(sep, p, false)}#{sep}" : '') +
703
+ "#{self.name}-#{self.id}"
704
+ end
705
+
706
+ def get_path(sep = '::', p = '', first = true)
707
+ path(sep, p, first)
708
+ end
709
+
710
+ def new_index()
711
+ if !u_l = Users.match_by_name('local')
712
+ dputs(0) { "Oups - user 'local' was not here: #{caller}" }
713
+ u_l = Users.create('local')
714
+ end
715
+ self.rev_index = u_l.account_index
716
+ u_l.account_index += 1
717
+ dputs(3) { "Index for account #{name} is #{index}" }
718
+ end
719
+
720
+ def update_total(precision = 3)
721
+ # Recalculate everything.
722
+ dputs(4) { "Calculating total for #{self.path_id} with mult #{self.multiplier}" }
723
+ self.total = (0.0).to_f
724
+ dputs(4) { "Total before update is #{self.total} - #{self.total.class.name}" }
725
+ self.movements.each { |m|
726
+ v = m.get_value(self)
727
+ dputs(5) { "Adding value #{v.inspect} to #{self.total.inspect}" }
728
+ self.total = self.total.to_f + v.to_f
729
+ dputs(5) { "And getting #{self.total.inspect}" }
730
+ }
731
+ self.total = self.total.to_f.round(precision)
732
+ dputs(4) { "Final total is #{self.total} - #{self.total.class.name}" }
733
+ end
734
+
735
+ # Sets different new parameters.
736
+ def set_nochildmult(name, desc, parent = nil, multiplier = 1, users = [],
737
+ keep_total = false)
738
+ self.name, self.desc, self.keep_total = name, desc, keep_total
739
+ parent and self.account_id = parent
740
+ # TODO: implement link between user-table and account-table
741
+ # self.users = users ? users.join(":") : ""
742
+ self.multiplier = multiplier
743
+ self.keep_total = keep_total
744
+ update_total
745
+ self
746
+ end
747
+
748
+ def set(name, desc, parent, multiplier = 1, users = [], keep_total = false)
749
+ dputs(3) { "Going to set #{name}-#{parent}-#{multiplier}" }
750
+ set_nochildmult(name, desc, parent, multiplier, users, keep_total)
751
+ # All descendants shall have the same multiplier
752
+ set_child_multiplier_total(multiplier, total)
753
+ end
754
+
755
+ # Sort first regarding inverse date (newest first), then description,
756
+ # and finally the value
757
+ def movements(from = nil, to = nil)
758
+ dputs(5) { 'Account::movements' }
759
+ movs = (movements_src + movements_dst)
760
+ if (from != nil and to != nil)
761
+ movs.delete_if { |m|
762
+ (m.date < from || m.date > to)
763
+ }
764
+ dputs(3) { 'Rejected some elements' }
765
+ end
766
+ movs.delete_if { |m| m.value == 0 }
767
+ sorted = movs.sort { |a, b|
768
+ ret = 0
769
+ if a.date and b.date
770
+ ret = a.date.to_s <=> b.date.to_s
771
+ end
772
+ if ret == 0
773
+ ret = a.rev_index <=> b.rev_index
774
+ =begin
775
+ if a.desc and b.desc
776
+ ret = a.desc <=> b.desc
777
+ end
778
+ if ret == 0
779
+ if a.value and b.value
780
+ ret = a.value.to_f <=> b.value.to_f
781
+ end
782
+ end
783
+ =end
784
+ end
785
+ if ret
786
+ ret * -1
787
+ else
788
+ dputs(0) { "Error: Ret shouldn't be nil... #{self.path}" }
789
+ 0
790
+ end
791
+ }
792
+ sorted
793
+ end
794
+
795
+ def bool_to_s(b)
796
+ b ? 'true' : 'false'
797
+ end
798
+
799
+ def to_s(add_path = false)
800
+ if account || true
801
+ dputs(4) { "Account-desc: #{name.to_s}, #{global_id}, #{account_id.inspect}" }
802
+ "#{desc}\r#{global_id}\t" +
803
+ "#{sprintf('%.3f', total.to_f.round(3))}\t#{name.to_s}\t#{multiplier.to_i.to_s}\t" +
804
+ (account_id ? ((account_id > 0) ? account.global_id.to_s : '') : '') +
805
+ "\t#{bool_to_s(self.deleted)}" + "\t#{bool_to_s(self.keep_total)}" +
806
+ (add_path ? "\t#{path}" : '')
807
+ else
808
+ 'nope'
809
+ end
810
+ end
811
+
812
+ def is_empty
813
+ size = self.movements.select { |m| m.value.to_f != 0.0 }.size
814
+ dputs(2) { "Account #{self.name} has #{size} non-zero elements" }
815
+ dputs(4) { "Non-zero elements: #{movements.inspect}" }
816
+ if size == 0 and self.accounts.size == 0
817
+ return true
818
+ end
819
+ return false
820
+ end
821
+
822
+ # Be sure that all descendants have the same multiplier and keep_total
823
+ def set_child_multiplier_total(m, t)
824
+ dputs(3) { "Setting multiplier from #{name} to #{m} and keep_total to #{t}" }
825
+ self.multiplier = m
826
+ self.keep_total = t
827
+ return if not accounts
828
+ accounts.each { |acc|
829
+ acc.set_child_multiplier_total(m, t)
830
+ }
831
+ self
832
+ end
833
+
834
+ def accounts
835
+ # Some hand-optimized stuff. This would be written shorter like this:
836
+ # Accounts.matches_by_account_id( self.id )
837
+ # But the code below is 3 times faster for some big data
838
+ ret = []
839
+ Accounts.data.each { |k, v|
840
+ if v[:account_id] == self.id
841
+ ret.push Accounts.get_data_instance(k)
842
+ end
843
+ }
844
+ ret
845
+ end
846
+
847
+ # This is the parent account
848
+ def account
849
+ Accounts.match_by_id(self.account_id)
850
+ end
851
+
852
+ def account= (a)
853
+ self.account_id = a.class == Account ? a.id : a
854
+ end
855
+
856
+ def parent
857
+ account
858
+ end
859
+
860
+ def parent= (a)
861
+ self.account = a
862
+ end
863
+
864
+ def movements_src
865
+ Movements.matches_by_account_src_id(self.id)
866
+ end
867
+
868
+ def movements_dst
869
+ Movements.matches_by_account_dst_id(self.id)
870
+ end
871
+
872
+ def multiplier
873
+ _multiplier.to_i
874
+ end
875
+
876
+ def delete(force = false)
877
+ if not is_empty and force
878
+ movements_src.each { |m|
879
+ dputs(3) { "Deleting movement #{m.to_json}" }
880
+ m.delete
881
+ }
882
+ end
883
+ if is_empty
884
+ dputs(2) { "Deleting account #{self.name}-#{self.id}" }
885
+ self.account_id = nil
886
+ self.deleted = true
887
+ else
888
+ dputs(1) { "Refusing to delete account #{name}" }
889
+ return false
890
+ end
891
+ return true
892
+ end
893
+
894
+ def print_pdf_document(pdf)
895
+ sum = 0
896
+ pdf.font_size 10
897
+ movs = movements.select { |m|
898
+ m.value.abs >= 0.001
899
+ }.sort { |a, b| a.date <=> b.date }
900
+ if movs.length > 0
901
+ header = [['', {:content => "#{path}", :colspan => 2, :align => :left},
902
+ {:content => "#{id}", :align => :right}],
903
+ %w(Date Description Other # Value Sum).collect { |ch|
904
+ {:content => ch, :align => :center} }]
905
+ pdf.table(header +
906
+ movs.collect { |m|
907
+ other = m.get_other_account(self)
908
+ value = m.get_value(self)
909
+ [{:content => m.date.to_s, :align => :center},
910
+ m.desc,
911
+ other.name,
912
+ {:content => "#{other.id}", :align => :right},
913
+ {:content => "#{Account.total_form(value)}", :align => :right},
914
+ {:content => "#{Account.total_form(sum += value)}", :align => :right}]
915
+ }, :header => true, :column_widths => [70, 400, 100, 40, 75, 75])
916
+ pdf.move_down(2.cm)
917
+ end
918
+ end
919
+
920
+ def print_pdf(file, recursive = false)
921
+ Prawn::Document.generate(file,
922
+ :page_size => 'A4',
923
+ :page_layout => :landscape,
924
+ :bottom_margin => 2.cm,
925
+ :top_margin => 2.cm) do |pdf|
926
+ if recursive
927
+ get_tree_depth { |a|
928
+ a.print_pdf_document(pdf)
929
+ }
930
+ else
931
+ print_pdf_document(pdf)
932
+ end
933
+ pdf.repeat(:all, :dynamic => true) do
934
+ pdf.draw_text self.path, :at => [0, -20]
935
+ pdf.draw_text pdf.page_number, :at => [14.85.cm, -20]
936
+ end
937
+ end
938
+ end
939
+
940
+ def dump(mov = false)
941
+ t = (self.keep_total ? 'K' : '.') + "#{self.multiplier.to_s.rjust(2, '+')}"
942
+ acc_desc = ["**#{t}**#{self.path_id}#{self.deleted ? ' -- deleted' : ''}"]
943
+ dputs(1) { acc_desc.first }
944
+ acc_desc +
945
+ if mov
946
+ movements.collect { |m|
947
+ m_desc = " #{m.to_json}"
948
+ dputs(1) { m_desc }
949
+ m_desc
950
+ }
951
+ else
952
+ []
953
+ end
954
+ end
955
+
956
+ def dump_rec(mov = false)
957
+ ret = []
958
+ get_tree_depth { |a|
959
+ ret.push a.dump(mov)
960
+ }
961
+ ret.flatten
962
+ end
963
+
964
+ def listp_path(depth = -1)
965
+ acc = []
966
+ get_tree(depth) { |a|
967
+ acc.push [a.id, a.path]
968
+ }
969
+ dputs(3) { "Ret is #{acc.inspect}" }
970
+ acc
971
+ end
972
+
973
+ def total_form
974
+ Account.total_form(total)
975
+ end
976
+
977
+ def self.total_form(v)
978
+ (v.to_f * 1000 + 0.5).floor.to_s.tap do |s|
979
+ :go while s.gsub!(/^([^.]*)(\ d)(?=(\ d { 3 })+)/, "\\1\\2,")
980
+ end
981
+ end
982
+
983
+ def get_archives
984
+ if archive = AccountRoot.archive
985
+ archive.accounts.collect { |arch|
986
+ Accounts.get_by_path("#{arch.path}::#{path.sub(/^Root::/, '')}")
987
+ }.select { |a| a }
988
+ end
989
+ end
990
+
991
+ def get_archive(year = Date.today.year - 1, month = Date.today.month)
992
+ dputs(0) { 'Error: not implemented yet' }
993
+ end
994
+
995
+ end