lucabook 0.2.7
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 +7 -0
- data/LICENSE +674 -0
- data/exe/luca-book +54 -0
- data/lib/luca_book.rb +8 -0
- data/lib/luca_book/console.rb +134 -0
- data/lib/luca_book/dict.rb +13 -0
- data/lib/luca_book/import.rb +130 -0
- data/lib/luca_book/journal.rb +80 -0
- data/lib/luca_book/report.rb +3 -0
- data/lib/luca_book/state.rb +250 -0
- data/lib/luca_book/version.rb +5 -0
- metadata +99 -0
data/exe/luca-book
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/ruby
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "luca_book/console"
|
|
5
|
+
|
|
6
|
+
def list(args, params)
|
|
7
|
+
if params["c"] or params["code"]
|
|
8
|
+
code = params["c"] || params["code"]
|
|
9
|
+
LucaBookConsole.new.by_code(code, args.dig(0), args.dig(1))
|
|
10
|
+
elsif args.length > 0
|
|
11
|
+
LucaBookConsole.new.by_month(args[0], args.dig(1))
|
|
12
|
+
else
|
|
13
|
+
LucaBookConsole.new.all
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def report(args, params)
|
|
18
|
+
if params['bs']
|
|
19
|
+
LucaBook::State.term(*args).bs.to_yaml
|
|
20
|
+
elsif params['pl']
|
|
21
|
+
LucaBook::State.term(*args).pl.to_yaml
|
|
22
|
+
else
|
|
23
|
+
LucaBook::State.term(*args).to_yaml
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
cmd = ARGV.shift
|
|
28
|
+
|
|
29
|
+
case cmd
|
|
30
|
+
when "list"
|
|
31
|
+
params = {}
|
|
32
|
+
OptionParser.new do |opt|
|
|
33
|
+
opt.banner = 'Usage: luca list [year month]'
|
|
34
|
+
opt.on('-c', '--code VAL', 'search with code'){|v| params["code"] = v }
|
|
35
|
+
opt.on_tail('List records. If you specify code and/or month, search on each criteria.')
|
|
36
|
+
args = opt.parse!(ARGV)
|
|
37
|
+
list(args, params)
|
|
38
|
+
end
|
|
39
|
+
when "report"
|
|
40
|
+
params = {}
|
|
41
|
+
OptionParser.new do |opt|
|
|
42
|
+
opt.banner = 'Usage: luca report'
|
|
43
|
+
opt.on('--bs', 'show Balance sheet'){|v| params["bs"] = v }
|
|
44
|
+
opt.on('--pl', 'show Income statement'){|v| params["pl"] = v }
|
|
45
|
+
args = opt.parse!(ARGV)
|
|
46
|
+
report(args, params)
|
|
47
|
+
end
|
|
48
|
+
when "--help"
|
|
49
|
+
puts 'Usage: luca subcommand'
|
|
50
|
+
puts ' list: list records'
|
|
51
|
+
puts ' report: show reports'
|
|
52
|
+
else
|
|
53
|
+
puts 'Invalid subcommand'
|
|
54
|
+
end
|
data/lib/luca_book.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require 'luca_book'
|
|
2
|
+
|
|
3
|
+
class LucaBookConsole
|
|
4
|
+
|
|
5
|
+
def initialize(dir_path=nil)
|
|
6
|
+
@report = LucaBookReport.new(dir_path)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def all
|
|
10
|
+
array = @report.scan_terms(@report.book.pjdir).map{|y,m| y}.uniq.map{|year|
|
|
11
|
+
@report.book.search(year)
|
|
12
|
+
}.flatten
|
|
13
|
+
show_records(array)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def by_code(code, year=nil, month=nil)
|
|
17
|
+
array = @report.by_code(code, year, month)
|
|
18
|
+
show_records(array)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def by_month(year, month)
|
|
22
|
+
array = @report.book.search(year, month)
|
|
23
|
+
show_records(array)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show_records(records)
|
|
27
|
+
print "#{cnsl_fmt("ID")} #{cnsl_fmt("debit")} #{cnsl_fmt("credit")} #{cnsl_fmt("")*2}"
|
|
28
|
+
print "#{cnsl_fmt("balance")}" unless records.first.dig(:balance).nil?
|
|
29
|
+
puts
|
|
30
|
+
records.each do |h|
|
|
31
|
+
puts "#{cnsl_fmt(h.dig(:id))} #{"-"*85}"
|
|
32
|
+
lines = [h.dig(:debit)&.length, h.dig(:credit)&.length]&.max || 0
|
|
33
|
+
lines.times do |i|
|
|
34
|
+
puts "#{cnsl_fmt("")} #{cnsl_fmt(h.dig(:debit, i, :amount))} #{cnsl_code(h.dig(:debit, i))}" if h.dig(:debit, i, :amount)
|
|
35
|
+
puts "#{cnsl_fmt("")*2} #{cnsl_fmt(h.dig(:credit, i, :amount))} #{cnsl_code(h.dig(:credit, i))}" if h.dig(:credit, i, :amount)
|
|
36
|
+
end
|
|
37
|
+
puts "#{cnsl_fmt(""*15)*5} #{cnsl_fmt(h.dig(:balance))}" unless h.dig(:balance).nil?
|
|
38
|
+
puts "#{cnsl_fmt(""*15)} #{h.dig(:note)}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def bs
|
|
43
|
+
target = []
|
|
44
|
+
report = []
|
|
45
|
+
output = @report.accumulate_all do |f|
|
|
46
|
+
target << f[:target]
|
|
47
|
+
report << f[:current]
|
|
48
|
+
#diff << f[:diff]
|
|
49
|
+
end
|
|
50
|
+
puts "---- BS ----"
|
|
51
|
+
target.each_slice(6) do |v|
|
|
52
|
+
puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
|
|
53
|
+
end
|
|
54
|
+
convert_collection(report).each do |h|
|
|
55
|
+
if /^[0-9]/.match(h[:code])
|
|
56
|
+
if /[^0]$/.match(h[:code])
|
|
57
|
+
print " "
|
|
58
|
+
print " " if h[:code].length > 3
|
|
59
|
+
end
|
|
60
|
+
puts cnsl_label(h[:label], h[:code])
|
|
61
|
+
h[:value].each_slice(6) do |v|
|
|
62
|
+
puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
puts "---- ----"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def pl
|
|
70
|
+
target = []
|
|
71
|
+
report = []
|
|
72
|
+
output = @report.accumulate_all do |f|
|
|
73
|
+
target << f[:target]
|
|
74
|
+
report << f[:diff]
|
|
75
|
+
#current << f[:current]
|
|
76
|
+
end
|
|
77
|
+
puts "---- PL ----"
|
|
78
|
+
target.each_slice(6) do |v|
|
|
79
|
+
puts "#{cnsl_fmt("", 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
|
|
80
|
+
end
|
|
81
|
+
convert_collection(report).each do |h|
|
|
82
|
+
if /^[A-Z]/.match(h[:code])
|
|
83
|
+
total = [h[:value].inject(:+)] + Array.new(h[:value].length)
|
|
84
|
+
if /[^0]$/.match(h[:code])
|
|
85
|
+
print " "
|
|
86
|
+
print " " if h[:code].length > 3
|
|
87
|
+
end
|
|
88
|
+
puts cnsl_label(h[:label], h[:code])
|
|
89
|
+
h[:value].each_slice(6).with_index(0) do |v, i|
|
|
90
|
+
puts "#{cnsl_fmt(total[i], 14)} #{v.map{|v| cnsl_fmt(v, 14)}.join}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
puts "---- ----"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def convert_collection(obj)
|
|
98
|
+
{}.tap {|res|
|
|
99
|
+
obj.each do |month|
|
|
100
|
+
month.each do |k,v|
|
|
101
|
+
if res.has_key?(k)
|
|
102
|
+
res[k] << v
|
|
103
|
+
else
|
|
104
|
+
res[k] = [v]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
}.sort.map do |k,v|
|
|
109
|
+
{code: k, label: @report.dict.dig(k, :label), value: v}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cnsl_label(label, code)
|
|
114
|
+
if /[0]$/.match(code)
|
|
115
|
+
cnsl_bold(label) + " " + "-"*80
|
|
116
|
+
else
|
|
117
|
+
label
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cnsl_bold(str)
|
|
122
|
+
"\e[1m#{str}\e[0m"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def cnsl_code(obj)
|
|
126
|
+
code = @report.dict.dig(obj&.dig(:code))&.dig(:label) || ""
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def cnsl_fmt(str, width=15, length=nil)
|
|
130
|
+
length ||= width
|
|
131
|
+
sprintf("%#{width}.#{length}s", str)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'luca_book'
|
|
6
|
+
require 'luca_support'
|
|
7
|
+
#require 'luca_book/dict'
|
|
8
|
+
require 'luca_record'
|
|
9
|
+
|
|
10
|
+
module LucaBook
|
|
11
|
+
class Import
|
|
12
|
+
DEBIT_DEFAULT = '仮払金'
|
|
13
|
+
CREDIT_DEFAULT = '仮受金'
|
|
14
|
+
|
|
15
|
+
def initialize(path)
|
|
16
|
+
raise 'no such file' unless FileTest.file?(path)
|
|
17
|
+
|
|
18
|
+
@target_file = path
|
|
19
|
+
# TODO: yaml need to be configurable
|
|
20
|
+
@dict = LucaRecord::Dict.new('import.yaml')
|
|
21
|
+
@code_map = LucaRecord::Dict.reverse(LucaRecord::Dict.load('base.tsv'))
|
|
22
|
+
@config = @dict.csv_config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# === JSON Format:
|
|
26
|
+
# {
|
|
27
|
+
# "date": "2020-05-04",
|
|
28
|
+
# "debit" : [
|
|
29
|
+
# {
|
|
30
|
+
# "label": "savings accounts",
|
|
31
|
+
# "value": 20000
|
|
32
|
+
# }
|
|
33
|
+
# ],
|
|
34
|
+
# "credit" : [
|
|
35
|
+
# {
|
|
36
|
+
# "label": "trade notes receivable",
|
|
37
|
+
# "value": 20000
|
|
38
|
+
# }
|
|
39
|
+
# ],
|
|
40
|
+
# "note": "settlement for the last month trade"
|
|
41
|
+
# }
|
|
42
|
+
#
|
|
43
|
+
def import_json(io)
|
|
44
|
+
d = JSON.parse(io)
|
|
45
|
+
validate(d)
|
|
46
|
+
|
|
47
|
+
# dict = LucaBook::Dict.reverse_dict(LucaBook::Dict::Data)
|
|
48
|
+
d['debit'].each { |h| h['code'] = @dict.search(h['label'], DEBIT_DEFAULT) }
|
|
49
|
+
d['credit'].each { |h| h['code'] = @dict.search(h['label'], CREDIT_DEFAULT) }
|
|
50
|
+
|
|
51
|
+
LucaBook.new.create!(d)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def import_csv
|
|
55
|
+
@dict.load_csv(@target_file) do |row|
|
|
56
|
+
if @config[:type] == 'single'
|
|
57
|
+
LucaBook::Journal.create!(parse_single(row))
|
|
58
|
+
elsif @config[:type] == 'double'
|
|
59
|
+
p parse_double(row)
|
|
60
|
+
else
|
|
61
|
+
p row
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#
|
|
67
|
+
# convert single entry data
|
|
68
|
+
#
|
|
69
|
+
def parse_single(row)
|
|
70
|
+
value = row.dig(@config[:credit_value])&.empty? ? row[@config[:debit_value]] : row[@config[:credit_value]]
|
|
71
|
+
{}.tap do |d|
|
|
72
|
+
d['date'] = parse_date(row)
|
|
73
|
+
if row.dig(@config[:credit_value])&.empty?
|
|
74
|
+
d['debit'] = [
|
|
75
|
+
{ 'code' => search_code(row[@config[:label]], DEBIT_DEFAULT) }
|
|
76
|
+
]
|
|
77
|
+
d['credit'] = [
|
|
78
|
+
{ 'code' => @code_map.dig(@config[:counter_label]) }
|
|
79
|
+
]
|
|
80
|
+
else
|
|
81
|
+
d['debit'] = [
|
|
82
|
+
{ 'code' => @code_map.dig(@config[:counter_label]) }
|
|
83
|
+
]
|
|
84
|
+
d['credit'] = [
|
|
85
|
+
{ 'code' => search_code(row[@config[:label]], CREDIT_DEFAULT) }
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
d['debit'][0]['value'] = value
|
|
89
|
+
d['credit'][0]['value'] = value
|
|
90
|
+
d['note'] = row[@config[:note]]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
# convert double entry data
|
|
96
|
+
#
|
|
97
|
+
def parse_double(row)
|
|
98
|
+
{}.tap do |d|
|
|
99
|
+
d['date'] = parse_date(row)
|
|
100
|
+
d['debit'] = {
|
|
101
|
+
'code' => search_code(row[@config[:debit_label]], DEBIT_DEFAULT),
|
|
102
|
+
'value' => row.dig(@config[:debit_value])
|
|
103
|
+
}
|
|
104
|
+
d['credit'] = {
|
|
105
|
+
'code' => search_code(row[@config[:credit_label]], CREDIT_DEFAULT),
|
|
106
|
+
'value' => row.dig(@config[:credit_value])
|
|
107
|
+
}
|
|
108
|
+
d['note'] = row[@config[:note]]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def search_code(label, default_label)
|
|
113
|
+
@code_map.dig(@dict.search(label, default_label))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_date(row)
|
|
117
|
+
return nil if row.dig(@config[:year]).empty?
|
|
118
|
+
|
|
119
|
+
"#{row.dig(@config[:year])}-#{row.dig(@config[:month])}-#{row.dig(@config[:day])}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate(obj)
|
|
123
|
+
raise 'NoDateKey' if ! obj.has_key?('date')
|
|
124
|
+
raise 'NoDebitKey' if ! obj.has_key?('debit')
|
|
125
|
+
raise 'NoDebitValue' if obj['debit'].length < 1
|
|
126
|
+
raise 'NoCreditKey' if ! obj.has_key?('credit')
|
|
127
|
+
raise 'NoCreditValue' if obj['credit'].length < 1
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#
|
|
2
|
+
# manipulate files based on transaction date
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
require 'csv'
|
|
6
|
+
require 'date'
|
|
7
|
+
require 'luca_record'
|
|
8
|
+
|
|
9
|
+
module LucaBook
|
|
10
|
+
class Journal < LucaRecord::Base
|
|
11
|
+
@dirname = 'journals'
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# create journal from hash
|
|
15
|
+
#
|
|
16
|
+
def self.create!(d)
|
|
17
|
+
date = Date.parse(d['date'])
|
|
18
|
+
|
|
19
|
+
debit_amount = serialize_on_key(d['debit'], 'value')
|
|
20
|
+
credit_amount = serialize_on_key(d['credit'], 'value')
|
|
21
|
+
raise 'BalanceUnmatch' if debit_amount.inject(:+) != credit_amount.inject(:+)
|
|
22
|
+
|
|
23
|
+
debit_code = serialize_on_key(d['debit'], 'code')
|
|
24
|
+
credit_code = serialize_on_key(d['credit'], 'code')
|
|
25
|
+
|
|
26
|
+
# TODO: limit code length for filename
|
|
27
|
+
codes = (debit_code + credit_code).uniq
|
|
28
|
+
create_record!(date, codes) do |f|
|
|
29
|
+
f << debit_code
|
|
30
|
+
f << debit_amount
|
|
31
|
+
f << credit_code
|
|
32
|
+
f << credit_amount
|
|
33
|
+
f << []
|
|
34
|
+
f << [d.dig('note')]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# collect values on specified key
|
|
40
|
+
#
|
|
41
|
+
def self.serialize_on_key(array_of_hash, key)
|
|
42
|
+
array_of_hash.map { |h| h[key] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# override de-serializing journal format
|
|
47
|
+
#
|
|
48
|
+
def self.load_data(io, path)
|
|
49
|
+
{}.tap do |record|
|
|
50
|
+
body = false
|
|
51
|
+
record[:id] = path[0] + path[1]
|
|
52
|
+
CSV.new(io, headers: false, col_sep: "\t", encoding: 'UTF-8')
|
|
53
|
+
.each.with_index(0) do |line, i|
|
|
54
|
+
case i
|
|
55
|
+
when 0
|
|
56
|
+
record[:debit] = line.map { |row| { code: row } }
|
|
57
|
+
when 1
|
|
58
|
+
line.each_with_index do |amount, i|
|
|
59
|
+
record[:debit][i][:amount] = amount.to_i # TODO: bigdecimal support
|
|
60
|
+
end
|
|
61
|
+
when 2
|
|
62
|
+
record[:credit] = line.map { |row| { code: row } }
|
|
63
|
+
when 3
|
|
64
|
+
line.each_with_index do |amount, i|
|
|
65
|
+
record[:credit][i][:amount] = amount.to_i # TODO: bigdecimal support
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
if line.empty?
|
|
69
|
+
record[:note] ||= []
|
|
70
|
+
body = true
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
record[:note] << line.join(' ') if body
|
|
74
|
+
end
|
|
75
|
+
record[:note]&.join('\n')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
|
|
2
|
+
require 'csv'
|
|
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
|
+
#
|
|
11
|
+
# Statement on specified term
|
|
12
|
+
#
|
|
13
|
+
module LucaBook
|
|
14
|
+
class State < LucaRecord::Base
|
|
15
|
+
@dirname = 'journals'
|
|
16
|
+
@record_type = 'raw'
|
|
17
|
+
|
|
18
|
+
attr_reader :statement
|
|
19
|
+
|
|
20
|
+
def initialize(data)
|
|
21
|
+
@data = data
|
|
22
|
+
@dict = LucaRecord::Dict.load('base.tsv')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# TODO: not compatible with LucaRecord::Base.open_records
|
|
26
|
+
def search_tag(code)
|
|
27
|
+
count = 0
|
|
28
|
+
Dir.children(LucaSupport::Config::Pjdir).sort.each do |dir|
|
|
29
|
+
next if ! FileTest.directory?(LucaSupport::Config::Pjdir+dir)
|
|
30
|
+
|
|
31
|
+
open_records(datadir, dir, 3) do |row, i|
|
|
32
|
+
next if i == 2
|
|
33
|
+
count += 1 if row.include?(code)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
puts "#{code}: #{count}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.term(from_year, from_month, to_year = from_year, to_month = from_month)
|
|
40
|
+
date = Date.new(from_year.to_i, from_month.to_i, -1)
|
|
41
|
+
last_date = Date.new(to_year.to_i, to_month.to_i, -1)
|
|
42
|
+
raise 'invalid term specified' if date > last_date
|
|
43
|
+
|
|
44
|
+
reports = [].tap do |r|
|
|
45
|
+
while date <= last_date do
|
|
46
|
+
r << accumulate_month(date.year, date.month)
|
|
47
|
+
date = date.next_month
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
new reports
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def by_code(code, year=nil, month=nil)
|
|
54
|
+
raise "not supported year range yet" if ! year.nil? && month.nil?
|
|
55
|
+
|
|
56
|
+
bl = @book.load_start.dig(code) || 0
|
|
57
|
+
full_term = scan_terms(LucaSupport::Config::Pjdir)
|
|
58
|
+
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)
|
|
62
|
+
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)
|
|
66
|
+
}.flatten.prepend(start)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def records_with_balance(year, month, code, balance)
|
|
71
|
+
@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)
|
|
73
|
+
h[:balance] = balance
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
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
|
+
def to_yaml
|
|
105
|
+
YAML.dump(code2label).tap { |data| puts data }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def code2label
|
|
109
|
+
@statement ||= @data
|
|
110
|
+
@statement.map do |report|
|
|
111
|
+
{}.tap do |h|
|
|
112
|
+
report.each { |k, v| h[@dict.dig(k, :label)] = v }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def bs
|
|
118
|
+
@statement = @data.map do |data|
|
|
119
|
+
data.select { |k, v| /^[0-9].+/.match(k) }
|
|
120
|
+
end
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pl
|
|
125
|
+
@statement = @data.map do |data|
|
|
126
|
+
data.select { |k, v| /^[A-F].+/.match(k) }
|
|
127
|
+
end
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.accumulate_month(year, month)
|
|
132
|
+
monthly_record = net(year, month)
|
|
133
|
+
total_subaccount(monthly_record)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def amount_by_code(items, code)
|
|
137
|
+
items
|
|
138
|
+
.select{|item| item.dig(:code) == code }
|
|
139
|
+
.inject(0){|sum, item| sum + item[:amount] }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.total_subaccount(report)
|
|
143
|
+
report.dup.tap do |res|
|
|
144
|
+
report.each do |k, v|
|
|
145
|
+
if k.length >= 4
|
|
146
|
+
if res[k[0, 3]]
|
|
147
|
+
res[k[0, 3]] += v
|
|
148
|
+
else
|
|
149
|
+
res[k[0, 3]] = v
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
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]/)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.sum_matched(report, reg)
|
|
174
|
+
report.select { |k, v| reg.match(k)}.values.sum
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# for assert purpose
|
|
178
|
+
def self.gross(year, month = nil, code = nil, date_range = nil, rows = 4)
|
|
179
|
+
if ! date_range.nil?
|
|
180
|
+
raise if date_range.class != Range
|
|
181
|
+
# TODO: date based range search
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
sum = { debit: {}, credit: {} }
|
|
185
|
+
idx_memo = []
|
|
186
|
+
asof(year, month) do |f, _path|
|
|
187
|
+
CSV.new(f, headers: false, col_sep: "\t", encoding: 'UTF-8')
|
|
188
|
+
.each_with_index do |row, i|
|
|
189
|
+
break if i >= rows
|
|
190
|
+
case i
|
|
191
|
+
when 0
|
|
192
|
+
idx_memo = row.map(&:to_s)
|
|
193
|
+
idx_memo.each { |r| sum[:debit][r] ||= 0 }
|
|
194
|
+
when 1
|
|
195
|
+
row.each_with_index { |r, i| sum[:debit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
|
|
196
|
+
when 2
|
|
197
|
+
idx_memo = row.map(&:to_s)
|
|
198
|
+
idx_memo.each { |r| sum[:credit][r] ||= 0 }
|
|
199
|
+
when 3
|
|
200
|
+
row.each_with_index { |r, i| sum[:credit][idx_memo[i]] += r.to_i } # TODO: bigdecimal support
|
|
201
|
+
else
|
|
202
|
+
puts row # for debug
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
sum
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# netting vouchers in specified term
|
|
210
|
+
def self.net(year, month = nil, code = nil, date_range = nil)
|
|
211
|
+
g = gross(year, month, code, date_range)
|
|
212
|
+
idx = (g[:debit].keys + g[:credit].keys).uniq.sort
|
|
213
|
+
{}.tap do |sum|
|
|
214
|
+
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)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def load_start
|
|
222
|
+
file = LucaSupport::Config::Pjdir + 'start.tsv'
|
|
223
|
+
{}.tap do |dic|
|
|
224
|
+
load_tsv(file) do |row|
|
|
225
|
+
dic[row[0]] = row[2].to_i if ! row[2].nil?
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def self.calc_diff(num, code)
|
|
231
|
+
amount = /\./.match(num.to_s) ? BigDecimal(num) : num.to_i
|
|
232
|
+
amount * pn_debit(code.to_s)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def self.pn_debit(code)
|
|
236
|
+
case code
|
|
237
|
+
when /^[0-4BCEGH]/
|
|
238
|
+
1
|
|
239
|
+
when /^[5-9ADF]/
|
|
240
|
+
-1
|
|
241
|
+
else
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def dict
|
|
247
|
+
LucaBook::Dict::Data
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|