lucabook 0.2.23 → 0.2.28

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 866a73f9e372f2ac755ed301c9e9b88bebb556b53a5ffd604391c45eda927653
4
- data.tar.gz: 41b02c0e5c567dbfedf37fe4148d11270a0f8776c6fcc78f630354e281aad5d3
3
+ metadata.gz: 0b2cc0f6aefd92521a518af25010f4fcb125e32aff9b71c9a18304b606451556
4
+ data.tar.gz: 6689918d842fca5a64060f998ea419aeb2f8db4350c7b0073e39b6a07b0dbb43
5
5
  SHA512:
6
- metadata.gz: 1f295b8ab521f4b95c81ec57c57579ebf87ae01c3f1acf2fbc564bf076731d1b1d089e0959660752529e190eecfa3e107ee184b6f91484f9bdc61977eda80886
7
- data.tar.gz: '0820bc3b42bfd48b0cf8f2924774acc8887c6fda373ec71fcebab5152b46e292096b68f174ac643613fda4ef72dc62a79300b4fa82e306d144f2fe404a770213'
6
+ metadata.gz: 192298f7a6cd006f5699ec02a109c6f4e10f9240ebc7fc228af277265eab5b467e90bcbd58063f8381b61f258d99732d69155c90af4fac619d9c94da43a2fb27
7
+ data.tar.gz: 1231261012e602b438469cbcd357dc64a58c14357b8a0c63c4b5fa7dbe398f2b5080a3d53c404c63f543ff87f681f7fc38cb183ea35d5528ccbc905cdcebfaa1
data/exe/luca-book CHANGED
@@ -19,12 +19,12 @@ class LucaCmd
19
19
  end
20
20
 
21
21
  def self.list(args, params)
22
- args = gen_range(params[:n] || 1) if args.empty?
22
+ args = gen_range(params[:n]) if args.empty?
23
23
  if params['code']
24
24
  if params['headers']
25
25
  render(LucaBook::ListByHeader.term(*args, code: params['code'], header: params['headers']).list_by_code, params)
26
26
  else
27
- render(LucaBook::List.term(*args, code: params['code']).list_by_code, params)
27
+ render(LucaBook::List.term(*args, code: params['code'], recursive: params[:recursive]).list_by_code(params[:recursive]), params)
28
28
  end
29
29
  else
30
30
  render(LucaBook::List.term(*args).list_journals, params)
@@ -34,14 +34,30 @@ class LucaCmd
34
34
  def self.stats(args, params)
35
35
  args = gen_range(params[:n]) if args.empty?
36
36
  if params['code']
37
- render(LucaBook::State.by_code(params['code'], *args), params)
37
+ render(LucaBook::State.by_code(params['code'], *args, recursive: params[:recursive]), params)
38
38
  else
39
39
  render(LucaBook::State.range(*args).stats(params[:level]), params)
40
40
  end
41
41
  end
42
+
43
+ def self.add_header(args, params)
44
+ args = gen_range(params[:n] || 1) if args.empty?
45
+ if params['code']
46
+ LucaBook::List.add_header(*args, code: params['code'], header_key: params[:key], header_val: params[:value])
47
+ else
48
+ puts 'no code specified.'
49
+ end
50
+ end
42
51
  end
43
52
 
44
53
  class Report < LucaCmd
54
+ def self.xbrl(args, params)
55
+ level = params[:level] || 3
56
+ legal = params[:legal] || false
57
+ args = gen_range(params[:n] || 1) if args.empty?
58
+ LucaBook::State.range(*args).render_xbrl(params[:output])
59
+ end
60
+
45
61
  def self.balancesheet(args, params)
46
62
  level = params[:level] || 3
47
63
  legal = params[:legal] || false
@@ -54,13 +70,27 @@ class LucaCmd
54
70
  args = gen_range(params[:n]) if args.empty?
55
71
  render(LucaBook::State.range(*args).pl(level), params)
56
72
  end
