lucabook 0.2.8 → 0.2.17

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.
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'date'
5
+ require 'luca_support'
6
+ require 'luca_record'
7
+ require 'luca_record/dict'
8
+ require 'luca_book'
9
+
10
+ # Journal List on specified term
11
+ #
12
+ module LucaBook
13
+ class List < LucaBook::Journal
14
+ @dirname = 'journals'
15
+
16
+ def initialize(data, start_date, code = nil)
17
+ @data = data
18
+ @code = code
19
+ @start = start_date
20
+ @dict = LucaRecord::Dict.load('base.tsv')
21
+ end
22
+
23
+ def self.term(from_year, from_month, to_year = from_year, to_month = from_month, code: nil, basedir: @dirname)
24
+ data = LucaBook::Journal.term(from_year, from_month, to_year, to_month, code).select do |dat|
25
+ if code.nil?
26
+ true
27
+ else
28
+ [:debit, :credit].map { |key| serialize_on_key(dat[key], :code) }.flatten.include?(code)
29
+ end
30
+ end
31
+ new data, Date.new(from_year.to_i, from_month.to_i, 1), code
32
+ end
33
+
34
+ def list_on_code
35
+ calc_code
36
+ convert_label
37
+ @data = [code_header] + @data.map do |dat|
38
+ date, txid = LucaSupport::Code.decode_id(dat[:id])
39
+ {}.tap do |res|
40
+ res['code'] = dat[:code]
41
+ res['date'] = date
42
+ res['no'] = txid
43
+ res['id'] = dat[:id]
44
+ res['diff'] = dat[:diff]
45
+ res['balance'] = dat[:balance]
46
+ res['counter_code'] = dat[:counter_code].length == 1 ? dat[:counter_code].first : dat[:counter_code]
47
+ res['note'] = dat[:note]
48
+ end
49
+ end
50
+ self
51
+ end
52
+
53
+ def list_journals
54
+ convert_label
55
+ @data = @data.map do |dat|
56
+ date, txid = LucaSupport::Code.decode_id(dat[:id])
57
+ {}.tap do |res|
58
+ res['date'] = date
59
+ res['no'] = txid
60
+ res['id'] = dat[:id]
61
+ res['debit_code'] = dat[:debit].length == 1 ? dat[:debit][0][:code] : dat[:debit].map { |d| d[:code] }
62
+ res['debit_amount'] = dat[:debit].inject(0) { |sum, d| sum + d[:amount] }
63
+ res['credit_code'] = dat[:credit].length == 1 ? dat[:credit][0][:code] : dat[:credit].map { |d| d[:code] }
64
+ res['credit_amount'] = dat[:credit].inject(0) { |sum, d| sum + d[:amount] }
65
+ res['note'] = dat[:note]
66
+ end
67
+ end
68
+ self
69
+ end
70
+
71
+ def accumulate_code
72
+ @data.inject(BigDecimal('0')) do |sum, dat|
73
+ sum + Util.diff_by_code(dat[:debit], @code) - Util.diff_by_code(dat[:credit], @code)
74
+ end
75
+ end
76
+
77
+ def to_yaml
78
+ YAML.dump(LucaSupport::Code.readable(@data)).tap { |data| puts data }
79
+ end
80
+
81
+ private
82
+
83
+ def set_balance
84
+ return BigDecimal('0') if @code.nil? || /^[A-H]/.match(@code)
85
+
86
+ balance_dict = Dict.latest_balance
87
+ start_balance = BigDecimal(balance_dict.dig(@code.to_s, :balance) || '0')
88
+ start = Dict.issue_date(balance_dict)&.next_month
89
+ last = @start.prev_month
90
+ if last.year >= start.year && last.month >= start.month
91
+ start_balance + self.class.term(start.year, start.month, last.year, last.month, code: @code).accumulate_code
92
+ else
93
+ start_balance
94
+ end
95
+ end
96
+
97
+ def calc_code
98
+ @balance = set_balance
99
+ if @code
100
+ balance = @balance
101
+ @data.each do |dat|
102
+ dat[:diff] = Util.diff_by_code(dat[:debit], @code) - Util.diff_by_code(dat[:credit], @code)
103
+ balance += dat[:diff]
104
+ dat[:balance] = balance
105
+ dat[:code] = @code
106
+ counter = dat[:diff] * Util.pn_debit(@code) > 0 ? :credit : :debit
107
+ dat[:counter_code] = dat[counter].map { |d| d[:code] }
108
+ end
109
+ end
110
+ self
111
+ end
112
+
113
+ def convert_label
114
+ @data.each do |dat|
115
+ if @code
116
+ dat[:code] = "#{dat[:code]} #{@dict.dig(dat[:code], :label)}"
117
+ dat[:counter_code] = dat[:counter_code].map { |counter| "#{counter} #{@dict.dig(counter, :label)}" }
118
+ else
119
+ dat[:debit].each { |debit| debit[:code] = "#{debit[:code]} #{@dict.dig(debit[:code], :label)}" }
120
+ dat[:credit].each { |credit| credit[:code] = "#{credit[:code]} #{@dict.dig(credit[:code], :label)}" }
121
+ end
122
+ end
123
+ self
124
+ end
125
+
126
+ def dict
127
+ LucaBook::Dict::Data
128
+ end
129
+
130
+ def code_header
131
+ {}.tap do |h|
132
+ %w[code date no id diff balance counter_code note].each do |k|
133
+ h[k] = k == 'balance' ? @balance : ''
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_book'
4
+ require 'fileutils'
5
+
6
+ module LucaBook
7
+ class Setup
8
+ # create project skeleton under specified directory
9
+ def self.create_project(country = nil, dir = LucaSupport::Config::Pjdir)
10
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
11
+ Dir.chdir(dir) do
12
+ %w[data/journals data/balance dict].each do |subdir|
13
+ FileUtils.mkdir_p(subdir) unless Dir.exist?(subdir)
14
+ end
15
+ dict = if File.exist?("#{__dir__}/templates/dict-#{country}.tsv")
16
+ "dict-#{country}.tsv"
17
+ else
18
+ 'dict-en.tsv'
19
+ end
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
36
+ end
37
+ end
38
+ end
39
+ 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,11 @@ module LucaBook
17
17
 
