lucabook 0.2.8 → 0.2.17

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: 8a024fa5882d001a6fce7d60863cf0d5d1fefbb4ae92dc0a774a5ce79eeb5749
4
- data.tar.gz: 7ce4e31ef3c99a02790a462a5db798fd65c9248ef1b2e467ad6a6920f0fa8cee
3
+ metadata.gz: 5b204a2ed70889d0750a6883de424ceb3b7e335cce99262eb98e288db6962cbd
4
+ data.tar.gz: e19db20331a4edce1ea02ffb47495c3f849f74ba0bf78b13c7cf0562207fc0d2
5
5
  SHA512:
6
- metadata.gz: 911cd82032b9d138e356d5a1d1fd7ca95c396ae953426071fd7334ec33cae25882746a5f4b353396211b24de3c79c16a2b8767f172e6368dc1dc0d5195b8a278
7
- data.tar.gz: eda9e10010600b88708b08d5b099d18f870b73dea3d59d6ea7f8ac216588d4c4335846a4c084c90a470dad90facfa0aa076cfa625ddc6615c65302d834162006
6
+ metadata.gz: 16800a60362abcc56adb26ed60611a98a5ec5c3545143b4a0c4a379aac3988952e733dac19bf5c2188ce9e7c244dff1b4d6269635a226eeda51ec85c4ab2178f
7
+ data.tar.gz: f60f61de1a1c18df3db8c1bc5278ae084089137b15ee88f198ff17c6418917c6adebb634820dcd9979b42a9ecf7e902329c7bb14fb950f5270154492a2b0d8b2
@@ -2,68 +2,112 @@
2
2
 
3
3
  require 'optparse'
4
4
  require 'luca_book'
5
- require 'luca_book/console'
6
5
 
7
- def import(args, params)
8
- LucaBook::Import.new(args[0]).import_csv
9
- end
6
+ module LucaCmd
7
+ class Journal
8
+ def self.import(args, params)
9
+ if params['config']
10
+ LucaBook::Import.new(args[0], params['config']).import_csv
11
+ elsif params['json']
12
+ LucaBook::Import.import_json(STDIN.read)
13
+ else
14
+ puts 'Usage: luca-book import -c import_config'
15
+ end
16
+ end
17
+
18
+ def self.list(args, params)
19
+ if params['code']
20
+ LucaBook::List.term(*args, code: params['code']).list_on_code.to_yaml
21
+ elsif args.length > 0
22
+ LucaBook::List.term(*args).list_journals.to_yaml
23
+ else
24
+ # TODO: define default function
25
+ end
26
+ end
10
27
 
11
- def list(args, params)
12
- if params["c"] or params["code"]
13
- code = params["c"] || params["code"]
14
- LucaBookConsole.new.by_code(code, args.dig(0), args.dig(1))
15
- elsif args.length > 0
16
- LucaBookConsole.new.by_month(args[0], args.dig(1))
17
- else
18
- LucaBookConsole.new.all
28
+ def self.stats(args, params)
29
+ LucaBook::State.term(*args).stats(params[:level])
30
+ end
19
31
  end
20
- end
21
32
 
22
- def report(args, params)
23
- if params['bs']
24
- LucaBook::State.term(*args).bs.to_yaml
25
- elsif params['pl']
26
- LucaBook::State.term(*args).pl.to_yaml
27
- else
28
- LucaBook::State.term(*args).to_yaml
33
+ class Report
34
+ def self.balancesheet(args, params)
35
+ level = params[:level] || 2
36
+ legal = params[:legal] || false
37
+ LucaBook::State.term(*args).bs(level, legal: legal)
38
+ end
39
+
40
+ def self.profitloss(args, params)
41
+ LucaBook::State.term(*args).pl.to_yaml
42
+ end
29
43
  end
30
44
  end
31
45
 
46
+ def new_pj(args = nil, params = {})
47
+ LucaBook::Setup.create_project params['country'], args[0]
48
+ end
49
+
32
50
  LucaRecord::Base.valid_project?
33
51
  cmd = ARGV.shift
52
+ params = {}
34
53
 
35
54
  case cmd
