africompta 1.9.8

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