Class: LucaBook::Journal

Inherits:
LucaRecord::Base
  • Object
show all
Defined in:
lib/luca_book/journal.rb

Overview

Journal has several annotations on headers:

x-customer

Identifying customer.

x-editor

Application name editing the journal.

x-tax

For tracking tax related transaction.

Direct Known Subclasses

List, ListByHeader

Constant Summary collapse

ACCEPTED_HEADERS =
['x-customer', 'x-editor', 'x-tax']

Class Method Summary collapse

Class Method Details

.add_header(journal_hash, key, val) ⇒ Object

Set accepted header with key/value, update record if exists.



77
78
79
80
81
82
83
84
85
86
# File 'lib/luca_book/journal.rb', line 77

def self.add_header(journal_hash, key, val)
  return journal_hash if val.nil?
  return journal_hash unless ACCEPTED_HEADERS.include?(key)

  journal_hash.tap do |o|
    o[:headers] = {} unless o.dig(:headers)
    o[:headers][key] = val
    save o if o[:id]
  end
end

.create(dat) ⇒ Object

create journal from hash



23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/luca_book/journal.rb', line 23

def self.create(dat)
  d = LucaSupport::Code.keys_stringify(dat)
  validate(d)
  raise 'NoDateKey' unless d.key?('date')

  date = Date.parse(d['date'])

  # TODO: need to sync filename & content. Limit code length for filename
  # codes = (debit_code + credit_code).uniq
  codes = nil

  create_record(nil, date, codes) { |f| f.write journal2csv(d) }
end

.filter_by_code(start_year, start_month, end_year, end_month, code, recursive = true, basedir = @dirname) ⇒ Object

Load data based on account code.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/luca_book/journal.rb', line 168

def self.filter_by_code(start_year, start_month, end_year, end_month, code, recursive = true, basedir = @dirname)
  return enum_for(:filter_by_code, start_year, start_month, end_year, end_month, code, basedir) unless block_given?

  re = recursive ? "^#{code}" : "^#{code}$"
  LucaSupport::Code.encode_term(start_year, start_month, end_year, end_month).each do |subdir|
    open_records(basedir, subdir, nil, nil) do |f, path|
      CSV.new(f, headers: false, col_sep: "\t", encoding: 'UTF-8')
        .each.with_index(0) do |line, i|
        case i
        when 0
          if line.find { |cd| /#{re}/.match(cd) }
            f.rewind
            yield load_data(f, path), path
            break
          end
        when 2
          if line.find { |cd| /#{re}/.match(cd) }
            f.rewind
            yield load_data(f, path), path
          end
        when 3
          break
        else # skip
        end
      end
    end
  end
end

.journal2csv(d) ⇒ Object

Convert journal object to TSV format.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/luca_book/journal.rb', line 54

def self.journal2csv(d)
  debit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['debit'], 'amount'))
  credit_amount = LucaSupport::Code.decimalize(serialize_on_key(d['credit'], 'amount'))
  raise 'BalanceUnmatch' if debit_amount.inject(:+) != credit_amount.inject(:+)

  debit_code = serialize_on_key(d['debit'], 'code')
  credit_code = serialize_on_key(d['credit'], 'code')

  csv = CSV.generate(String.new, col_sep: "\t", headers: false) do |f|
    f << debit_code
    f << LucaSupport::Code.readable(debit_amount)
    f << credit_code
    f << LucaSupport::Code.readable(credit_amount)
    ACCEPTED_HEADERS.each do |x_header|
      f << [x_header, d['headers'][x_header]] if d.dig('headers', x_header)
    end
    f << []
    f << [d.dig('note')]
  end
end

.load_data(io, path) ⇒ Object

override de-serializing journal format. Sample format is:

{
  id: '2021A/V001',
  headers: {
    'x-customer' => 'Some Customer Co.'
  },
  debit: [
    { code: 'A12', amount: 1000 }
  ],
  credit: [
    { code: '311', amount: 1000 }
  ],
  note: 'note for each journal'
}


132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/luca_book/journal.rb', line 132

def self.load_data(io, path)
  {}.tap do |record|
    body = false
    record[:id] = "#{path[0]}/#{path[1]}"
    CSV.new(io, headers: false, col_sep: "\t", encoding: 'UTF-8')
      .each.with_index(0) do |line, i|
      case i
      when 0
        record[:debit] = line.map { |row| { code: row } }
      when 1
        line.each_with_index { |amount, j| record[:debit][j][:amount] = BigDecimal(amount.to_s) }
      when 2
        record[:credit] = line.map { |row| { code: row } }
      when 3
        line.each_with_index { |amount, j| record[:credit][j][:amount] = BigDecimal(amount.to_s) }
      else
        case body
        when false
          if line.empty?
            record[:note] ||= []
            body = true
          else
            record[:headers] ||= {}
            record[:headers][line[0]] = line[1]
          end
        when true
          record[:note] << line.join(' ') if body
        end
      end
    end
    record[:note] = record[:note]&.join('\n')
  end
end

.save(dat) ⇒ Object

update journal with hash. If record not found with id, no record will be created.



40
41
42
43
44
45
46
47
48
49
50
# File 'lib/luca_book/journal.rb', line 40

def self.save(dat)
  d = LucaSupport::Code.keys_stringify(dat)
  raise 'record has no id.' if d['id'].nil?

  validate(d)
  parts = d['id'].split('/')
  raise 'invalid ID' if parts.length != 2

  codes = nil
  open_records(@dirname, parts[0], parts[1], codes, 'w') { |f, _path| f.write journal2csv(d) }
end

.serialize_on_key(array_of_hash, key) ⇒ Object

collect values on specified key



112
113
114
# File 'lib/luca_book/journal.rb', line 112

def self.serialize_on_key(array_of_hash, key)
  array_of_hash.map { |h| h[key] }
end

.update_codes(obj) ⇒ Object



88
89
90
91
92
93
# File 'lib/luca_book/journal.rb', line 88

def self.update_codes(obj)
  debit_code = serialize_on_key(obj[:debit], :code)
  credit_code = serialize_on_key(obj[:credit], :code)
  codes = (debit_code + credit_code).uniq.sort.compact
  change_codes(obj[:id], codes)
end

.validate(obj) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/luca_book/journal.rb', line 95

def self.validate(obj)
  raise 'NoDebitKey' unless obj.key?('debit')
  raise 'NoCreditKey' unless obj.key?('credit')
  debit_codes = serialize_on_key(obj['debit'], 'code').compact
  debit_amount = serialize_on_key(obj['debit'], 'amount').compact
  raise 'NoDebitCode' if debit_codes.empty?
  raise 'NoDebitAmount' if debit_amount.empty?
  raise 'UnmatchDebit' if debit_codes.length != debit_amount.length
  credit_codes = serialize_on_key(obj['credit'], 'code').compact
  credit_amount = serialize_on_key(obj['credit'], 'amount').compact
  raise 'NoCreditCode' if credit_codes.empty?
  raise 'NoCreditAmount' if credit_amount.empty?
  raise 'UnmatchCredit' if credit_codes.length != credit_amount.length
end