36
- when 'import'
37
- params = {}
38
- OptionParser.new do |opt|
39
- opt.banner = 'Usage: luca import filepath'
40
- # TODO: need to handle import config
41
- args = opt.parse!(ARGV)
42
- import(args, params)
55
+ when /journals?/, 'j'
56
+ subcmd = ARGV.shift
57
+ case subcmd
58
+ when 'import'
59
+ OptionParser.new do |opt|
60
+ opt.banner = 'Usage: luca-book journals import filepath'
61
+ opt.on('-c', '--config VAL', 'import definition'){ |v| params['config'] = v }
62
+ opt.on('-j', '--json', 'import via json format'){ |_v| params['json'] = true }
63
+ args = opt.parse!(ARGV)
64
+ LucaCmd::Journal.import(args, params)
65
+ end
66
+ when 'list'
67
+ OptionParser.new do |opt|
68
+ opt.banner = 'Usage: luca-book journals list [year month]'
69
+ opt.on('-c', '--code VAL', 'search with code') { |v| params['code'] = v }
70
+ opt.on_tail('List records. If you specify code and/or month, search on each criteria.')
71
+ args = opt.parse!(ARGV)
72
+ LucaCmd::Journal.list(args, params)
73
+ end
74
+ when 'stats'
75
+ OptionParser.new do |opt|
76
+ opt.banner = 'Usage: luca-book reports bs'
77
+ opt.on('-l', '--level VAL', 'account level') { |v| params[:level] = v.to_i }
78
+ args = opt.parse!(ARGV)
79
+ LucaCmd::Journal.stats(args, params)
80
+ end
43
81
  end
44
- when 'list'
45
- params = {}
82
+ when 'new'
46
83
  OptionParser.new do |opt|
47
- opt.banner = 'Usage: luca list [year month]'
48
- opt.on('-c', '--code VAL', 'search with code'){|v| params["code"] = v }
49
- opt.on_tail('List records. If you specify code and/or month, search on each criteria.')
50
- args = opt.parse!(ARGV)
51
- list(args, params)
84
+ opt.banner = 'Usage: luca-book new Dir'
85
+ opt.on('-c', '--country VAL', 'specify country code') { |v| params['coountry'] = v }
86
+ args = opt.parse(ARGV)
87
+ new_pj(args, params)
52
88
  end
53
- when 'report'
54
- params = {}
55
- OptionParser.new do |opt|
56
- opt.banner = 'Usage: luca report'
57
- opt.on('--bs', 'show Balance sheet'){|v| params["bs"] = v }
58
- opt.on('--pl', 'show Income statement'){|v| params["pl"] = v }
59
- args = opt.parse!(ARGV)
60
- report(args, params)
89
+ when /reports?/, 'r'
90
+ subcmd = ARGV.shift
91
+ case subcmd
92
+ when 'bs'
93
+ OptionParser.new do |opt|
94
+ opt.banner = 'Usage: luca-book reports bs'
95
+ opt.on('-l', '--level VAL', 'account level') { |v| params[:level] = v.to_i }
96
+ opt.on('--legal', 'show legal mandatory account') { |_v| params[:legal] = true }
97
+ args = opt.parse!(ARGV)
98
+ LucaCmd::Report.balancesheet(args, params)
99
+ end
100
+ when 'pl'
101
+ OptionParser.new do |opt|
102
+ opt.banner = 'Usage: luca-book reports pl'
103
+ args = opt.parse!(ARGV)
104
+ LucaCmd::Report.profitloss(args, params)
105
+ end
61
106
  end
62
107
  when '--help'
63
- puts 'Usage: luca subcommand'
64
- puts ' import: import records'
65
- puts ' list: list records'
66
- puts ' report: show reports'
108
+ puts 'Usage: luca-book subcommand'
109
+ puts ' journals: operate journal records'
110
+ puts ' reports: show reports'
67
111
  else
68
112
  puts 'Invalid subcommand'
69
113
  end
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'csv'
3
4
  require 'luca_record'
4
5
  require 'luca_book/version'
5
6
 
6
7
  module LucaBook
8
+ autoload :Dict, 'luca_book/dict'
7
9
  autoload :Import, 'luca_book/import'
