lucabook 0.2.24 → 0.2.29

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: 55e730a696b4537183e9a298cff7d715c2a93dfa22fabc10cc889fd18c72b1a1
4
- data.tar.gz: f5e579c528ad978533f7df60eed6b4a494dd723a4fdef15180622737fd9be32a
3
+ metadata.gz: b6e3ddc43f78a55be0357756cc617efd9fc798836cd771e4814a058e62abc6eb
4
+ data.tar.gz: b5b6d3419d2dbfcee9f859744c7513830a6576868a6a51241ea0ee04633c0c5f
5
5
  SHA512:
6
- metadata.gz: 607110ed2c686b35c1be749b16bd6f4c0827450e55277d44c62ddb5acfb06fbb67d601c9fe475c453f5999afa23a1bf83923d7251ba72a7a24267e0cfc8b4057
7
- data.tar.gz: d8fe5540370bc81494847b5f9d975667d19bda4e7979fc852556e81b2adbd644b87ca11bdf7601334454cfe545070698e0e20570895fd2e0eb4bb790eea19711
6
+ metadata.gz: f2c7c3b7043a60f39379be1c6e36b02367e2ec31261b0268768934c2f258cba62d3fc042f6286c5ac62b15289a89d1ac6e05143dcb9f2ecc04b5aa8d37bb9a89
7
+ data.tar.gz: 653e43c3f11785dabf83e8997810ae3316b8549b8370854e42bcc170ba54729dc50762e11e0733fbff2994fc15a37d6ac1eeb60c2581885f451acafeb57f5cdc
data/exe/luca-book CHANGED
@@ -19,13 +19,15 @@ 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
+ elsif params['render']
30
+ puts LucaBook::List.term(*args).render_html(params['render'])
29
31
  else
30
32
  render(LucaBook::List.term(*args).list_journals, params)
31
33
  end
@@ -34,7 +36,7 @@ class LucaCmd
34
36
  def self.stats(args, params)
35
37
  args = gen_range(params[:n]) if args.empty?
36
38
  if params['code']
37
- render(LucaBook::State.by_code(params['code'], *args), params)
39
+ render(LucaBook::State.by_code(params['code'], *args, recursive: params[:recursive]), params)
38
40
  else
39
41
  render(LucaBook::State.range(*args).stats(params[:level]), params)
40
42
  end
@@ -51,6 +53,13 @@ class LucaCmd
51
53
  end
52
54
 
53
55
  class Report < LucaCmd
56
+ def self.xbrl(args, params)
57
+ level = params[:level] || 3
58
+ legal = params[:legal] || false
59
+ args = gen_range(params[:n] || 1) if args.empty?
60
+ LucaBook::State.range(*args).render_xbrl(params[:output])
61
+ end
62
+
54
63
  def self.balancesheet(args, params)
55
64
  level = params[:level] || 3
56
65
  legal = params[:legal] || false
@@ -63,13 +72,27 @@ class LucaCmd
63
72
  args = gen_range(params[:n]) if args.empty?
64
73
  render(LucaBook::State.range(*args).pl(level), params)
65
74
  end
75
+
76
+ def self.report_mail(args, params)
77
+ level = params[:level] || 3
78
+ args = gen_range(params[:n] || 12) if args.empty?
79
+ render(LucaBook::State.range(*args).report_mail(level), params)
80
+ end
66
81
  end
67
82
 
68
- def self.gen_range(count)
69
- count ||= 3
83
+ def self.gen_range(count = nil)
70
84
  today = Date.today
71
- start = today.prev_month(count - 1)
72
- [start.year, start.month, today.year, today.month]
85
+ if count
86
+ start = today.prev_month(count - 1)
87
+ [start.year, start.month, today.year, today.month]
88
+ else
89
+ start_year = if today.month >= LucaSupport::CONFIG['fy_start'].to_i
90
+ today.year
91
+ else
92
+ today.year - 1
93
+ end
94
+ [start_year, LucaSupport::CONFIG['fy_start'], start_year + 1, LucaSupport::CONFIG['fy_start'].to_i - 1]
95
+ end
73
96
  end