18
18
  attr_reader :statement
19
19
 
20
- def initialize(data)
20
+ def initialize(data, count = nil)
21
21
  @data = data
22
+ @count = count
22
23
  @dict = LucaRecord::Dict.load('base.tsv')
24
+ @start_balance = set_balance
23
25
  end
24
26
 
25
27
  # TODO: not compatible with LucaRecord::Base.open_records
@@ -41,66 +43,42 @@ module LucaBook
41
43
  last_date = Date.new(to_year.to_i, to_month.to_i, -1)
42
44
  raise 'invalid term specified' if date > last_date
43
45
 
46
+ counts = []
44
47
  reports = [].tap do |r|
45
48
  while date <= last_date do
46
- r << accumulate_month(date.year, date.month)
49
+ diff, count = accumulate_month(date.year, date.month)
50
+ r << diff
51
+ counts << count
47
52
  date = date.next_month
48
53
  end
49
54
  end
50
- new reports
55
+ new reports, counts
51
56
  end
52
57
 
53
58
  def by_code(code, year=nil, month=nil)
54
- raise "not supported year range yet" if ! year.nil? && month.nil?
59
+ raise 'not supported year range yet' if ! year.nil? && month.nil?
55
60
 
56
- bl = @book.load_start.dig(code) || 0
57
- full_term = scan_terms(LucaSupport::Config::Pjdir)
61
+ balance = @book.load_start.dig(code) || 0
62
+ full_term = self.class.scan_terms
58
63
  if ! month.nil?
