lucabook 0.2.22 → 0.2.27

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.
@@ -35,13 +35,13 @@ module LucaBook
35
35
  # "debit" : [
36
36
  # {
37
37
  # "label": "savings accounts",
38
- # "value": 20000
38
+ # "amount": 20000
39
39
  # }
40
40
  # ],
41
41
  # "credit" : [
42
42
  # {
43
43
  # "label": "trade notes receivable",
44
- # "value": 20000
44
+ # "amount": 20000
45
45
  # }
46
46
  # ],
47
47
  # "note": "settlement for the last month trade"
@@ -75,20 +75,21 @@ module LucaBook
75
75
  # convert single entry data
76
76
  #
77
77
  def parse_single(row)
78
- if (row.dig(@config[:credit_value]) || []).empty?
79
- value = BigDecimal(row[@config[:debit_value]])
78
+ if (row.dig(@config[:credit_amount]) || []).empty?
79
+ amount = BigDecimal(row[@config[:debit_amount]])
80
80
  debit = true
81
81
  else
82
- value = BigDecimal(row[@config[:credit_value]])
82
+ amount = BigDecimal(row[@config[:credit_amount]])
83
83
  end
84
84
  default_label = debit ? @config.dig(:default_debit) : @config.dig(:default_credit)
85
- code, options = search_code(row[@config[:label]], default_label, value)
85
+ code, options = search_code(row[@config[:label]], default_label, amount)
86
86
  counter_code = @code_map.dig(@config[:counter_label])
87
- if respond_to? :tax_extension
88
- data, data_c = tax_extension(code, counter_code, value, options) if options
87
+ if options
88
+ x_customer = options[:'x-customer'] if options[:'x-customer']
89
+ data, data_c = tax_extension(code, counter_code, amount, options) if respond_to? :tax_extension
89
90
  end
90
- data ||= [{ 'code' => code, 'value' => value }]
91
- data_c ||= [{ 'code' => counter_code, 'value' => value }]
91
+ data ||= [{ 'code' => code, 'amount' => amount }]
92
+ data_c ||= [{ 'code' => counter_code, 'amount' => amount }]
92
93
  {}.tap do |d|
93
94
  d['date'] = parse_date(row)
94
95
  if debit
@@ -99,7 +100,8 @@ module LucaBook
99
100
  d['credit'] = data
100
101
  end
101
102
  d['note'] = Array(@config[:note]).map{ |col| row[col] }.join(' ')
102
- d['x-editor'] = "LucaBook::Import/#{@dict_name}"
103
+ d['headers'] = { 'x-editor' => "LucaBook::Import/#{@dict_name}" }
104
+ d['headers']['x-customer'] = x_customer if x_customer
103
105
  end
104
106
  end
105
107
 
@@ -110,11 +112,11 @@ module LucaBook
110
112
  d['date'] = parse_date(row)
111
113
  d['debit'] = {
112
114
  'code' => search_code(row[@config[:label]], @config.dig(:default_debit)) || DEBIT_DEFAULT,
113
- 'value' => row.dig(@config[:debit_value])
115
+ 'amount' => row.dig(@config[:debit_amount])
114
116
  }
115
117
  d['credit'] = {
116
118
  'code' => search_code(row[@config[:label]], @config.dig(:default_credit)) || CREDIT_DEFAULT,
117
- 'value' => row.dig(@config[:credit_value])
119
+ 'amount' => row.dig(@config[:credit_amount])
118
120
  }
119
121
  d['note'] = Array(@config[:note]).map{ |col| row[col] }.join(' ')
120
122
  d['x-editor'] = "LucaBook::Import/#{@dict_name}"
@@ -31,17 +31,17 @@ module LucaBook
31
31
  amount1 = amount
32
32
  amount1 -= consumption_amount if consumption_idx == 0
33
33
  amount1 += gensen_amount if gensen_idx == 1
34
- res1 << { 'code' => code1, 'value' => amount1 }
35
- res1 << { 'code' => consumption_code, 'value' => consumption_amount } if consumption_idx == 0
36
- res1 << { 'code' => gensen_code, 'value' => gensen_amount } if gensen_idx == 0
34
+ res1 << { 'code' => code1, 'amount' => amount1 }
35
+ res1 << { 'code' => consumption_code, 'amount' => consumption_amount } if consumption_idx == 0
36
+ res1 << { 'code' => gensen_code, 'amount' => gensen_amount } if gensen_idx == 0
37
37
  end
38
38
  res << [].tap do |res2|
39
39
  amount2 = amount
