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