59
- pre_term = full_term.select{|y,m| y <= year.to_i && m < month.to_i }
60
- bl += pre_term.map{|y,m| self.class.net(y, m)}.inject(0){|sum, h| sum + h[code]}
61
- [{ code: code, balance: bl, note: "#{code} #{dict.dig(code, :label)}" }] + records_with_balance(year, month, code, bl)
64
+ pre_term = full_term.select { |y, m| y <= year.to_i && m < month.to_i }
65
+ balance += pre_term.map { |y, m| self.class.net(y, m)}.inject(0){|sum, h| sum + h[code] }
66
+ [{ code: code, balance: balance, note: "#{code} #{@dict.dig(code, :label)}" }] + records_with_balance(year, month, code, balance)
62
67
  else
63
- start = { code: code, balance: bl, note: "#{code} #{dict.dig(code, :label)}" }
64
- full_term.map {|y, m| y }.uniq.map {|y|
65
- records_with_balance(y, nil, code, bl)
68
+ start = { code: code, balance: balance, note: "#{code} #{@dict.dig(code, :label)}" }
69
+ full_term.map { |y, m| y }.uniq.map { |y|
70
+ records_with_balance(y, nil, code, balance)
66
71
  }.flatten.prepend(start)
67
72
  end
68
73
  end
69
74
 
70
75
  def records_with_balance(year, month, code, balance)
71
76
  @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)
77
+ balance += Util.calc_diff(Util.amount_by_code(h[:debit], code), code) - Util.calc_diff(Util.amount_by_code(h[:credit], code), code)
73
78
  h[:balance] = balance
74
79
  end
75
80
  end
76
81
 
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 = scan_terms(@book.pjdir).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
82
  def to_yaml
105
83
  YAML.dump(code2label).tap { |data| puts data }
106
84
  end
@@ -114,33 +92,105 @@ module LucaBook
114
92
  end
115
93
  end
116
94
 
117
- def bs
118
- @statement = @data.map do |data|
119
- data.select { |k, v| /^[0-9].+/.match(k) }
95
+ def stats(level = nil)
96
+ keys = @count.map(&:keys).flatten.uniq.sort
97
+ @count.map! do |data|
98
+ keys.each do |k|
99
+ data[k] ||= 0
100
+ next if level.nil? || k.length <= level
101
+
102
+ if data[k[0, level]]
103
+ data[k[0, level]] += data[k]
104
+ else
105
+ data[k[0, level]] = data[k]
106
+ end
107
+ end
108
+ data.select! { |k, _v| k.length <= level } if level
109
+ data.sort.to_h
120
110
  end
121
- self
111
+ keys.map! { |k| k[0, level] }.uniq.select! { |k| k.length <= level } if level
112
+ @count.prepend({}.tap { |header| keys.each { |k| header[k] = @dict.dig(k, :label) }})
113
+ puts YAML.dump(@count)
114
+ @count
122
115
  end
123
116
 
124
- def pl
125
- @statement = @data.map do |data|
126
- data.select { |k, v| /^[A-F].+/.match(k) }
117
+ def bs(level = 3, legal: false)
118
+ @data.map! { |data| data.select { |k, _v| k.length <= level } }
119
+ @data.map! { |data| code_sum(data).merge(data) } if legal
120
+ base = accumulate_balance(@data)
121
+ length = [base[:debit].length, base[:credit].length].max
122
+ @statement = [].tap do |a|
123
+ length.times do |i|
124
+ {}.tap do |res|
125
+ res['debit_label'] = base[:debit][i] ? @dict.dig(base[:debit][i].keys[0], :label) : ''
126
+ res['debit_balance'] = base[:debit][i] ? @start_balance.dig(base[:debit][i].keys[0]) + base[:debit][i].values[0] : ''
127
+ res['debit_diff'] = base[:debit][i] ? base[:debit][i].values[0] : ''
128
+ res['credit_label'] = base[:credit][i] ? @dict.dig(base[:credit][i].keys[0], :label) : ''
129
+ res['credit_balance'] = base[:credit][i] ? @start_balance.dig(base[:credit][i].keys[0]) + base[:credit][i].values[0] : ''
130
+ res['credit_diff'] = base[:credit][i] ? base[:credit][i].values[0] : ''
131
+ a << res
132
+ end
133
+ end
127
134
  end
135
+ puts YAML.dump(@statement)
128
136
  self
129
137
  end
130
138
 