40
40
  amount2 -= consumption_amount if consumption_idx == 1
41
41
  amount2 += gensen_amount if gensen_idx == 0
42
- res2 << { 'code' => code2, 'value' => amount2 }
43
- res2 << { 'code' => consumption_code, 'value' => consumption_amount } if consumption_idx == 1
44
- res2 << { 'code' => gensen_code, 'value' => gensen_amount } if gensen_idx == 1
42
+ res2 << { 'code' => code2, 'amount' => amount2 }
43
+ res2 << { 'code' => consumption_code, 'amount' => consumption_amount } if consumption_idx == 1
44
+ res2 << { 'code' => gensen_code, 'amount' => gensen_amount } if gensen_idx == 1
45
45
  end
46
46
  end
47
47
  elsif options[:tax_options].include?('jp-gensen')
@@ -51,15 +51,15 @@ module LucaBook
51
51
  res << [].tap do |res1|
52
52
  amount1 = amount
53
53
  amount1 += gensen_amount if gensen_idx == 1
54
- res1 << { 'code' => code, 'value' => amount1 }
55
- res1 << { 'code' => gensen_code, 'value' => gensen_amount } if gensen_idx == 0
54
+ res1 << { 'code' => code, 'amount' => amount1 }
55
+ res1 << { 'code' => gensen_code, 'amount' => gensen_amount } if gensen_idx == 0
56
56
  end
57
57
  res << [].tap do |res2|
58
58
  amount2 = amount
59
59
  amount2 += gensen_amount if gensen_idx == 0
60
60
  mount2 ||= amount
61
- res2 << { 'code' => code2, 'value' => amount2 }
62
- res2 << { 'code' => gensen_code, 'value' => gensen_amount } if gensen_idx == 1
61
+ res2 << { 'code' => code2, 'amount' => amount2 }
62
+ res2 << { 'code' => gensen_code, 'amount' => gensen_amount } if gensen_idx == 1
63
63
  end
64
64
  end
65
65
  elsif options[:tax_options].include?('jp-consumption')
@@ -68,14 +68,14 @@ module LucaBook
68
68
  res << [].tap do |res1|
69
69
  amount1 = amount
70
70
  amount1 -= consumption_amount if consumption_idx == 0
71
- res1 << { 'code' => code1, 'value' => amount1 }
72
- res1 << { 'code' => consumption_code, 'value' => consumption_amount } if consumption_idx == 0
71
+ res1 << { 'code' => code1, 'amount' => amount1 }
72
+ res1 << { 'code' => consumption_code, 'amount' => consumption_amount } if consumption_idx == 0
73
73
  end
74
74
  res << [].tap do |res2|
75
75
  amount2 = amount
76
76
  amount2 -= consumption_amount if consumption_idx == 1
77
- res2 << { 'code' => code2, 'value' => amount2 }
78
- res2 << { 'code' => consumption_code, 'value' => consumption_amount } if consumption_idx == 1
77
+ res2 << { 'code' => code2, 'amount' => amount2 }
78
+ res2 << { 'code' => consumption_code, 'amount' => consumption_amount } if consumption_idx == 1
79
79
  end
80
80
  end
81
81
  end
@@ -1,44 +1,90 @@
1
- #
2
- # manipulate files based on transaction date
3
- #
1
+ # frozen_string_literal: true
4
2
 
5
3
  require 'csv'
6
4
  require 'date'
7
5
  require 'luca_record'
8
6
 
9
- module LucaBook
7
+ module LucaBook #:nodoc:
8
+ # Journal has several annotations on headers:
9
+ #
10
+ # x-customer::
11
+ # Identifying customer.
12
+ # x-editor::
13
+ # Application name editing the journal.
14
+ # x-tax::
15
+ # For tracking tax related transaction.
16
+ #
10
17
  class Journal < LucaRecord::Base
18
+ ACCEPTED_HEADERS = ['x-customer', 'x-editor', 'x-tax']
11
19
  @dirname = 'journals'
12
20
 
13
21
  # create journal from hash
14
22
  #
15
- def self.create(d)
23
+ def self.create(dat)
24
+ d = LucaSupport::Code.keys_stringify(dat)
16
25
  validate(d)
26
+ raise 'NoDateKey' unless d.key?('date')
27
+
17
28
  date = Date.parse(d['date'])
18
29
 