8
10
  autoload :Journal, 'luca_book/journal'
11
+ autoload :List, 'luca_book/list'
12
+ autoload :Setup, 'luca_book/setup'
9
13
  autoload :State, 'luca_book/state'
14
+ autoload :Util, 'luca_book/util'
10
15
  end
@@ -1,25 +1,15 @@
1
1
  require 'luca_book'
2
2
 
3
+ # This class will be deleted
4
+ #
3
5
  class LucaBookConsole
4
6
 
5
7
  def initialize(dir_path=nil)
6
- @report = LucaBookReport.new(dir_path)
8
+ @report = LucaBook::State.new(dir_path)
7
9
  end
8
10
 
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)
11
+ def by_term(year, month, end_year = year, end_month = month)
12
+ array = @report.book.class.term(year, month, end_year, end_month)
23
13
  show_records(array)
24
14
  end
25
15
 
@@ -39,6 +29,7 @@ class LucaBookConsole
39
29
  end
40
30
  end
41
31
 
32
+ # TODO: deprecated. accumulate_all() already removed.
42
33
  def bs
43
34
  target = []
44
35
  report = []
@@ -66,6 +57,7 @@ class LucaBookConsole
66
57
  puts "---- ----"
67
58
  end
68
59
 
60
+ # TODO: deprecated. accumulate_all() already removed.
69
61
  def pl
70
62
  target = []
71
63
  report = []
@@ -2,12 +2,19 @@
2
2
 
3
3
  require 'luca_support/config'
4
4
  require 'luca_record/dict'
5
+ require 'date'
6
+ require 'pathname'
5
7
 
6
8
  module LucaBook
7
9
  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')
14
+ end
8
15
 
9
- @filename = 'dict.tsv'
10
-
11
- Data = load
16
+ def self.issue_date(obj)
17
+ Date.parse(obj.dig('_date', :label))
18
+ end
12
19
  end
13
20
  end
@@ -9,70 +9,84 @@ require 'luca_record'
9
9
 
10
10
  module LucaBook
11
11
  class Import
12
- DEBIT_DEFAULT = '仮払金'
13
- CREDIT_DEFAULT = '仮受金'
12
+ DEBIT_DEFAULT = '10XX'
13
+ CREDIT_DEFAULT = '50XX'
14
14
 
15
- def initialize(path)
15
+ def initialize(path, dict)
16
16
  raise 'no such file' unless FileTest.file?(path)
17
17
 
18
18
  @target_file = path
19
19
  # TODO: yaml need to be configurable
20
- @dict = LucaRecord::Dict.new('import.yaml')
20
+ @dict_name = dict
21
+ @dict = LucaRecord::Dict.new("import-#{dict}.yaml")
21
22
  @code_map = LucaRecord::Dict.reverse(LucaRecord::Dict.load('base.tsv'))
22
- @config = @dict.csv_config
23
+ @config = @dict.csv_config if dict
23
24
  end
24
25
 
25
26
  # === 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
- # }
27
+ # [
28
+ # {
29
+ # "date": "2020-05-04",
30
+ # "debit" : [
31
+ # {
32
+ # "label": "savings accounts",
33
+ # "value": 20000
34
+ # }
35
+ # ],
36
+ # "credit" : [
37
+ # {
38
+ # "label": "trade notes receivable",
39
+ # "value": 20000
40
+ # }
41
+ # ],
42
+ # "note": "settlement for the last month trade"
43
+ # }
44
+ # ]
42
45
  #
43
- def import_json(io)
44
- d = JSON.parse(io)
45
- validate(d)
46
+ def self.import_json(io)
47
+ JSON.parse(io).each do |d|
48
+ validate(d)
46
49
 
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
+ code_map = LucaRecord::Dict.reverse(LucaRecord::Dict.load('base.tsv'))
51
+ d['debit'].each { |h| h['code'] = code_map.dig(h['label']) || DEBIT_DEFAULT }
52
+ d['credit'].each { |h| h['code'] = code_map.dig(h['label']) || CREDIT_DEFAULT }
50
53
 
51
- LucaBook.new.create!(d)
54
+ LucaBook::Journal.create(d)
55
+ end
52
56
  end
