lucabook 0.2.15 → 0.2.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,7 +9,7 @@ module LucaBook
9
9
  def self.create_project(country = nil, dir = LucaSupport::Config::Pjdir)
10
10
  FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
11
11
  Dir.chdir(dir) do
12
- %w[data/journals dict].each do |subdir|
12
+ %w[data/journals data/balance dict].each do |subdir|
13
13
  FileUtils.mkdir_p(subdir) unless Dir.exist?(subdir)
14
14
  end
15
15
  dict = if File.exist?("#{__dir__}/templates/dict-#{country}.tsv")
@@ -18,6 +18,21 @@ module LucaBook
18
18
  'dict-en.tsv'
19
19
  end
20
20
  FileUtils.cp("#{__dir__}/templates/#{dict}", 'dict/base.tsv') unless File.exist?('dict/base.tsv')
21
+ prepare_starttsv(dict) unless File.exist? 'data/balance/start.tsv'
22
+ end
23
+ end
24
+
25
+ # Generate initial balance template.
26
+ # Codes are same as base dictionary.
27
+ # The previous month of start date is better for _date.
28
+ #
29
+ def self.prepare_starttsv(dict)
30
+ CSV.open('data/balance/start.tsv', 'w', col_sep: "\t", encoding: 'UTF-8') do |csv|
31
+ csv << ['code', 'label', 'balance']
32
+ csv << ['_date', '2020-1-1']
33
+ CSV.open("#{__dir__}/templates/#{dict}", 'r', col_sep: "\t", encoding: 'UTF-8').each do |row|
34
+ csv << row if /^[1-9]/.match(row[0])
35
+ end
21
36
  end
22
37
  end
23
38
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require 'csv'
3
4
  require 'pathname'
@@ -7,7 +8,6 @@ require 'luca_record'
7
8
  require 'luca_record/dict'
8
9
  require 'luca_book'
9
10
 
10
- #
11
11
  # Statement on specified term
12
12
  #
13
13
  module LucaBook
@@ -17,9 +17,12 @@ module LucaBook
17
17
 
18
18
  attr_reader :statement
19
19
 
20
- def initialize(data)
20
+ def initialize(data, count = nil, date: nil)
21
21
  @data = data
22
+ @count = count
22
23
  @dict = LucaRecord::Dict.load('base.tsv')
24
+ @start_date = date
25
+ @start_balance = set_balance
23
26
  end
24
27
 
25
28
  # TODO: not compatible with LucaRecord::Base.open_records
@@ -41,106 +44,216 @@ module LucaBook
41
44
  last_date = Date.new(to_year.to_i, to_month.to_i, -1)
42
45
  raise 'invalid term specified' if date > last_date
43
46
 
47
+ counts = []
44
48
  reports = [].tap do |r|
45
49
  while date <= last_date do
46
- r << accumulate_month(date.year, date.month)
47
- date = date.next_month
50
+ diff, count = accumulate_month(date.year, date.month)
51
+ r << diff.tap { |c| c['_d'] = date.to_s }
52
+ counts << count.tap { |c| c['_d'] = date.to_s }
53
+ date = Date.new(date.next_month.year, date.next_month.month, -1)
48
54
  end
49
55
  end
50
- new reports
56
+ new(reports, counts, date: Date.new(from_year.to_i, from_month.to_i, -1))
51
57
  end
52
58
 