19
- debit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['debit'], 'value'))
20
- credit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['credit'], 'value'))
30
+ # TODO: need to sync filename & content. Limit code length for filename
31
+ # codes = (debit_code + credit_code).uniq
32
+ codes = nil
33
+
34
+ create_record(nil, date, codes) { |f| f.write journal2csv(d) }
35
+ end
36
+
37
+ # update journal with hash.
38
+ # If record not found with id, no record will be created.
39
+ #
40
+ def self.save(dat)
41
+ d = LucaSupport::Code.keys_stringify(dat)
42
+ raise 'record has no id.' if d['id'].nil?
43
+
44
+ validate(d)
45
+ parts = d['id'].split('/')
46
+ raise 'invalid ID' if parts.length != 2
47
+
48
+ codes = nil
49
+ open_records(@dirname, parts[0], parts[1], codes, 'w') { |f, _path| f.write journal2csv(d) }
50
+ end
51
+
52
+ # Convert journal object to TSV format.
53
+ #
54
+ def self.journal2csv(d)
55
+ debit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['debit'], 'amount'))
56
+ credit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['credit'], 'amount'))
21
57
  raise 'BalanceUnmatch' if debit_amount.inject(:+) != credit_amount.inject(:+)
22
58
 
23
59
  debit_code = serialize_on_key(d['debit'], 'code')
24
60
  credit_code = serialize_on_key(d['credit'], 'code')
25
61
 
26
- # TODO: need to sync filename & content. Limit code length for filename
27
- # codes = (debit_code + credit_code).uniq
28
- codes = nil
29
- create_record!(date, codes) do |f|
62
+ csv = CSV.generate(String.new, col_sep: "\t", headers: false) do |f|
30
63
  f << debit_code
31
64
  f << LucaSupport::Code.readable(debit_amount)
32
65
  f << credit_code
33
66
  f << LucaSupport::Code.readable(credit_amount)
34
- ['x-customer', 'x-editor'].each do |x_header|
35
- f << [x_header, d[x_header]] if d.dig(x_header)
67
+ ACCEPTED_HEADERS.each do |x_header|
68
+ f << [x_header, d['headers'][x_header]] if d.dig('headers', x_header)
36
69
  end
37
70
  f << []
38
71
  f << [d.dig('note')]
39
72
  end
40
73
  end
41
74
 
75
+ # Set accepted header with key/value, update record if exists.
76
+ #
77
+ def self.add_header(journal_hash, key, val)
78
+ return journal_hash if val.nil?
79
+ return journal_hash unless ACCEPTED_HEADERS.include?(key)
80
+
81
+ journal_hash.tap do |o|
82
+ o[:headers] = {} unless o.dig(:headers)
83
+ o[:headers][key] = val
84
+ save o if o[:id]
85
+ end
86
+ end
87
+
42
88
  def self.update_codes(obj)
43
89
  debit_code = serialize_on_key(obj[:debit], :code)
44
90
  credit_code = serialize_on_key(obj[:credit], :code)
@@ -46,27 +92,19 @@ module LucaBook
46
92
  change_codes(obj[:id], codes)
47
93
  end
48
94
 
49
- # define new transaction ID & write data at once
50
- def self.create_record!(date_obj, codes = nil)
51
- create_record(nil, date_obj, codes) do |f|
52
- f.write CSV.generate('', col_sep: "\t", headers: false) { |c| yield(c) }
53
- end
54
- end
55
-
56
95
  def self.validate(obj)
57
- raise 'NoDateKey' unless obj.key?('date')
58
96
  raise 'NoDebitKey' unless obj.key?('debit')
59
97
  raise 'NoCreditKey' unless obj.key?('credit')
60
98
  debit_codes = serialize_on_key(obj['debit'], 'code').compact
61
- debit_values = serialize_on_key(obj['debit'], 'value').compact
99
+ debit_amount = serialize_on_key(obj['debit'], 'amount').compact
62
100
  raise 'NoDebitCode' if debit_codes.empty?
63
- raise 'NoDebitValue' if debit_values.empty?
64
- raise 'UnmatchDebit' if debit_codes.length != debit_values.length
101
+ raise 'NoDebitAmount' if debit_amount.empty?
102
+ raise 'UnmatchDebit' if debit_codes.length != debit_amount.length
65
103
  credit_codes = serialize_on_key(obj['credit'], 'code').compact
66
- credit_values = serialize_on_key(obj['credit'], 'value').compact
104
+ credit_amount = serialize_on_key(obj['credit'], 'amount').compact
67
105
  raise 'NoCreditCode' if credit_codes.empty?