73
+
74
+ def self.report_mail(args, params)
75
+ level = params[:level] || 3
76
+ args = gen_range(params[:n] || 12) if args.empty?
77
+ render(LucaBook::State.range(*args).report_mail(level), params)
78
+ end
57
79
  end
58
80
 
59
- def self.gen_range(count)
60
- count ||= 3
81
+ def self.gen_range(count = nil)
61
82
  today = Date.today
62
- start = today.prev_month(count - 1)
63
- [start.year, start.month, today.year, today.month]
83
+ if count
84
+ start = today.prev_month(count - 1)
85
+ [start.year, start.month, today.year, today.month]
86
+ else
87
+ start_year = if today.month >= LucaSupport::CONFIG['fy_start'].to_i
88
+ today.year
89
+ else
90
+ today.year - 1
91
+ end
92
+ [start_year, LucaSupport::CONFIG['fy_start'], start_year + 1, LucaSupport::CONFIG['fy_start'].to_i - 1]
93
+ end
64
94
  end
65
95
 
66
96
  def self.render(dat, params)
@@ -73,6 +103,12 @@ class LucaCmd
73
103
  puts YAML.dump(dat)
74
104
  end
75
105
  end
106
+
107
+ class Dict < LucaCmd
108
+ def self.update_balance(args, params)
109
+ LucaBook::Dict.generate_balance(*args)
110
+ end
111
+ end
76
112
  end
77
113
 
78
114
  def new_pj(args = nil, params = {})
@@ -96,9 +132,11 @@ when /journals?/, 'j'
96
132
  LucaCmd::Journal.import(args, params)
97
133
  end
98
134
  when 'list'
135
+ params[:recursive] = false
99
136
  OptionParser.new do |opt|
100
137
  opt.banner = 'Usage: luca-book journals list [options] [YYYY M]'
101
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
138
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
139
+ opt.on('-r', '--recursive', 'include subaccounts') { |_v| params[:recursive] = true }
102
140
  opt.on('--customer', 'categorize by x-customer header') { |_v| params['headers'] = 'x-customer' }
103
141
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
104
142
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
@@ -107,10 +145,22 @@ when /journals?/, 'j'
107
145
  args = opt.parse!(ARGV)
108
146
  LucaCmd::Journal.list(args, params)
109
147
  end
148
+ when 'set'
149
+ OptionParser.new do |opt|
150
+ opt.banner = 'Usage: luca-book journals set [options] [YYYY M]'
151
+ opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
152
+ opt.on('--header VAL', 'header key') { |v| params[:key] = v }
153
+ opt.on('--val VAL', 'header value') { |v| params[:value] = v }
154
+ opt.on_tail('set header to journals on specified code.')
155
+ args = opt.parse!(ARGV)
156
+ LucaCmd::Journal.add_header(args, params)
157
+ end
110
158
  when 'stats'
159
+ params[:recursive] = false
111
160
  OptionParser.new do |opt|
112
161
  opt.banner = 'Usage: luca-book journals stats [options] [YYYY M]'
113
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
162
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
163
+ opt.on('-r', '--recursive', 'include subaccounts') { |_v| params[:recursive] = true }
114
164
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
115
165
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
116
166
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
@@ -136,6 +186,13 @@ when 'new'
136
186
  when /reports?/, 'r'
137
187
  subcmd = ARGV.shift
138
188
  case subcmd
189
+ when 'xbrl'
190
+ OptionParser.new do |opt|
191
+ opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
192
+ opt.on('-o', '--output VAL', 'output filename') { |v| params[:output] = v }
193
+ args = opt.parse!(ARGV)
194
+ LucaCmd::Report.xbrl(args, params)
195
+ end
139
196
  when 'bs'
140
197
  OptionParser.new do |opt|
141
198
  opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
@@ -156,6 +213,14 @@ when /reports?/, 'r'
156
213
  args = opt.parse!(ARGV)
157
214
  LucaCmd::Report.profitloss(args, params)
158
215
  end