131
- def self.accumulate_month(year, month)
132
- monthly_record = net(year, month)
133
- total_subaccount(monthly_record)
139
+ def accumulate_balance(monthly_diffs)
140
+ data = monthly_diffs.each_with_object({}) do |month, h|
141
+ month.each do |k, v|
142
+ h[k] = h[k].nil? ? v : h[k] + v
143
+ end
144
+ end.sort.to_h
145
+ { debit: [], credit: [] }.tap do |res|
146
+ data.each do |k, v|
147
+ case k
148
+ when /^[0-4].*/
149
+ res[:debit] << { k => v }
150
+ when /^[5-9H].*/
151
+ res[:credit] << { k => v }
152
+ end
153
+ end
154
+ end
134
155
  end
135
156
 
136
- def amount_by_code(items, code)
137
- items
138
- .select{|item| item.dig(:code) == code }
139
- .inject(0){|sum, item| sum + item[:amount] }
157
+ def pl
158
+ @statement = @data.map { |data| data.select { |k, _v| /^[A-H].+/.match(k) } }
159
+ @statement << @statement.each_with_object({}) { |item, h| item.each { |k, v| h[k].nil? ? h[k] = v : h[k] += v } }
160
+ self
140
161
  end
141
162
 
163
+ def self.accumulate_month(year, month)
164
+ monthly_record, count = net(year, month)
165
+ [total_subaccount(monthly_record), count]
166
+ end
167
+
168
+ # Accumulate Level 2, 3 account.
169
+ #
142
170
  def self.total_subaccount(report)
143
- report.dup.tap do |res|
171
+ {}.tap do |res|
172
+ res['10'] = sum_matched(report, /^[123][0-9A-Z]{2,}/)
173
+ res['40'] = sum_matched(report, /^[4][0-9A-Z]{2,}/)
174
+ res['50'] = sum_matched(report, /^[56][0-9A-Z]{2,}/)
175
+ res['70'] = sum_matched(report, /^[78][0-9A-Z]{2,}/)
176
+ res['8ZZ'] = res['50'] + res['70']
177
+ res['9ZZ'] = sum_matched(report, /^[9][0-9A-Z]{2,}/)
178
+ res['A0'] = sum_matched(report, /^[A][0-9A-Z]{2,}/)
179
+ res['B0'] = sum_matched(report, /^[B][0-9A-Z]{2,}/)
180
+ res['BA'] = res['A0'] - res['B0']
181
+ res['C0'] = sum_matched(report, /^[C][0-9A-Z]{2,}/)
182
+ res['CA'] = res['BA'] - res['C0']
183
+ res['D0'] = sum_matched(report, /^[D][0-9A-Z]{2,}/)
184
+ res['E0'] = sum_matched(report, /^[E][0-9A-Z]{2,}/)
185
+ res['EA'] = res['CA'] + res['D0'] - res['E0']
186
+ res['F0'] = sum_matched(report, /^[F][0-9A-Z]{2,}/)
187
+ res['G0'] = sum_matched(report, /^[G][0-9A-Z]{2,}/)
188
+ res['GA'] = res['EA'] + res['F0'] - res['G0']
189
+ res['HA'] = res['GA'] - sum_matched(report, /^[H][0-9A-Z]{2,}/)
190
+
191
+ res['1'] = res['10'] + res['40']
192
+ res['5'] = res['8ZZ'] + res['9ZZ'] + res['HA']
193
+
144
194
  report.each do |k, v|
145
195
  if k.length >= 4
146
196
  if res[k[0, 3]]
@@ -150,26 +200,23 @@ module LucaBook
150
200
  end
151
201
  end
152
202
  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]/)
203
+ res.sort.to_h
170
204
  end
171
205
  end
172
206
 