68
- raise 'NoCreditValue' if credit_values.empty?
69
- raise 'UnmatchCredit' if credit_codes.length != credit_values.length
106
+ raise 'NoCreditAmount' if credit_amount.empty?
107
+ raise 'UnmatchCredit' if credit_codes.length != credit_amount.length
70
108
  end
71
109
 
72
110
  # collect values on specified key
@@ -75,7 +113,21 @@ module LucaBook
75
113
  array_of_hash.map { |h| h[key] }
76
114
  end
77
115
 
78
- # override de-serializing journal format
116
+ # override de-serializing journal format. Sample format is:
117
+ #
118
+ # {
119
+ # id: '2021A/V001',
120
+ # headers: {
121
+ # 'x-customer' => 'Some Customer Co.'
122
+ # },
123
+ # debit: [
124
+ # { code: 'A12', amount: 1000 }
125
+ # ],
126
+ # credit: [
127
+ # { code: '311', amount: 1000 }
128
+ # ],
129
+ # note: 'note for each journal'
130
+ # }
79
131
  #
80
132
  def self.load_data(io, path)
81
133
  {}.tap do |record|
@@ -93,10 +145,16 @@ module LucaBook
93
145
  when 3
94
146
  line.each_with_index { |amount, j| record[:credit][j][:amount] = BigDecimal(amount.to_s) }
95
147
  else
96
- if body == false && line.empty?
97
- record[:note] ||= []
98
- body = true
99
- else
148
+ case body
149
+ when false
150
+ if line.empty?
151
+ record[:note] ||= []
152
+ body = true
153
+ else
154
+ record[:headers] ||= {}
155
+ record[:headers][line[0]] = line[1]
156
+ end
157
+ when true
100
158
  record[:note] << line.join(' ') if body
101
159
  end
102
160
  end
@@ -7,37 +7,53 @@ require 'luca_record'
7
7
  require 'luca_record/dict'
8
8
  require 'luca_book'
9
9
 
10
- # Journal List on specified term
11
- #
12
- module LucaBook
10
+ module LucaBook #:nodoc:
11
+ # Journal List on specified term
12
+ #
13
13
  class List < LucaBook::Journal
14
14
  @dirname = 'journals'
15
+ @@dict = LucaRecord::Dict.new('base.tsv')
16
+ attr_reader :data
15
17
 
16
18
  def initialize(data, start_date, code = nil)
17
19
  @data = data
18
20
  @code = code
19
21
  @start = start_date
20
- @dict = LucaRecord::Dict.load('base.tsv')
21
22
  end
22
23
 
23
- def self.term(from_year, from_month, to_year = from_year, to_month = from_month, code: nil, basedir: @dirname)
24
+ def self.term(from_year, from_month, to_year = from_year, to_month = from_month, code: nil, basedir: @dirname, recursive: false)
25
+ code = search_code(code) if code
24
26
  data = LucaBook::Journal.term(from_year, from_month, to_year, to_month, code).select do |dat|
25
27
  if code.nil?
26
28
  true
27
29
  else