53
- def by_code(code, year=nil, month=nil)
54
- raise 'not supported year range yet' if ! year.nil? && month.nil?
55
-
56
- balance = @book.load_start.dig(code) || 0
57
- full_term = self.class.scan_terms
58
- if ! month.nil?
59
- pre_term = full_term.select { |y, m| y <= year.to_i && m < month.to_i }
60
- balance += pre_term.map { |y, m| self.class.net(y, m)}.inject(0){|sum, h| sum + h[code] }
61
- [{ code: code, balance: balance, note: "#{code} #{dict.dig(code, :label)}" }] + records_with_balance(year, month, code, balance)
62
- else
63
- start = { code: code, balance: balance, note: "#{code} #{dict.dig(code, :label)}" }
64
- full_term.map { |y, m| y }.uniq.map { |y|
65
- records_with_balance(y, nil, code, balance)
66
- }.flatten.prepend(start)
59
+ def self.by_code(code, from_year, from_month, to_year = from_year, to_month = from_month)
60
+ date = Date.new(from_year.to_i, from_month.to_i, -1)
61
+ last_date = Date.new(to_year.to_i, to_month.to_i, -1)
62
+ raise 'invalid term specified' if date > last_date
63
+
64
+ reports = [].tap do |r|
65
+ while date <= last_date do
66
+ diff = {}.tap do |h|
67
+ g = gross(date.year, date.month, code)
68
+ sum = g.dig(:debit).nil? ? BigDecimal('0') : Util.calc_diff(g[:debit], code)
69
+ sum -= g.dig(:credit).nil? ? BigDecimal('0') : Util.calc_diff(g[:credit], code)
70
+ h['code'] = code
71
+ h['label'] = LucaRecord::Dict.load('base.tsv').dig(code, :label)
72
+ h['net'] = sum
73
+ h['debit_amount'] = g[:debit]
74
+ h['debit_count'] = g[:debit_count]
75
+ h['credit_amount'] = g[:credit]
76
+ h['credit_count'] = g[:credit_count]
77
+ h['_d'] = date.to_s
78
+ end
79
+ r << diff
80
+ date = Date.new(date.next_month.year, date.next_month.month, -1)
81
+ end
67
82
  end
83
+ #YAML.dump(LucaSupport::Code.readable(reports)) #.tap{ |data| puts data }
84
+ #new(reports, counts, date: Date.new(from_year.to_i, from_month.to_i, -1))
85
+ LucaSupport::Code.readable(reports)
68
86
  end
69
87
 
70
88
  def records_with_balance(year, month, code, balance)
71
89
  @book.search(year, month, nil, code).each do |h|
72
- balance += self.class.calc_diff(amount_by_code(h[:debit], code), code) - @book.calc_diff(amount_by_code(h[:credit], code), code)
90
+ balance += Util.calc_diff(Util.amount_by_code(h[:debit], code), code) - Util.calc_diff(Util.amount_by_code(h[:credit], code), code)
73
91
  h[:balance] = balance
74
92
  end
75
93
  end
76
94
 
77
- #
78
- # TODO: useless method. consider to remove
79
- #
80
- def accumulate_all
81
- current = @book.load_start
82
- target = []
83
- Dir.chdir(@book.pjdir) do
84
- net_records = self.class.scan_terms.map do |year, month|
85
- target << [year, month]
86
- accumulate_month(year, month)
87
- end
88
- all_keys = net_records.map{|h| h.keys}.flatten.uniq
89
- net_records.each.with_index(0) do |diff, i|
90
- all_keys.each {|key| diff[key] = 0 unless diff.has_key?(key)}
91
- diff.each do |k,v|
92
- if current[k]
93
- current[k] += v
94
- else
95
- current[k] = v
96
- end
97
- end
98
- f = { target: "#{target[i][0]}-#{target[i][1]}", diff: diff.sort, current: current.sort }
99
- yield f
100
- end
101
- end
102
- end
103
-
104
95
  def to_yaml
105
- YAML.dump(code2label).tap { |data| puts data }
96
+ YAML.dump(readable(code2label)).tap { |data| puts data }
106
97
  end
107
98
 
108
99
  def code2label
109
100
  @statement ||= @data
110
101
  @statement.map do |report|
111
102
  {}.tap do |h|
112
- report.each { |k, v| h[@dict.dig(k, :label)] = v }
103
+ report.each { |k, v| h[@dict.dig(k, :label) || k] = v }
113
104
  end
114
105
  end
115
106
  end
116
107
 
