lucabook 0.2.21 → 0.2.26

Sign up to get free protection for your applications and to get access to all the features.
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