lucabook 0.2.15 → 0.2.21

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