117
- def bs
118
- @statement = @data.map do |data|
119
- data.select { |k, v| /^[0-9].+/.match(k) }
108
+ def stats(level = nil)
109
+ keys = @count.map(&:keys).flatten.push('_t').uniq.sort
110
+ @count.map! do |data|
111
+ sum = 0
112
+ keys.each do |k|
113
+ data[k] ||= 0
114
+ sum += data[k] if /^[^_]/.match(k)
115
+ next if level.nil? || k.length <= level
116
+
117
+ if data[k[0, level]]
118
+ data[k[0, level]] += data[k]
119
+ else
120
+ data[k[0, level]] = data[k]
121
+ end
122
+ end
123
+ data.select! { |k, _v| k.length <= level } if level
124
+ data['_t'] = sum
125
+ data.sort.to_h
126
+ end
127
+ keys.map! { |k| k[0, level] }.uniq.select! { |k| k.length <= level } if level
128
+ @count.prepend({}.tap { |header| keys.each { |k| header[k] = @dict.dig(k, :label) }})
129
+ @count
130
+ end
131
+
132
+ def bs(level = 3, legal: false)
133
+ @start_balance.keys.each { |k| @data.first[k] ||= 0 }
134
+ @data.map! { |data| data.select { |k, _v| k.length <= level } }
135
+ @data.map! { |data| code_sum(data).merge(data) } if legal
136
+ base = accumulate_balance(@data)
137
+ rows = [base[:debit].length, base[:credit].length].max
138
+ @statement = [].tap do |a|
139
+ rows.times do |i|
140
+ {}.tap do |res|
141
+ res['debit_label'] = base[:debit][i] ? @dict.dig(base[:debit][i].keys[0], :label) : ''
142
+ res['debit_balance'] = base[:debit][i] ? (@start_balance.dig(base[:debit][i].keys[0]) || 0) + base[:debit][i].values[0] : ''
143
+ res['debit_diff'] = base[:debit][i] ? base[:debit][i].values[0] : ''
144
+ res['credit_label'] = base[:credit][i] ? @dict.dig(base[:credit][i].keys[0], :label) : ''
145
+ res['credit_balance'] = base[:credit][i] ? (@start_balance.dig(base[:credit][i].keys[0]) || 0) + base[:credit][i].values[0] : ''
146
+ res['credit_diff'] = base[:credit][i] ? base[:credit][i].values[0] : ''
147
+ a << res
148
+ end
149
+ end
120
150
  end
121
- self
151
+ readable(@statement)
122
152
  end
123
153
 
124
- def pl
154
+ def accumulate_balance(monthly_diffs)
155
+ data = monthly_diffs.each_with_object({}) do |month, h|
156
+ month.each do |k, v|
157
+ h[k] = h[k].nil? ? v : h[k] + v
158
+ end
159
+ end
160
+ { debit: [], credit: [] }.tap do |res|
161
+ data.sort.to_h.each do |k, v|
162
+ case k
163
+ when /^[0-4].*/
164
+ res[:debit] << { k => v }
165
+ when /^[5-9].*/
166
+ res[:credit] << { k => v }
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ def pl(level = 2)
173
+ term_keys = @data.inject([]) { |a, data| a + data.keys }
174
+ .compact.select { |k| /^[A-H_].+/.match(k) }
175
+ fy = @start_balance.select { |k, _v| /^[A-H].+/.match(k) }
176
+ keys = (term_keys + fy.keys).uniq.sort
177
+ keys.select! { |k| k.length <= level }
125
178
  @statement = @data.map do |data|
126
- data.select { |k, v| /^[A-F].+/.match(k) }
179
+ {}.tap do |h|
180
+ keys.each { |k| h[k] = data[k] || BigDecimal('0') }
181
+ end
127
182
  end
128
- self
183
+ term = @statement.each_with_object({}) do |item, h|
184
+ item.each do |k, v|
185
+ h[k] = h[k].nil? ? v : h[k] + v if /^[^_]/.match(k)
186
+ end
187
+ end
188
+ fy = {}.tap do |h|
189
+ keys.each do |k|
190
+ h[k] = BigDecimal(fy[k] || '0') + BigDecimal(term[k] || '0')
191
+ end
192
+ end
193
+ @statement << term.tap { |h| h['_d'] = 'Period Total' }
194
+ @statement << fy.tap { |h| h['_d'] = 'FY Total' }
195
+ readable(code2label)
129
196
  end
130
197
 
