africompta 1.9.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +74 -0
- data/TODO +31 -0
- data/Test/ac_account.rb +128 -0
- data/Test/ac_africompta.rb +1001 -0
- data/Test/ac_big.rb +62 -0
- data/Test/ac_movement.rb +59 -0
- data/Test/ac_sqlite.rb +139 -0
- data/Test/config_test.yaml +31 -0
- data/Test/db.testGestion +0 -0
- data/Test/test.rb +39 -0
- data/VERSION +140 -0
- data/africompta.gemspec +20 -0
- data/lib/africompta/acaccess.rb +257 -0
- data/lib/africompta/acqooxview.rb +77 -0
- data/lib/africompta/africompta.rb +83 -0
- data/lib/africompta/entities/account.rb +995 -0
- data/lib/africompta/entities/acschemas.rb +16 -0
- data/lib/africompta/entities/movement.rb +292 -0
- data/lib/africompta/entities/remote.rb +27 -0
- data/lib/africompta/entities/users.rb +55 -0
- data/lib/africompta/views/edit/movement.rb +8 -0
- data/lib/africompta/views/edit/tabs.rb +8 -0
- data/lib/africompta/views/report/annual.rb +3 -0
- data/lib/africompta/views/report/tabs.rb +3 -0
- data/lib/africompta.rb +2 -0
- metadata +84 -0
@@ -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
|