lucabook 0.2.21 → 0.2.26

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: 3f7366698bee6b770fbae39f8fe650e1f460b04dec25c8036c84013f970adaa3
4
- data.tar.gz: 8ecbf412b31f001f0cfa2de27ceb879463d17f28e3db552770866b44c0b73114
3
+ metadata.gz: 933e3d04315c04b7f6e998b16499b458ceeeafe8bfae9ce4ef4422018eb17040
4
+ data.tar.gz: 2e243b4e85cdc50bd45e39ed311cf78a8aa12e1956be0d0273c2a634147be16c
5
5
  SHA512:
6
- metadata.gz: e30fabd50119ad470c90512bcf066b3992b5df2d67139f437a5f4336d73c843fb65dab2a70b5914c190155de68f82ec722840e0f056079061634f77bb5da4964
7
- data.tar.gz: 3e8775200e05f8bfe0923b53690fa5245441ea4d71768246a7309ee503294d847a466e82da85568d3071f47eccd4f9783a79199b4d4eb630c961de84488a31ed
6
+ metadata.gz: b2292a4972ce1fcf883a828d17479f1580e3145e0db2d26ff6044a73495d2e151d60f0bd98fd5b2f945c3b74cdff879f9f7779073487c05c70a078306b069bbe
7
+ data.tar.gz: 27bf6b4bcea1b2eb9d5dac761d5465ce478af02a4074f0a20038b7f8a21c528b66faf399d4364702ebf22f06cc0a0112d13a4ee2e10acb22eb16b52859aaf4b6
data/exe/luca-book CHANGED
@@ -10,7 +10,8 @@ class LucaCmd
10
10
  if params['config']
11
11
  LucaBook::Import.new(args[0], params['config']).import_csv
12
12
  elsif params['json']
13
- LucaBook::Import.import_json(STDIN.read)
13
+ str = args[0].nil? ? STDIN.read : File.read(args[0])
14
+ LucaBook::Import.import_json(str)
14
15
  else
15
16
  puts 'Usage: luca-book import -c import_config'
16
17
  exit 1
@@ -20,7 +21,11 @@ class LucaCmd
20
21
  def self.list(args, params)
21
22
  args = gen_range(params[:n] || 1) if args.empty?
22
23
  if params['code']
23
- render(LucaBook::List.term(*args, code: params['code']).list_on_code, params)
24
+ if params['headers']
25
+ render(LucaBook::ListByHeader.term(*args, code: params['code'], header: params['headers']).list_by_code, params)
26
+ else
27
+ render(LucaBook::List.term(*args, code: params['code']).list_by_code, params)
28
+ end
24
29
  else
25
30
  render(LucaBook::List.term(*args).list_journals, params)
26
31
  end
@@ -31,23 +36,45 @@ class LucaCmd
31
36
  if params['code']
32
37
  render(LucaBook::State.by_code(params['code'], *args), params)
33
38
  else
34
- render(LucaBook::State.term(*args).stats(params[:level]), params)
39
+ render(LucaBook::State.range(*args).stats(params[:level]), params)
40
+ end
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.'
35
49
  end
36
50
  end
37
51
  end
38
52
 
39
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
+
40
61
  def self.balancesheet(args, params)
41
62
  level = params[:level] || 3
42
63
  legal = params[:legal] || false
43
64
  args = gen_range(params[:n] || 1) if args.empty?
44
- render(LucaBook::State.term(*args).bs(level, legal: legal), params)
65
+ render(LucaBook::State.range(*args).bs(level, legal: legal), params)
45
66
  end
46
67
 
47
68
  def self.profitloss(args, params)
48
69
  level = params[:level] || 2
49
70
  args = gen_range(params[:n]) if args.empty?
50
- render(LucaBook::State.term(*args).pl(level), params)
71
+ render(LucaBook::State.range(*args).pl(level), params)
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)
51
78
  end
52
79
  end
53
80
 
@@ -68,6 +95,12 @@ class LucaCmd
68
95
  puts YAML.dump(dat)
69
96
  end
70
97
  end
98
+
99
+ class Dict < LucaCmd
100
+ def self.update_balance(args, params)
101
+ LucaBook::Dict.generate_balance(*args)
102
+ end
103
+ end
71
104
  end
72
105
 
73
106
  def new_pj(args = nil, params = {})