216
+ when 'mail'
217
+ OptionParser.new do |opt|
218
+ opt.banner = 'Usage: luca-book reports mail [options] [YYYY M YYYY M]'
219
+ opt.on('-l', '--level VAL', 'account level') { |v| params[:level] = v.to_i }
220
+ opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
221
+ args = opt.parse!(ARGV)
222
+ LucaCmd::Report.report_mail(args, params)
223
+ end
159
224
  else
160
225
  puts 'Proper subcommand needed.'
161
226
  puts
@@ -164,6 +229,16 @@ when /reports?/, 'r'
164
229
  puts ' pl: show statement of income'
165
230
  exit 1
166
231
  end
232
+ when /balance/
233
+ subcmd = ARGV.shift
234
+ case subcmd
235
+ when 'update'
236
+ OptionParser.new do |opt|
237
+ opt.banner = 'Usage: luca-book balance update YYYY [M]'
238
+ args = opt.parse!(ARGV)
239
+ LucaCmd::Dict.update_balance(args, params)
240
+ end
241
+ end
167
242
  else
168
243
  puts 'Proper subcommand needed.'
169
244
  puts
data/lib/luca_book.rb CHANGED
@@ -5,6 +5,7 @@ require 'luca_record'
5
5
  require 'luca_book/version'
6
6
 
7
7
  module LucaBook
8
+ autoload :Accumulator, 'luca_book/accumulator'
8
9
  autoload :Dict, 'luca_book/dict'
9
10
  autoload :Import, 'luca_book/import'
10
11
  autoload :Journal, 'luca_book/journal'
@@ -12,5 +13,6 @@ module LucaBook
12
13
  autoload :ListByHeader, 'luca_book/list_by_header'
13
14
  autoload :Setup, 'luca_book/setup'
14
15
  autoload :State, 'luca_book/state'
16
+ autoload :Test, 'luca_book/test'
15
17
  autoload :Util, 'luca_book/util'
16
18
  end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_book/util'