207
+ def code_sum(report)
208
+ legal_items.each.with_object({}) do |k, h|
209
+ h[k] = self.class.sum_matched(report, /^#{k}.*/)
210
+ end
211
+ end
212
+
213
+ def set_balance
214
+ base = Dict.latest_balance.each_with_object({}) do |(k, v), h|
215
+ h[k] = v[:balance].to_i if v[:balance]
216
+ end
217
+ code_sum(base).merge(self.class.total_subaccount(base))
218
+ end
219
+
173
220
  def self.sum_matched(report, reg)
174
221
  report.select { |k, v| reg.match(k)}.values.sum
175
222
  end
@@ -181,7 +228,7 @@ module LucaBook
181
228
  # TODO: date based range search
182
229
  end
183
230
 
184
- sum = { debit: {}, credit: {} }
231
+ sum = { debit: {}, credit: {}, debit_count: {}, credit_count: {} }
185
232
  idx_memo = []
186
233
  asof(year, month) do |f, _path|
187
234
  CSV.new(f, headers: false, col_sep: "\t", encoding: 'UTF-8')
@@ -190,14 +237,26 @@ module LucaBook
190
237
  case i
191
238
  when 0
192
239
  idx_memo = row.map(&:to_s)
193
- idx_memo.each { |r| sum[:debit][r] ||= 0 }
240
+ idx_memo.each do |r|
241
+ sum[:debit][r] ||= 0
242
+ sum[:debit_count][r] ||= 0
243
+ end
194
244
  when 1
195
- row.each_with_index { |r, i| sum[:debit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
245
+ row.each_with_index do |r, j|
246
+ sum[:debit][idx_memo[j]] += r.to_i # TODO: bigdecimal support
247
+ sum[:debit_count][idx_memo[j]] += 1
248
+ end
196
249
  when 2
197
250
  idx_memo = row.map(&:to_s)
198
- idx_memo.each { |r| sum[:credit][r] ||= 0 }
251
+ idx_memo.each do |r|
252
+ sum[:credit][r] ||= 0
253
+ sum[:credit_count][r] ||= 0
254
+ end
199
255
  when 3
200
- row.each_with_index { |r, i| sum[:credit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
256
+ row.each_with_index do |r, j|
257
+ sum[:credit][idx_memo[j]] += r.to_i # TODO: bigdecimal support
258
+ sum[:credit_count][idx_memo[j]] += 1
259
+ end
201
260
  else
202
261
  puts row # for debug
203
262
  end
@@ -210,17 +269,20 @@ module LucaBook
210
269
  def self.net(year, month = nil, code = nil, date_range = nil)
211
270
  g = gross(year, month, code, date_range)
212
271
  idx = (g[:debit].keys + g[:credit].keys).uniq.sort
213
- {}.tap do |sum|
272
+ count = {}
273
+ diff = {}.tap do |sum|
214
274
  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)
275
+ sum[code] = g.dig(:debit, code).nil? ? 0 : Util.calc_diff(g[:debit][code], code)
276
+ sum[code] -= g.dig(:credit, code).nil? ? 0 : Util.calc_diff(g[:credit][code], code)
277
+ count[code] = (g.dig(:debit_count, code) || 0) + (g.dig(:credit_count, code) || 0)
217
278
  end
218
279
  end
280
+ [diff, count]
219
281
  end
220
282
 
221
283
  # TODO: replace load_tsv -> generic load_tsv_dict
222
284
  def load_start
223
- file = LucaSupport::Config::Pjdir + 'start.tsv'
285
+ file = Pathname(LucaSupport::Config::Pjdir) / 'data' / 'balance' / 'start.tsv'
224
286
  {}.tap do |dic|
225
287
  load_tsv(file) do |row|
226
288
  dic[row[0]] = row[2].to_i if ! row[2].nil?
@@ -235,24 +297,15 @@ module LucaBook
235
297
  data.each { |row| yield row }
236
298
  end
237
299
 
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
300
+ private
242
301
 
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
251
- end
252
- end
302
+ def legal_items
303
+ return [] unless LucaSupport::Config::COUNTRY
253
304
 
254
- def dict
255
- LucaBook::Dict::Data
305
+ case LucaSupport::Config::COUNTRY
306
+ when 'jp'
307
+ ['91', '911', '912', '913', '9131', '9132', '914', '9141', '9142', '915', '916', '92', '93']
308
+ end
256
309
  end
257
310
  end
258
311
  end