@@ -93,7 +126,8 @@ when /journals?/, 'j'
93
126
  when 'list'
94
127
  OptionParser.new do |opt|
95
128
  opt.banner = 'Usage: luca-book journals list [options] [YYYY M]'
96
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
129
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
130
+ opt.on('--customer', 'categorize by x-customer header') { |_v| params['headers'] = 'x-customer' }
97
131
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
98
132
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
99
133
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
@@ -101,10 +135,20 @@ when /journals?/, 'j'
101
135
  args = opt.parse!(ARGV)
102
136
  LucaCmd::Journal.list(args, params)
103
137
  end
138
+ when 'set'
139
+ OptionParser.new do |opt|
140
+ opt.banner = 'Usage: luca-book journals set [options] [YYYY M]'
141
+ opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
142
+ opt.on('--header VAL', 'header key') { |v| params[:key] = v }
143
+ opt.on('--val VAL', 'header value') { |v| params[:value] = v }
144
+ opt.on_tail('set header to journals on specified code.')
145
+ args = opt.parse!(ARGV)
146
+ LucaCmd::Journal.add_header(args, params)
147
+ end
104
148
  when 'stats'
105
149
  OptionParser.new do |opt|
106
150
  opt.banner = 'Usage: luca-book journals stats [options] [YYYY M]'
107
- opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
151
+ opt.on('-c', '--code VAL', 'filter with code or label') { |v| params['code'] = v }
108
152
  opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
109
153
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
110
154
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
@@ -130,6 +174,13 @@ when 'new'
130
174
  when /reports?/, 'r'
131
175
  subcmd = ARGV.shift
132
176
  case subcmd
177
+ when 'xbrl'
178
+ OptionParser.new do |opt|
179
+ opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
180
+ opt.on('-o', '--output VAL', 'output filename') { |v| params[:output] = v }
181
+ args = opt.parse!(ARGV)
182
+ LucaCmd::Report.xbrl(args, params)
183
+ end
133
184
  when 'bs'
134
185
  OptionParser.new do |opt|
135
186
  opt.banner = 'Usage: luca-book reports bs [options] [YYYY M]'
@@ -150,6 +201,14 @@ when /reports?/, 'r'
150
201
  args = opt.parse!(ARGV)
151
202
  LucaCmd::Report.profitloss(args, params)
152
203
  end
204
+ when 'mail'
205
+ OptionParser.new do |opt|
206
+ opt.banner = 'Usage: luca-book reports mail [options] [YYYY M YYYY M]'
207
+ opt.on('-l', '--level VAL', 'account level') { |v| params[:level] = v.to_i }
208
+ opt.on('-n VAL', 'report count') { |v| params[:n] = v.to_i }
209
+ args = opt.parse!(ARGV)
210
+ LucaCmd::Report.report_mail(args, params)
211
+ end
153
212
  else
154
213
  puts 'Proper subcommand needed.'
155
214
  puts
@@ -158,6 +217,16 @@ when /reports?/, 'r'
158
217
  puts ' pl: show statement of income'
159
218
  exit 1
160
219
  end
220
+ when /balance/
221
+ subcmd = ARGV.shift
222
+ case subcmd
223
+ when 'update'
224
+ OptionParser.new do |opt|
225
+ opt.banner = 'Usage: luca-book balance update YYYY [M]'
226
+ args = opt.parse!(ARGV)
227
+ LucaCmd::Dict.update_balance(args, params)
228
+ end
229
+ end
161
230
  else
162
231
  puts 'Proper subcommand needed.'
163
232
  puts
data/lib/luca_book.rb CHANGED
@@ -5,10 +5,12 @@ 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'
11
12
  autoload :List, 'luca_book/list'
13
+ autoload :ListByHeader, 'luca_book/list_by_header'
12
14
  autoload :Setup, 'luca_book/setup'
13
15
  autoload :State, 'luca_book/state'
14
16
  autoload :Util, 'luca_book/util'
