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.
- 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
|