131
- def self.accumulate_month(year, month)
132
- monthly_record = net(year, month)
133
- total_subaccount(monthly_record)
198
+ def self.accumulate_term(start_year, start_month, end_year, end_month)
199
+ date = Date.new(start_year, start_month, 1)
200
+ last_date = Date.new(end_year, end_month, -1)
201
+ return nil if date > last_date
202
+
203
+ {}.tap do |res|
204
+ while date <= last_date do
205
+ diff, _count = net(date.year, date.month)
206
+ diff.each do |k, v|
207
+ next if /^[_]/.match(k)
208
+
209
+ res[k] = res[k].nil? ? v : res[k] + v
210
+ end
211
+ date = date.next_month
212
+ end
213
+ end
134
214
  end
135
215
 
136
- def amount_by_code(items, code)
137
- items
138
- .select{|item| item.dig(:code) == code }
139
- .inject(0){|sum, item| sum + item[:amount] }
216
+ def self.accumulate_month(year, month)
217
+ monthly_record, count = net(year, month)
218
+ [total_subaccount(monthly_record), count]
140
219
  end
141
220
 
221
+ # Accumulate Level 2, 3 account.
222
+ #
142
223
  def self.total_subaccount(report)
143
- report.dup.tap do |res|
224
+ {}.tap do |res|
225
+ res['A0'] = sum_matched(report, /^[A][0-9A-Z]{2,}/)
226
+ res['B0'] = sum_matched(report, /^[B][0-9A-Z]{2,}/)
227
+ res['BA'] = res['A0'] - res['B0']
228
+ res['C0'] = sum_matched(report, /^[C][0-9A-Z]{2,}/)
229
+ res['CA'] = res['BA'] - res['C0']
230
+ res['D0'] = sum_matched(report, /^[D][0-9A-Z]{2,}/)
231
+ res['E0'] = sum_matched(report, /^[E][0-9A-Z]{2,}/)
232
+ res['EA'] = res['CA'] + res['D0'] - res['E0']
233
+ res['F0'] = sum_matched(report, /^[F][0-9A-Z]{2,}/)
234
+ res['G0'] = sum_matched(report, /^[G][0-9][0-9A-Z]{1,}/)
235
+ res['GA'] = res['EA'] + res['F0'] - res['G0']
236
+ res['H0'] = sum_matched(report, /^[H][0-9][0-9A-Z]{1,}/)
237
+ res['HA'] = res['GA'] - res['H0']
238
+
239
+ report['9142'] = (report['9142'] || BigDecimal('0')) + res['HA']
240
+ res['9142'] = report['9142']
241
+ res['10'] = sum_matched(report, /^[123][0-9A-Z]{2,}/)
242
+ res['40'] = sum_matched(report, /^[4][0-9A-Z]{2,}/)
243
+ res['50'] = sum_matched(report, /^[56][0-9A-Z]{2,}/)
244
+ res['70'] = sum_matched(report, /^[78][0-9A-Z]{2,}/)
245
+ res['91'] = sum_matched(report, /^91[0-9A-Z]{1,}/)
246
+ res['8ZZ'] = res['50'] + res['70']
247
+ res['9ZZ'] = sum_matched(report, /^[9][0-9A-Z]{2,}/)
248
+
249
+ res['1'] = res['10'] + res['40']
250
+ res['5'] = res['8ZZ'] + res['9ZZ']
251
+ res['_d'] = report['_d']
252
+
253
+ report.each do |k, v|
254
+ res[k] = v if k.length == 3
255
+ end
256
+
144
257
  report.each do |k, v|
145
258
  if k.length >= 4
146
259
  if res[k[0, 3]]
@@ -150,109 +263,131 @@ module LucaBook
150
263
  end
151
264
  end
152
265
  end