53
57
 
54
58
  def import_csv
55
59
  @dict.load_csv(@target_file) do |row|
56
60
  if @config[:type] == 'single'
57
- LucaBook::Journal.create!(parse_single(row))
61
+ LucaBook::Journal.create(parse_single(row))
58
62
  elsif @config[:type] == 'double'
59
- p parse_double(row)
63
+ p parse_double(row) # TODO: Not implemented yet
60
64
  else
61
65
  p row
62
66
  end
63
67
  end
64
68
  end
65
69
 
70
+ def self.validate(obj)
71
+ raise 'NoDateKey' unless obj.key?('date')
72
+ raise 'NoDebitKey' unless obj.key?('debit')
73
+ raise 'NoDebitValue' if obj['debit'].empty?
74
+ raise 'NoCreditKey' unless obj.key?('credit')
75
+ raise 'NoCreditValue' if obj['credit'].empty?
76
+ end
77
+
78
+ private
79
+
66
80
  #
67
81
  # convert single entry data
68
82
  #
69
83
  def parse_single(row)
70
84
  value = row.dig(@config[:credit_value])&.empty? ? row[@config[:debit_value]] : row[@config[:credit_value]]
71
- {}.tap do |d|
85
+ {}.tap do |d|
72
86
  d['date'] = parse_date(row)
73
87
  if row.dig(@config[:credit_value])&.empty?
74
88
  d['debit'] = [
75
- { 'code' => search_code(row[@config[:label]], DEBIT_DEFAULT) }
89
+ { 'code' => search_code(row[@config[:label]], @config.dig(:default_debit)) || DEBIT_DEFAULT }
76
90
  ]
77
91
  d['credit'] = [
78
92
  { 'code' => @code_map.dig(@config[:counter_label]) }
@@ -82,12 +96,13 @@ module LucaBook
82
96
  { 'code' => @code_map.dig(@config[:counter_label]) }
83
97
  ]
84
98
  d['credit'] = [
85
- { 'code' => search_code(row[@config[:label]], CREDIT_DEFAULT) }
99
+ { 'code' => search_code(row[@config[:label]], @config.dig(:default_credit)) || CREDIT_DEFAULT }
86
100
  ]
87
101
  end
88
102
  d['debit'][0]['value'] = value
89
103
  d['credit'][0]['value'] = value
90
- d['note'] = row[@config[:note]]
104
+ d['note'] = Array(@config[:note]).map{ |col| row[col] }.join(' ')
105
+ d['x-editor'] = "LucaBook::Import/#{@dict_name}"
91
106
  end
92
107
  end
93
108
 
@@ -98,14 +113,15 @@ module LucaBook
98
113
  {}.tap do |d|
99
114
  d['date'] = parse_date(row)
100
115
  d['debit'] = {
101
- 'code' => search_code(row[@config[:debit_label]], DEBIT_DEFAULT),
116
+ 'code' => search_code(row[@config[:label]], @config.dig(:default_debit)) || DEBIT_DEFAULT,
102
117
  'value' => row.dig(@config[:debit_value])
103
118
  }
104
119
  d['credit'] = {
105
- 'code' => search_code(row[@config[:credit_label]], CREDIT_DEFAULT),
120
+ 'code' => search_code(row[@config[:label]], @config.dig(:default_credit)) || CREDIT_DEFAULT,
106
121
  'value' => row.dig(@config[:credit_value])
107
122
  }
108
- d['note'] = row[@config[:note]]
123
+ d['note'] = Array(@config[:note]).map{ |col| row[col] }.join(' ')
124
+ d['x-editor'] = "LucaBook::Import/#{@dict_name}"
109
125
  end
110
126
  end
111
127
 
@@ -118,13 +134,5 @@ module LucaBook
118
134
 
119
135
  "#{row.dig(@config[:year])}-#{row.dig(@config[:month])}-#{row.dig(@config[:day])}"
120
136
  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
137
  end
130
138
  end
@@ -10,77 +10,81 @@ module LucaBook
10
10
  class Journal < LucaRecord::Base
11
11
  @dirname = 'journals'
12
12
 