28
- [:debit, :credit].map { |key| serialize_on_key(dat[key], :code) }.flatten.include?(code)
30
+ if recursive
31
+ ! [:debit, :credit].map { |key| serialize_on_key(dat[key], :code) }.flatten.select { |idx| /^#{code}/.match(idx) }.empty?
32
+ else
33
+ [:debit, :credit].map { |key| serialize_on_key(dat[key], :code) }.flatten.include?(code)
34
+ end
29
35
  end
30
36
  end
31
37
  new data, Date.new(from_year.to_i, from_month.to_i, 1), code
32
38
  end
33
39
 
34
- def list_on_code
35
- calc_code
40
+ def self.add_header(from_year, from_month, to_year = from_year, to_month = from_month, code: nil, header_key: nil, header_val: nil)
41
+ return nil if code.nil?
42
+ return nil unless Journal::ACCEPTED_HEADERS.include?(header_key)
43
+
44
+ term(from_year, from_month, to_year, to_month, code: code)
45
+ .data.each do |journal|
46
+ Journal.add_header(journal, header_key, header_val)
47
+ end
48
+ end
49
+
50
+ def list_by_code(recursive = false)
51
+ calc_code(recursive: recursive)
36
52
  convert_label
37
53
  @data = [code_header] + @data.map do |dat|
38
54
  date, txid = LucaSupport::Code.decode_id(dat[:id])
39
55
  {}.tap do |res|
40
- res['code'] = dat[:code]
56
+ res['code'] = dat[:code].length == 1 ? dat[:code].first : dat[:code]
41
57
  res['date'] = date
42
58
  res['no'] = txid
43
59
  res['id'] = dat[:id]
@@ -59,7 +75,7 @@ module LucaBook
59
75
  res['no'] = txid
60
76
  res['id'] = dat[:id]
61
77
  res['debit_code'] = dat[:debit].length == 1 ? dat[:debit][0][:code] : dat[:debit].map { |d| d[:code] }
62
- res['debit_amount'] = dat[:debit].inject(0) { |sum, d| sum + d[:amount] }
78
+ res['debit_amount'] = dat[:debit].inject(0) { |sum, d| sum + d[:amount] }
63
79
  res['credit_code'] = dat[:credit].length == 1 ? dat[:credit][0][:code] : dat[:credit].map { |d| d[:code] }
64
80
  res['credit_amount'] = dat[:credit].inject(0) { |sum, d| sum + d[:amount] }
65
81
  res['note'] = dat[:note]
@@ -74,32 +90,35 @@ module LucaBook
74
90
  end
75
91
  end
76
92
 
93
+ def self.search_code(code)
94
+ return code if @@dict.dig(code)
95
+
96
+ @@dict.search(code).tap do |new_code|
97
+ if new_code.nil?
98
+ puts "Search word is not matched with labels"
99
+ exit 1
100
+ end
101
+ end
102
+ end
103
+
77
104
  private
78
105
 
79
- def set_balance
106
+ def set_balance(recursive = false)
80
107
  return BigDecimal('0') if @code.nil? || /^[A-H]/.match(@code)
81
108
 
82
- balance_dict = Dict.latest_balance
83
- start_balance = BigDecimal(balance_dict.dig(@code.to_s, :balance) || '0')
84
- start = Dict.issue_date(balance_dict)&.next_month
85
- last = @start.prev_month
86
- if last.year >= start.year && last.month >= start.month
87
- start_balance + self.class.term(start.year, start.month, last.year, last.month, code: @code).accumulate_code
88
- else
89
- start_balance
90
- end
109
+ LucaBook::State.start_balance(@start.year, @start.month, recursive: recursive)
91
110
  end
92
111
 
93
- def calc_code
94
- @balance = set_balance
112
+ def calc_code(recursive: false)
113
+ @balance = set_balance(recursive)[@code] || BigDecimal('0')
95
114
  if @code
96
115
  balance = @balance
97
116
  @data.each do |dat|
98
117
  dat[:diff] = Util.diff_by_code(dat[:debit], @code) - Util.diff_by_code(dat[:credit], @code)
99
118
  balance += dat[:diff]
100
119
  dat[:balance] = balance
101
- dat[:code] = @code
102
- counter = dat[:diff] * Util.pn_debit(@code) > 0 ? :credit : :debit
120
+ target, counter = dat[:diff] * Util.pn_debit(@code) > 0 ? [:debit, :credit] : [:credit, :debit]
121
+ dat[:code] = dat[target].map { |d| d[:code] }
103
122
  dat[:counter_code] = dat[counter].map { |d| d[:code] }
104
123
  end
105
124
  end
@@ -109,20 +128,16 @@ module LucaBook
109
128
  def convert_label
110
129
  @data.each do |dat|
111
130
  if @code
112
- dat[:code] = "#{dat[:code]} #{@dict.dig(dat[:code], :label)}"
113
- dat[:counter_code] = dat[:counter_code].map { |counter| "#{counter} #{@dict.dig(counter, :label)}" }
131
+ dat[:code] = dat[:code].map { |target| "#{target} #{@@dict.dig(target, :label)}" }
132
+ dat[:counter_code] = dat[:counter_code].map { |counter| "#{counter} #{@@dict.dig(counter, :label)}" }
114
133
  else
115
- dat[:debit].each { |debit| debit[:code] = "#{debit[:code]} #{@dict.dig(debit[:code], :label)}" }
116
- dat[:credit].each { |credit| credit[:code] = "#{credit[:code]} #{@dict.dig(credit[:code], :label)}" }
134
+ dat[:debit].each { |debit| debit[:code] = "#{debit[:code]} #{@@dict.dig(debit[:code], :label)}" }
135
+ dat[:credit].each { |credit| credit[:code] = "#{credit[:code]} #{@@dict.dig(credit[:code], :label)}" }
117
136
  end
118
137
  end
119
138
  self
120
139
  end
121
140
 
122
- def dict
123
- LucaBook::Dict::Data
124
- end
125
-
126
141
  def code_header
127
142
  {}.tap do |h|
128
143
  %w[code date no id diff balance counter_code note].each do |k|