4
+ require 'luca_support/config'
5
+
6
+ module LucaBook
7
+ module Accumulator
8
+ def self.included(klass) # :nodoc:
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def accumulate_month(year, month)
14
+ monthly_record, count = net(year, month)
15
+ [total_subaccount(monthly_record), count]
16
+ end
17
+
18
+ # Accumulate Level 2, 3 account.
19
+ #
20
+ def total_subaccount(report)
21
+ {}.tap do |res|
22
+ res['A0'] = sum_matched(report, /^[A][0-9A-Z]{2,}/)
23
+ res['B0'] = sum_matched(report, /^[B][0-9A-Z]{2,}/)
24
+ res['BA'] = res['A0'] - res['B0']
25
+ res['C0'] = sum_matched(report, /^[C][0-9A-Z]{2,}/)
26
+ res['CA'] = res['BA'] - res['C0']
27
+ res['D0'] = sum_matched(report, /^[D][0-9A-Z]{2,}/)
28
+ res['E0'] = sum_matched(report, /^[E][0-9A-Z]{2,}/)
29
+ res['EA'] = res['CA'] + res['D0'] - res['E0']
30
+ res['F0'] = sum_matched(report, /^[F][0-9A-Z]{2,}/)
31
+ res['G0'] = sum_matched(report, /^[G][0-9][0-9A-Z]{1,}/)
32
+ res['GA'] = res['EA'] + res['F0'] - res['G0']
33
+ res['H0'] = sum_matched(report, /^[H][0-9][0-9A-Z]{1,}/)
34
+ res['HA'] = res['GA'] - res['H0']
35
+
36
+ report['9142'] = (report['9142'] || BigDecimal('0')) + res['HA']
37
+ res['9142'] = report['9142']
38
+ res['10'] = sum_matched(report, /^[12][0-9A-Z]{2,}/)
39
+ jp_4v = sum_matched(report, /^[4][V]{2,}/) # deferred assets for JP GAAP
40
+ res['30'] = sum_matched(report, /^[34][0-9A-Z]{2,}/) - jp_4v
41
+ res['4V'] = jp_4v if LucaSupport::CONFIG['country'] == 'jp'
42
+ res['50'] = sum_matched(report, /^[56][0-9A-Z]{2,}/)
43
+ res['70'] = sum_matched(report, /^[78][0-9A-Z]{2,}/)
44
+ res['91'] = sum_matched(report, /^91[0-9A-Z]{1,}/)
45
+ res['8ZZ'] = res['50'] + res['70']
46
+ res['9ZZ'] = sum_matched(report, /^[9][0-9A-Z]{2,}/)
47
+
48
+ res['1'] = res['10'] + res['30']
49
+ res['5'] = res['8ZZ'] + res['9ZZ']
50
+ res['_d'] = report['_d']
51
+
52
+ report.each do |k, v|
53
+ res[k] ||= sum_matched(report, /^#{k}[0-9A-Z]{1,}/) if k.length == 2
54
+ res[k] = v if k.length == 3
55
+ end
56
+
57
+ report.each do |k, v|
58
+ if k.length >= 4
59
+ if res[k[0, 3]]
60
+ res[k[0, 3]] += v
61
+ else
62
+ res[k[0, 3]] = v
63
+ end
64
+ res[k] = v
65
+ end
66
+ end
67
+ res.sort.to_h
68
+ end
69
+ end
70
+
71
+ def sum_matched(report, reg)
72
+ report.select { |k, v| reg.match(k)}.values.sum
73
+ end
74
+
75
+ # for assert purpose
76
+ #
77
+ def gross(start_year, start_month, end_year = nil, end_month = nil, code: nil, date_range: nil, rows: 4, recursive: false)
78
+ if ! date_range.nil?
79
+ raise if date_range.class != Range
80
+ # TODO: date based range search
81
+ end
82
+
83
+ end_year ||= start_year
84
+ end_month ||= start_month
85
+ sum = { debit: {}, credit: {}, debit_count: {}, credit_count: {} }
86
+ idx_memo = []
87
+ term(start_year, start_month, end_year, end_month, code, 'journals') do |f, _path|
88
+ CSV.new(f, headers: false, col_sep: "\t", encoding: 'UTF-8')
89
+ .each_with_index do |row, i|
90
+ break if i >= rows
91
+
92
+ case i
93
+ when 0
94
+ idx_memo = row.map(&:to_s)
95
+ next if code && idx_memo.select { |idx| /^#{code}/.match(idx) }.empty?
96
+
97
+ idx_memo.each do |r|
98
+ sum[:debit][r] ||= BigDecimal('0')
99
+ sum[:debit_count][r] ||= 0
100
+ end
101
+ when 1
102
+ next if code && idx_memo.select { |idx| /^#{code}/.match(idx) }.empty?
103
+
104
+ row.each_with_index do |r, j|
105
+ sum[:debit][idx_memo[j]] += BigDecimal(r.to_s)
106
+ sum[:debit_count][idx_memo[j]] += 1
107
+ end
108
+ when 2
109
+ idx_memo = row.map(&:to_s)
110
+ break if code && idx_memo.select { |idx| /^#{code}/.match(idx) }.empty?
111
+
112
+ idx_memo.each do |r|
113
+ sum[:credit][r] ||= BigDecimal('0')
114
+ sum[:credit_count][r] ||= 0
115
+ end
116
+ when 3
117
+ row.each_with_index do |r, j|
118
+ sum[:credit][idx_memo[j]] += BigDecimal(r.to_s)
119
+ sum[:credit_count][idx_memo[j]] += 1
120
+ end
121
+ else
122
+ puts row # for debug
123
+ end
124
+ end
125
+ end
126
+ return sum if code.nil?
127
+
128
+ codes = if recursive
129
+ sum[:debit].keys.concat(sum[:credit].keys).uniq.select { |k| /^#{code}/.match(k) }
130
+ else
131
+ Array(code)
132
+ end
133
+ res = { debit: { code => 0 }, credit: { code => 0 }, debit_count: { code => 0 }, credit_count: { code => 0 } }
134
+ codes.each do |cd|
135
+ res[:debit][code] += sum[:debit][cd] || BigDecimal('0')
136
+ res[:credit][code] += sum[:credit][cd] || BigDecimal('0')
137
+ res[:debit_count][code] += sum[:debit_count][cd] || 0
138
+ res[:credit_count][code] += sum[:credit_count][cd] || 0
139
+ end
140
+ res
141
+ end
142
+
143
+ # netting vouchers in specified term
144
+ #
145
+ def net(start_year, start_month, end_year = nil, end_month = nil, code: nil, date_range: nil, recursive: false)
146
+ g = gross(start_year, start_month, end_year, end_month, code: code, date_range: date_range, recursive: recursive)
147
+ idx = (g[:debit].keys + g[:credit].keys).uniq.sort
148
+ count = {}
149
+ diff = {}.tap do |sum|
150
+ idx.each do |code|
151
+ sum[code] = g.dig(:debit, code).nil? ? BigDecimal('0') : LucaBook::Util.calc_diff(g[:debit][code], code)
152
+ sum[code] -= g.dig(:credit, code).nil? ? BigDecimal('0') : LucaBook::Util.calc_diff(g[:credit][code], code)
153
+ count[code] = (g.dig(:debit_count, code) || 0) + (g.dig(:credit_count, code) || 0)
154
+ end
155
+ end
156
+ [diff, count]
157
+ end
158
+
159
+ # Override LucaRecord::IO.load_data
160
+ #
161
+ def load_data(io, path = nil)
162
+ io
163
+ end
164
+ end
165
+
166
+ def net_amount(code, start_year = nil, start_month = nil, end_year = nil, end_month = nil, recursive: true)
167
+ start_year ||= @cursor_start&.year || @start_date.year
168
+ start_month ||= @cursor_start&.month || @start_date.month
169
+ end_year ||= @cursor_end&.year || @end_date.year
170
+ end_month ||= @cursor_end&.month || @end_date.month
171
+ self.class.net(start_year, start_month, end_year, end_month, code: code, recursive: recursive)[0][code]
172
+ end
173
+
174
+ def debit_amount(code, start_year = nil, start_month = nil, end_year = nil, end_month = nil, recursive: true)
175
+ gross_amount(code, start_year, start_month, end_year, end_month, recursive: recursive)[0]
176
+ end
177
+
178
+ def credit_amount(code, start_year = nil, start_month = nil, end_year = nil, end_month = nil, recursive: true)
179
+ gross_amount(code, start_year, start_month, end_year, end_month, recursive: recursive)[1]
180
+ end
181
+
182
+ def gross_amount(code, start_year = nil, start_month = nil, end_year = nil, end_month = nil, recursive: true)
183
+ start_year ||= @cursor_start&.year || @start_date.year
184
+ start_month ||= @cursor_start&.month || @start_date.month
185
+ end_year ||= @cursor_end&.year || @end_date.year
186
+ end_month ||= @cursor_end&.month || @end_date.month
187
+ g = self.class.gross(start_year, start_month, end_year, end_month, code: code, recursive: recursive)
188
+ [g[:debit][code], g[:credit][code]]
189
+ end
190
+
191
+ def debit_count(code, start_year = nil, start_month = nil, end_year = nil, end_month = nil, recursive: true)
192
+ gross_count(code, start_year, start_month, end_year, end_month, recursive: recursive)[0]
193
+ end
194
+
195
+ def credit_count(code, start_year = nil, start_month = nil, end_year = nil, end_month = nil, recursive: true)
196
+ gross_count(code, start_year, start_month, end_year, end_month, recursive: recursive)[1]
197
+ end
198
+
199
+ def gross_count(code, start_year = nil, start_month = nil, end_year = nil, end_month = nil, recursive: true)
200
+ start_year ||= @cursor_start&.year || @start_date.year
201
+ start_month ||= @cursor_start&.month || @start_date.month
202
+ end_year ||= @cursor_end&.year || @end_date.year
203
+ end_month ||= @cursor_end&.month || @end_date.month
204
+ g = self.class.gross(start_year, start_month, end_year, end_month, code: code, recursive: recursive)
205
+ [g[:debit_count][code], g[:credit_count][code]]
206
+ end
207
+
208
+ def each_month
209
+ yield
210
+ end
211
+ end
212
+ end
@@ -49,7 +49,7 @@ class LucaBookConsole
49
49
  print " " if h[:code].length > 3
50
50
  end
51
51
  puts cnsl_label(h[:label], h[:code])
52
- h[:value].each_slice(6) do |v|
52
+ h[:amount].each_slice(6) do |v|
53
53
  puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
54
54
  end
55
55
  end
@@ -72,13 +72,13 @@ class LucaBookConsole
72
72
  end
73
73
  convert_collection(report).each do |h|
74
74
  if /^[A-Z]/.match(h[:code])
75
- total = [h[:value].inject(:+)] + Array.new(h[:value].length)
75
+ total = [h[:amount].inject(:+)] + Array.new(h[:amount].length)
76
76
  if /[^0]$/.match(h[:code])
77
77
  print " "
78
78
  print " " if h[:code].length > 3
79
79
  end
80
80
  puts cnsl_label(h[:label], h[:code])
81
- h[:value].each_slice(6).with_index(0) do |v, i|
81
+ h[:amount].each_slice(6).with_index(0) do |v, i|
82
82
  puts "#{cnsl_fmt(total[i], 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
83
83
  end
84
84
  end
@@ -98,7 +98,7 @@ class LucaBookConsole
98
98
  end
99
99
  end
100
100
  }.sort.map do |k,v|
101
- {code: k, label: @report.dict.dig(k, :label), value: v}
101
+ {code: k, label: @report.dict.dig(k, :label), amount: v}
102
102
  end
103
103
  end
104
104
 
@@ -1,12 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'luca_support/code'
3
4
  require 'luca_support/config'
5
+ require 'luca_support/range'
4
6
  require 'luca_record/dict'
7
+ require 'luca_record/io'
8
+ require 'luca_book'
5
9
  require 'date'
6
10
  require 'pathname'
7
11
 
8
12
  module LucaBook
9
13
  class Dict < LucaRecord::Dict
14
+ include LucaSupport::Range
15
+ include LucaBook::Util
16
+ include LucaRecord::IO
17
+ include Accumulator
18
+
19
+ @dirname = 'journals'
10
20
  # Column number settings for CSV/TSV convert
11
21
  #
12
22
  # :label
@@ -15,10 +25,10 @@ module LucaBook
15
25
  # must be specified with label
16
26
  # :debit_label
17
27
  # for double entry data
18
- # * debit_value
28
+ # * debit_amount
19
29
  # :credit_label
20
30
  # for double entry data
21
- # * credit_value
31
+ # * credit_amount
22
32
  # :note
23
33
  # can be the same column as another label
24
34
  #
@@ -41,8 +51,8 @@ module LucaBook
41
51
  end
42
52
  end
43
53
  config[:type] ||= 'invalid'
44
- config[:debit_value] = @config['debit_value'].to_i if @config.dig('debit_value')
45
- config[:credit_value] = @config['credit_value'].to_i if @config.dig('credit_value')
54
+ config[:debit_amount] = @config['debit_amount'].to_i if @config.dig('debit_amount')
55
+ config[:credit_amount] = @config['credit_amount'].to_i if @config.dig('credit_amount')
46
56
  config[:note] = @config['note'] if @config.dig('note')
47
57
  config[:encoding] = @config['encoding'] if @config.dig('encoding')
48
58
 
@@ -84,14 +94,83 @@ module LucaBook
84
94
  nil
85
95
  end
86
96
 
87
- def self.latest_balance
97
+ # Find balance at financial year start by given date.
98
+ # If not found 'start-yyyy-mm-*.tsv', use 'start.tsv' as default.
99
+ #
100
+ def self.latest_balance(date)
101
+ load_tsv_dict(latest_balance_path(date))
102
+ end
103
+
104
+ def self.latest_balance_path(date)
105
+ start_year = date.month >= LucaSupport::CONFIG['fy_start'] ? date.year : date.year - 1
106
+ latest = Date.new(start_year, LucaSupport::CONFIG['fy_start'], 1).prev_month
88
107
  dict_dir = Pathname(LucaSupport::PJDIR) / 'data' / 'balance'
89
- # TODO: search latest balance dictionary
90
- load_tsv_dict(dict_dir / 'start.tsv')
108
+ fileglob = %Q(start-#{latest.year}-#{format("%02d", latest.month)}-*)
109
+ path = Dir.glob(fileglob, base: dict_dir)[0] || 'start.tsv'
110
+ dict_dir / path
91
111
  end
92
112
 
93
113
  def self.issue_date(obj)
94
114
  Date.parse(obj.dig('_date', :label))
95
115
  end
116
+
117
+ def self.generate_balance(year, month = nil)
118
+ start_date = Date.new((year.to_i - 1), LucaSupport::CONFIG['fy_start'], 1)
119
+ month ||= LucaSupport::CONFIG['fy_start'] - 1
120
+ end_date = Date.new(year.to_i, month, -1)
121
+ labels = load('base.tsv')
122
+ bs = load_balance(start_date, end_date)
123
+ fy_digest = checksum(start_date, end_date)
124
+ current_ref = gitref
125
+ csv = CSV.generate(String.new, col_sep: "\t", headers: false) do |f|
126
+ f << ['code', 'label', 'balance']
127
+ f << ['_date', end_date]
128
+ f << ['_digest', fy_digest]
129
+ f << ['_gitref', current_ref] if current_ref
130
+ bs.each do |code, balance|
131
+ next if LucaSupport::Code.readable(balance) == 0
132
+
133
+ f << [code, labels.dig(code, :label), LucaSupport::Code.readable(balance)]
134
+ end
135
+ end
136
+ dict_dir = Pathname(LucaSupport::PJDIR) / 'data' / 'balance'
137
+ filepath = dict_dir / "start-#{end_date.to_s}.tsv"
138
+
139
+ File.open(filepath, 'w') { |f| f.write csv }
140
+ end
141
+
142
+ def self.load_balance(start_date, end_date)
143
+ base = latest_balance(start_date).each_with_object({}) do |(k, v), h|
144
+ h[k] = BigDecimal(v[:balance].to_s) if v[:balance]
145
+ end
146
+
147
+ search_range = term_by_month(start_date, end_date)
148
+ bs = search_range.each_with_object(base) do |date, h|
149
+ net(date.year, date.month)[0].each do |code, amount|
150
+ next if /^[^1-9]/.match(code)
151
+
152
+ h[code] ||= BigDecimal('0')
153
+ h[code] += amount
154
+ end
155
+ end
156
+ bs['9142'] ||= BigDecimal('0')
157
+ bs['9142'] += LucaBook::State
158
+ .range(start_date.year, start_date.month, end_date.year, end_date.month)
159
+ .net_income
160
+ bs.sort
161
+ end
162
+
163
+ def self.checksum(start_date, end_date)
164
+ digest = update_digest(String.new, File.read(latest_balance_path(start_date)))
165
+ term_by_month(start_date, end_date)
166
+ .map { |date| dir_digest(date.year, date.month) }
167
+ .each { |month_digest| digest = update_digest(digest, month_digest) }
168
+ digest
169
+ end
170
+
171
+ def self.gitref
172
+ digest = `git rev-parse HEAD`
173
+ $?.exitstatus == 0 ? digest.strip : nil
174
+ end
96
175
  end
97
176
  end