13
- #
14
13
  # create journal from hash
15
14
  #
16
- def self.create!(d)
15
+ def self.create(d)
17
16
  date = Date.parse(d['date'])
18
17
 
19
- debit_amount = serialize_on_key(d['debit'], 'value')
20
- credit_amount = serialize_on_key(d['credit'], 'value')
18
+ debit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['debit'], 'value'))
19
+ credit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['credit'], 'value'))
21
20
  raise 'BalanceUnmatch' if debit_amount.inject(:+) != credit_amount.inject(:+)
22
21
 
23
22
  debit_code = serialize_on_key(d['debit'], 'code')
24
23
  credit_code = serialize_on_key(d['credit'], 'code')
25
24
 
26
- # TODO: limit code length for filename
27
- codes = (debit_code + credit_code).uniq
25
+ # TODO: need to sync filename & content. Limit code length for filename
26
+ # codes = (debit_code + credit_code).uniq
27
+ codes = nil
28
28
  create_record!(date, codes) do |f|
29
29
  f << debit_code
30
- f << debit_amount
30
+ f << LucaSupport::Code.readable(debit_amount)
31
31
  f << credit_code
32
- f << credit_amount
32
+ f << LucaSupport::Code.readable(credit_amount)
33
+ ['x-customer', 'x-editor'].each do |x_header|
34
+ f << [x_header, d[x_header]] if d.dig(x_header)
35
+ end
33
36
  f << []
34
37
  f << [d.dig('note')]
35
38
  end
36
39
  end
37
40
 
41
+ def self.update_codes(obj)
42
+ debit_code = serialize_on_key(obj[:debit], :code)
43
+ credit_code = serialize_on_key(obj[:credit], :code)
44
+ codes = (debit_code + credit_code).uniq.sort.compact
45
+ change_codes(obj[:id], codes)
46
+ end
47
+
38
48
  # define new transaction ID & write data at once
39
49
  def self.create_record!(date_obj, codes = nil)
40
- gen_record_file!(@dirname, date_obj, codes) do |f|
50
+ create_record(nil, date_obj, codes) do |f|
41
51
  f.write CSV.generate('', col_sep: "\t", headers: false) { |c| yield(c) }
42
52
  end
43
53
  end
44
54
 
45
- #
46
55
  # collect values on specified key
47
56
  #
48
57
  def self.serialize_on_key(array_of_hash, key)
49
58
  array_of_hash.map { |h| h[key] }
50
59
  end
51
60
 
52
- #
53
61
  # override de-serializing journal format
54
62
  #
55
63
  def self.load_data(io, path)
56
64
  {}.tap do |record|
57
65
  body = false
58
- record[:id] = path[0] + path[1]
66
+ record[:id] = "#{path[0]}/#{path[1]}"
59
67
  CSV.new(io, headers: false, col_sep: "\t", encoding: 'UTF-8')
60
68
  .each.with_index(0) do |line, i|
61
69
  case i
62
70
  when 0
63
71
  record[:debit] = line.map { |row| { code: row } }
64
72
  when 1
65
- line.each_with_index do |amount, i|
66
- record[:debit][i][:amount] = amount.to_i # TODO: bigdecimal support
67
- end
73
+ line.each_with_index { |amount, j| record[:debit][j][:amount] = BigDecimal(amount.to_s) }
68
74
  when 2
69
75
  record[:credit] = line.map { |row| { code: row } }
70
76
  when 3
71
- line.each_with_index do |amount, i|
72
- record[:credit][i][:amount] = amount.to_i # TODO: bigdecimal support
73
- end
77
+ line.each_with_index { |amount, j| record[:credit][j][:amount] = BigDecimal(amount.to_s) }
74
78
  else
75
- if line.empty?
79
+ if body == false && line.empty?
76
80
  record[:note] ||= []
77
81
  body = true
78
- next
82
+ else
83
+ record[:note] << line.join(' ') if body
79
84
  end
80
- record[:note] << line.join(' ') if body
81
85
  end
82
- record[:note]&.join('\n')
83
86
  end
87
+ record[:note] = record[:note]&.join('\n')
84
88
  end
85
89
  end
86
90
  end