@@ -0,0 +1,152 @@
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)
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) 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.include?(code)
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.include?(code)
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.include?(code)
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
+ if code
127
+ sum[:debit] = sum[:debit][code] || BigDecimal('0')
128
+ sum[:credit] = sum[:credit][code] || BigDecimal('0')
129
+ sum[:debit_count] = sum[:debit_count][code] || 0
130
+ sum[:credit_count] = sum[:credit_count][code] || 0
131
+ end
132
+ sum
133
+ end
134
+
135
+ # netting vouchers in specified term
136
+ #
137
+ def net(start_year, start_month, end_year = nil, end_month = nil, code: nil, date_range: nil)
138
+ g = gross(start_year, start_month, end_year, end_month, code: code, date_range: date_range)
139
+ idx = (g[:debit].keys + g[:credit].keys).uniq.sort
140
+ count = {}
141
+ diff = {}.tap do |sum|
142
+ idx.each do |code|
143
+ sum[code] = g.dig(:debit, code).nil? ? BigDecimal('0') : LucaBook::Util.calc_diff(g[:debit][code], code)
144
+ sum[code] -= g.dig(:credit, code).nil? ? BigDecimal('0') : LucaBook::Util.calc_diff(g[:credit][code], code)
145
+ count[code] = (g.dig(:debit_count, code) || 0) + (g.dig(:credit_count, code) || 0)
146
+ end
147
+ end
148
+ [diff, count]
149
+ end
150
+ end
151
+ end
152
+ 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,20 +1,184 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'luca_support/code'
3
4
  require 'luca_support/config'
4
5
  require 'luca_record/dict'
6
+ require 'luca_record/io'
7
+ require 'luca_book'
5
8
  require 'date'
6
9
  require 'pathname'
7
10
 
8
11
  module LucaBook
9
12
  class Dict < LucaRecord::Dict