153
- res['10'] = sum_matched(report, /^[123].[^0]/)
154
- res['40'] = sum_matched(report, /^[4].[^0]}/)
155
- res['50'] = sum_matched(report, /^[56].[^0]/)
156
- res['70'] = sum_matched(report, /^[78].[^0]/)
157
- res['90'] = sum_matched(report, /^[9].[^0]/)
158
- res['A0'] = sum_matched(report, /^[A].[^0]/)
159
- res['B0'] = sum_matched(report, /^[B].[^0]/)
160
- res['BA'] = res['A0'] - res['B0']
161
- res['C0'] = sum_matched(report, /^[C].[^0]/)
162
- res['CA'] = res['BA'] - res['C0']
163
- res['D0'] = sum_matched(report, /^[D].[^0]/)
164
- res['E0'] = sum_matched(report, /^[E].[^0]/)
165
- res['EA'] = res['CA'] + res['D0'] - res['E0']
166
- res['F0'] = sum_matched(report, /^[F].[^0]/)
167
- res['G0'] = sum_matched(report, /^[G].[^0]/)
168
- res['GA'] = res['EA'] + res['F0'] - res['G0']
169
- res['HA'] = res['GA'] - sum_matched(report, /^[H].[^0]/)
266
+ res.sort.to_h
267
+ end
268
+ end
269
+
270
+ def code_sum(report)
271
+ legal_items.each.with_object({}) do |k, h|
272
+ h[k] = self.class.sum_matched(report, /^#{k}.*/)
170
273
  end
171
274
  end
172
275
 
276
+ def set_balance
277
+ pre_last = @start_date.prev_month
278
+ pre = if @start_date.month > LucaSupport::CONFIG['fy_start'].to_i
279
+ self.class.accumulate_term(pre_last.year, LucaSupport::CONFIG['fy_start'], pre_last.year, pre_last.month)
280
+ elsif @start_date.month < LucaSupport::CONFIG['fy_start'].to_i
281
+ self.class.accumulate_term(pre_last.year - 1, LucaSupport::CONFIG['fy_start'], pre_last.year, pre_last.month)
282
+ end
283
+
284
+ base = Dict.latest_balance.each_with_object({}) do |(k, v), h|
285
+ h[k] = BigDecimal(v[:balance].to_s) if v[:balance]
286
+ end
287
+ if pre
288
+ idx = (pre.keys + base.keys).uniq
289
+ base = {}.tap do |h|
290
+ idx.each { |k| h[k] = (base[k] || BigDecimal('0')) + (pre[k] || BigDecimal('0')) }
291
+ end
292
+ end
293
+ self.class.total_subaccount(base)
294
+ end
295
+
173
296
  def self.sum_matched(report, reg)
174
297
  report.select { |k, v| reg.match(k)}.values.sum
175
298
  end
176
299
 
177
300
  # for assert purpose
301
+ #
178
302
  def self.gross(year, month = nil, code = nil, date_range = nil, rows = 4)
179
303
  if ! date_range.nil?
180
304
  raise if date_range.class != Range
181
305
  # TODO: date based range search
182
306
  end
183
307
 
184
- sum = { debit: {}, credit: {} }
308
+ sum = { debit: {}, credit: {}, debit_count: {}, credit_count: {} }
185
309
  idx_memo = []
186
- asof(year, month) do |f, _path|
310
+ search(year, month, nil, code) do |f, _path|
187
311
  CSV.new(f, headers: false, col_sep: "\t", encoding: 'UTF-8')
188
312
  .each_with_index do |row, i|
189
313
  break if i >= rows
314
+
190
315
  case i
191
316
  when 0
192
317
  idx_memo = row.map(&:to_s)
193
- idx_memo.each { |r| sum[:debit][r] ||= 0 }
318
+ next if code && !idx_memo.include?(code)
319
+
320
+ idx_memo.each do |r|
321
+ sum[:debit][r] ||= BigDecimal('0')
322
+ sum[:debit_count][r] ||= 0
323
+ end
194
324
  when 1
195
- row.each_with_index { |r, i| sum[:debit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
325
+ next if code && !idx_memo.include?(code)
326
+
327
+ row.each_with_index do |r, j|
328
+ sum[:debit][idx_memo[j]] += BigDecimal(r.to_s)
329
+ sum[:debit_count][idx_memo[j]] += 1
330
+ end
196
331
  when 2
197
332
  idx_memo = row.map(&:to_s)
198
- idx_memo.each { |r| sum[:credit][r] ||= 0 }
333
+ break if code && !idx_memo.include?(code)
334
+
335
+ idx_memo.each do |r|
336
+ sum[:credit][r] ||= BigDecimal('0')
337
+ sum[:credit_count][r] ||= 0
338
+ end
199
339
  when 3
200
- row.each_with_index { |r, i| sum[:credit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
340
+ row.each_with_index do |r, j|
341
+ sum[:credit][idx_memo[j]] += BigDecimal(r.to_s)
342
+ sum[:credit_count][idx_memo[j]] += 1
343
+ end
201
344
  else
202
345
  puts row # for debug
203
346
  end
204
347
  end
205
348
  end
349
+ if code
350
+ sum[:debit] = sum[:debit][code] || BigDecimal('0')
351
+ sum[:credit] = sum[:credit][code] || BigDecimal('0')
352
+ sum[:debit_count] = sum[:debit_count][code] || 0
353
+ sum[:credit_count] = sum[:credit_count][code] || 0
354
+ end
206
355
  sum
207
356
  end
208
357
 
209
358
  # netting vouchers in specified term
359
+ #
210
360
  def self.net(year, month = nil, code = nil, date_range = nil)
211
361
  g = gross(year, month, code, date_range)
212
362
  idx = (g[:debit].keys + g[:credit].keys).uniq.sort
213
- {}.tap do |sum|
363
+ count = {}
364
+ diff = {}.tap do |sum|
214
365
  idx.each do |code|
215
- sum[code] = g.dig(:debit, code).nil? ? 0 : calc_diff(g[:debit][code], code)
216
- sum[code] -= g.dig(:credit, code).nil? ? 0 : calc_diff(g[:credit][code], code)
366
+ sum[code] = g.dig(:debit, code).nil? ? BigDecimal('0') : Util.calc_diff(g[:debit][code], code)
367
+ sum[code] -= g.dig(:credit, code).nil? ? BigDecimal('0') : Util.calc_diff(g[:credit][code], code)
368
+ count[code] = (g.dig(:debit_count, code) || 0) + (g.dig(:credit_count, code) || 0)
217
369
  end
218
370
  end
371
+ [diff, count]
219
372
  end
220
373
 
221
- # TODO: replace load_tsv -> generic load_tsv_dict
374
+ # TODO: obsolete in favor of Dict.latest_balance()
222
375
  def load_start
223
- file = LucaSupport::Config::Pjdir + 'start.tsv'
224
- {}.tap do |dic|
225
- load_tsv(file) do |row|
226
- dic[row[0]] = row[2].to_i if ! row[2].nil?
227
- end
376
+ file = Pathname(LucaSupport::Config::Pjdir) / 'data' / 'balance' / 'start.tsv'
377
+ {}.tap do |dict|
378
+ LucaRecord::Dict.load_tsv_dict(file).each { |k, v| h[k] = v[:balance] if !v[:balance].nil? }
228
379
  end
229
380
  end
230
381
 
231
- def load_tsv(path)
232
- return enum_for(:load_tsv, path) unless block_given?
382
+ private
233
383
 
234
- data = CSV.read(path, headers: true, col_sep: "\t", encoding: 'UTF-8')
235
- data.each { |row| yield row }
236
- end
237
-
238
- def self.calc_diff(num, code)
239
- amount = /\./.match(num.to_s) ? BigDecimal(num) : num.to_i
240
- amount * pn_debit(code.to_s)
241
- end
384
+ def legal_items
385
+ return [] unless LucaSupport::Config::COUNTRY
242
386
 
243
- def self.pn_debit(code)
244
- case code
245
- when /^[0-4BCEGH]/
246
- 1
247
- when /^[5-9ADF]/
248
- -1
249
- else
250
- nil
387
+ case LucaSupport::Config::COUNTRY
388
+ when 'jp'
389
+ ['91', '911', '912', '913', '9131', '9132', '914', '9141', '9142', '915', '916', '92', '93']
251
390
  end
252
391
  end
253
-
254
- def dict
255
- LucaBook::Dict::Data
256
- end
257
392
  end
258
393
  end