74
97
 
75
98
  def self.render(dat, params)
@@ -82,6 +105,12 @@ class LucaCmd
82
105
  puts YAML.dump(dat)
83
106
  end
84
107
  end
108
+
109
+ class Dict < LucaCmd
110
+ def self.update_balance(args, params)
111
+ LucaBook::Dict.generate_balance(*args)
112
+ end
113
+ end
85
114
  end
86
115
 
87
116
  def new_pj(args = nil, params = {})
@@ -105,13 +134,17 @@ when /journals?/, 'j'
105
134
  LucaCmd::Journal.import(args, params)
106
135
  end
107
136
  when 'list'
137
+ params[:recursive] = false
108
138
  OptionParser.new do |opt|
109
139
  opt.banner = 'Usage: luca-book journals list [options] [YYYY M]'
110
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
140
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
141
+ opt.on('-r', '--recursive', 'include subaccounts') { |_v| params[:recursive] = true }
111
142
  opt.on('--customer', 'categorize by x-customer header') { |_v| params['headers'] = 'x-customer' }
112
143
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
113
144
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
114
145
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
146
+ opt.on('--html', 'output journals html') { |_v| params['render'] = :html }
147
+ opt.on('--pdf', 'output journals PDF') { |_v| params['render'] = :pdf }
115
148
  opt.on_tail('List records. If you specify code and/or month, search on each criteria.')
116
149
  args = opt.parse!(ARGV)
117
150
  LucaCmd::Journal.list(args, params)
@@ -127,9 +160,11 @@ when /journals?/, 'j'
127
160
  LucaCmd::Journal.add_header(args, params)
128
161
  end
129
162
  when 'stats'
163
+ params[:recursive] = false
130
164
  OptionParser.new do |opt|
131
165
  opt.banner = 'Usage: luca-book journals stats [options] [YYYY M]'
132
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
166
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
167
+ opt.on('-r', '--recursive', 'include subaccounts') { |_v| params[:recursive] = true }
133
168
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
134
169
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
135
170
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
@@ -155,6 +190,13 @@ when 'new'
155
190
  when /reports?/, 'r'
156
191
  subcmd = ARGV.shift
157
192
  case subcmd
193
+ when 'xbrl'
194
+ OptionParser.new do |opt|
195
+ opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
196
+ opt.on('-o', '--output VAL', 'output filename') { |v| params[:output] = v }
197
+ args = opt.parse!(ARGV)
198
+ LucaCmd::Report.xbrl(args, params)
199
+ end
158
200
  when 'bs'
159
201
  OptionParser.new do |opt|
160
202
  opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
@@ -175,6 +217,14 @@ when /reports?/, 'r'
175
217
  args = opt.parse!(ARGV)
176
218
  LucaCmd::Report.profitloss(args, params)
177
219
  end
220
+ when 'mail'
221
+ OptionParser.new do |opt|
222
+ opt.banner = 'Usage: luca-book reports mail [options] [YYYY M YYYY M]'
223
+ opt.on('-l', '--level VAL', 'account level') { |v| params[:level] = v.to_i }
224
+ opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
225
+ args = opt.parse!(ARGV)
226
+ LucaCmd::Report.report_mail(args, params)
227
+ end
178
228
  else
179
229
  puts 'Proper subcommand needed.'
180
230
  puts
@@ -183,6 +233,16 @@ when /reports?/, 'r'
183
233
  puts ' pl: show statement of income'
184
234
  exit 1
185
235
  end
236
+ when /balance/
237
+ subcmd = ARGV.shift
238
+ case subcmd
239
+ when 'update'
240
+ OptionParser.new do |opt|
241
+ opt.banner = 'Usage: luca-book balance update YYYY [M]'
242
+ args = opt.parse!(ARGV)
243
+ LucaCmd::Dict.update_balance(args, params)
244
+ end
245
+ end
186
246
  else
187
247
  puts 'Proper subcommand needed.'
188
248
  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
@@ -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
@@ -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