10
- def self.latest_balance
11
- dict_dir = Pathname(LucaSupport::Config::Pjdir) / 'data' / 'balance'
12
- # TODO: search latest balance dictionary
13
- load_tsv_dict(dict_dir / 'start.tsv')
13
+ include Accumulator
14
+ include LucaRecord::IO
15
+
16
+ @dirname = 'journals'
17
+ @record_type = 'raw'
18
+ # Column number settings for CSV/TSV convert
19
+ #
20
+ # :label
21
+ # for double entry data
22
+ # :counter_label
23
+ # must be specified with label
24
+ # :debit_label
25
+ # for double entry data
26
+ # * debit_amount
27
+ # :credit_label
28
+ # for double entry data
29
+ # * credit_amount
30
+ # :note
31
+ # can be the same column as another label
32
+ #
33
+ # :encoding
34
+ # file encoding
35
+ #
36
+ def csv_config
37
+ {}.tap do |config|
38
+ if @config.dig('label')
39
+ config[:label] = @config['label'].to_i
40
+ if @config.dig('counter_label')
41
+ config[:counter_label] = @config['counter_label']
42
+ config[:type] = 'single'
43
+ end
44
+ elsif @config.dig('debit_label')
45
+ config[:debit_label] = @config['debit_label'].to_i
46
+ if @config.dig('credit_label')
47
+ config[:credit_label] = @config['credit_label'].to_i
48
+ config[:type] = 'double'
49
+ end
50
+ end
51
+ config[:type] ||= 'invalid'
52
+ config[:debit_amount] = @config['debit_amount'].to_i if @config.dig('debit_amount')
53
+ config[:credit_amount] = @config['credit_amount'].to_i if @config.dig('credit_amount')
54
+ config[:note] = @config['note'] if @config.dig('note')
55
+ config[:encoding] = @config['encoding'] if @config.dig('encoding')
56
+
57
+ config[:year] = @config['year'] if @config.dig('year')
58
+ config[:month] = @config['month'] if @config.dig('month')
59
+ config[:day] = @config['day'] if @config.dig('day')
60
+ config[:default_debit] = @config['default_debit'] if @config.dig('default_debit')
61
+ config[:default_credit] = @config['default_credit'] if @config.dig('default_credit')
62
+ end
63
+ end
64
+
65
+ def search(word, default_word = nil, amount = nil)
66
+ res = super(word, default_word, main_key: 'account_label')
67
+ if res.is_a?(Array) && res[0].is_a?(Array)
68
+ filter_amount(res, amount)
69
+ else
70
+ res
71
+ end
72
+ end
73
+
74
+ # Choose setting on Big or small condition.
75
+ #
76
+ def filter_amount(settings, amount = nil)
77
+ return settings[0] if amount.nil?
78
+
79
+ settings.each do |item|
80
+ return item unless item[1].keys.include?(:on_amount)
81
+
82
+ condition = item.dig(1, :on_amount)
83
+ case condition[0]
84
+ when '>'
85
+ return item if amount > BigDecimal(condition[1..])
86
+ when '<'
87
+ return item if amount < BigDecimal(condition[1..])
88
+ else
89
+ return item
90
+ end
91
+ end
92
+ nil
93
+ end
94
+
95
+ # Find balance at financial year start by given date.
96
+ # If not found 'start-yyyy-mm-*.tsv', use 'start.tsv' as default.
97
+ #
98
+ def self.latest_balance(date)
99
+ load_tsv_dict(latest_balance_path(date))
100
+ end
101
+
102
+ def self.latest_balance_path(date)
103
+ start_year = date.month >= LucaSupport::CONFIG['fy_start'] ? date.year : date.year - 1
104
+ latest = Date.new(start_year, LucaSupport::CONFIG['fy_start'], 1).prev_month
105
+ dict_dir = Pathname(LucaSupport::PJDIR) / 'data' / 'balance'
106
+ fileglob = %Q(start-#{latest.year}-#{format("%02d", latest.month)}-*)
107
+ path = Dir.glob(fileglob, base: dict_dir)[0] || 'start.tsv'
108
+ dict_dir / path
14
109
  end
15
110
 
16
111
  def self.issue_date(obj)
17
112
  Date.parse(obj.dig('_date', :label))
18
113
  end
114
+
115
+ def self.generate_balance(year, month = nil)
116
+ start_date = Date.new((year.to_i - 1), LucaSupport::CONFIG['fy_start'], 1)
117
+ month ||= LucaSupport::CONFIG['fy_start'] - 1
118
+ end_date = Date.new(year.to_i, month, -1)
119
+ labels = load('base.tsv')
120
+ bs = load_balance(start_date, end_date)
121
+ fy_digest = checksum(start_date, end_date)
122
+ current_ref = gitref
123
+ csv = CSV.generate(String.new, col_sep: "\t", headers: false) do |f|
124
+ f << ['code', 'label', 'balance']
125
+ f << ['_date', end_date]
126
+ f << ['_digest', fy_digest]
127
+ f << ['_gitref', current_ref] if current_ref
128
+ bs.each do |code, balance|
129
+ next if LucaSupport::Code.readable(balance) == 0
130
+
131
+ f << [code, labels.dig(code, :label), LucaSupport::Code.readable(balance)]
132
+ end
133
+ end
134
+ dict_dir = Pathname(LucaSupport::PJDIR) / 'data' / 'balance'
135
+ filepath = dict_dir / "start-#{end_date.to_s}.tsv"
136
+
137
+ File.open(filepath, 'w') { |f| f.write csv }
138
+ end
139
+
140
+ def self.load_balance(start_date, end_date)
141
+ base = latest_balance(start_date).each_with_object({}) do |(k, v), h|
142
+ h[k] = BigDecimal(v[:balance].to_s) if v[:balance]
143
+ end
144
+
145
+ search_range = term_by_month(start_date, end_date)
146
+ bs = search_range.each_with_object(base) do |date, h|
147
+ net(date.year, date.month)[0].each do |code, amount|
148
+ next if /^[^1-9]/.match(code)
149
+
150
+ h[code] ||= BigDecimal('0')
151
+ h[code] += amount
152
+ end
153
+ end
154
+ bs['9142'] ||= BigDecimal('0')
155
+ bs['9142'] += LucaBook::State
156
+ .range(start_date.year, start_date.month, end_date.year, end_date.month)
157
+ .net_income
158
+ bs.sort
159
+ end
160
+
161
+ def self.checksum(start_date, end_date)
162
+ digest = update_digest(String.new, File.read(latest_balance_path(start_date)))
163
+ term_by_month(start_date, end_date)
164
+ .map { |date| dir_digest(date.year, date.month) }
165
+ .each { |month_digest| digest = update_digest(digest, month_digest) }
166
+ digest
167
+ end
168
+
169
+ def self.gitref
170
+ digest = `git rev-parse HEAD`
171
+ $?.exitstatus == 0 ? digest.strip : nil
172
+ end
173
+
174
+ def self.term_by_month(start_date, end_date)
175
+ Enumerator.new do |yielder|
176
+ each_month = start_date
177
+ while each_month <= end_date
178
+ yielder << each_month
179
+ each_month = each_month.next_month
180
+ end
181
+ end
182
+ end
19
183
  end
20
184
  end