Class: Voucher

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/voucher.rb

Overview

== Schema Information

Table name: vouchers
Database name: primary

id :integer not null, primary key
category :string(255)
currency :string(255)
exchange_rate :float
gl_date :date
invoice_date :date
invoice_number :string(255)
payment_terms :string(255)
reference_number :string(255) not null
reversal_date :date
state :string(255)
created_at :datetime
updated_at :datetime
business_unit_id :integer
company_id :integer
creator_id :integer
order_id :integer
supplier_id :integer
updater_id :integer

Indexes

index_vouchers_on_business_unit_id (business_unit_id)
index_vouchers_on_category (category)
index_vouchers_on_company_id (company_id)
index_vouchers_on_currency (currency)
index_vouchers_on_gl_date (gl_date)
index_vouchers_on_invoice_date (invoice_date)
index_vouchers_on_invoice_number (invoice_number)
index_vouchers_on_order_id (order_id)
index_vouchers_on_payment_terms (payment_terms)
index_vouchers_on_reference_number (reference_number)
index_vouchers_on_state (state)
supplier_id_invoice_number (supplier_id,invoice_number)

Constant Summary collapse

CATEGORIES =

Categories.

%w[voucher debit_memo commission].freeze
REFERENCE_NUMBER_PATTERN =

Regex pattern matching reference number.

/^PV(\d+)$/i

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#business_unit_idObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

#categoryObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

#company_idObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

#currencyObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

#gl_dateObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

#invoice_dateObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

#invoice_numberObject (readonly)



68
# File 'app/models/voucher.rb', line 68

validates :invoice_number, uniqueness: { scope: :supplier_id, message: 'is already in use on another voucher for this supplier', unless: :voided? }

#order_idObject (readonly)



62
# File 'app/models/voucher.rb', line 62

validates :order_id, presence: { if: proc { |v| v.category == 'commission' } }

#payment_termsObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

#supplier_idObject (readonly)



61
# File 'app/models/voucher.rb', line 61

validates :category, :company_id, :supplier_id, :invoice_date, :gl_date, :currency, :business_unit_id, :payment_terms, presence: true

Class Method Details

.activeActiveRecord::Relation<Voucher>

A relation of Vouchers that are active. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Voucher>)

See Also:



87
# File 'app/models/voucher.rb', line 87

scope :active, -> { where.not(state: 'voided') }

.build_gl_entries_from_csv(file_path) ⇒ Array<Hash>

Parse a GL distribution CSV (used by the upload-CSV flow) into the
hash shape that #distribute_to_gl accepts. Resolves company,
account, project, and business unit by their natural keys.

Parameters:

  • file_path (String, Pathname)

    CSV with headers
    company, account, project, amount, business_unit, description

Returns:

  • (Array<Hash>)


394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'app/models/voucher.rb', line 394

def self.build_gl_entries_from_csv(file_path)
  ledger_entries = []
  CSV.foreach(file_path, headers: true) do |row|
     = LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['company'] },
                                                                                 ledger_accounts: { number: row['account'] }).first
    project = LedgerDetailProject.where(project_number: row['project']).first
    ledger_entries << {
      account_id: .try(:id),
      account_ref: .try(:identifier),
      project_id: project.try(:id),
      project_ref: project.try(:project_number),
      amount: row['amount'],
      business_unit_id: BusinessUnit.where(number: row['business_unit']).first.try(:id),
      remark: row['description']
    }
  end
  ledger_entries
end

.get_next_reference_numberString

Pull the next number from voucher_reference_numbers_seq and return
it as a string for the reference_number column. Class-level despite
the private block, but kept private by convention.

Returns:

  • (String)


420
421
422
423
# File 'app/models/voucher.rb', line 420

def self.get_next_reference_number
  seq = Voucher.find_by_sql("SELECT nextval('voucher_reference_numbers_seq') AS reference_number")
  seq[0].reference_number.to_s
end

.states_for_selectArray<Array(String, Symbol)>

[label, value] pairs of every workflow state for select inputs.

Returns:

  • (Array<Array(String, Symbol)>)


148
149
150
# File 'app/models/voucher.rb', line 148

def self.states_for_select
  Voucher.state_machines[:state].states.map { |s| [s.human_name.titleize, s.name] }.sort
end

.voucher_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer

Count vouchers, optionally scoped to a company and arbitrary
where/where-not conditions. Used by AP dashboard counters.

Parameters:

  • company_id (Integer, nil) (defaults to: nil)
  • where_conditions (Hash, String, nil) (defaults to: nil)
  • where_not_conditions (Hash, String, nil) (defaults to: nil)

Returns:

  • (Integer)


137
138
139
140
141
142
143
# File 'app/models/voucher.rb', line 137

def self.voucher_count(company_id = nil, where_conditions = nil, where_not_conditions = nil)
  v = Voucher.order(:id)
  v = v.where(company_id: company_id) unless company_id.nil?
  v = v.where(where_conditions) unless where_conditions.nil?
  v = v.where.not(where_not_conditions) unless where_not_conditions.nil?
  v.count
end

.with_lazy_loadsActiveRecord::Relation<Voucher>

A relation of Vouchers that are with lazy loads. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Voucher>)

See Also:



83
84
85
86
# File 'app/models/voucher.rb', line 83

scope :with_lazy_loads, -> {
  includes({ voucher_items: :tax_rate }, :business_unit, :company, :supplier,
           { ledger_transactions: [:company, { ledger_entries: { ledger_company_account: %i[company ledger_detail_account] } }] })
}

Instance Method Details

#all_items_fully_paid?Boolean

Returns:

  • (Boolean)


195
196
197
198
# File 'app/models/voucher.rb', line 195

def all_items_fully_paid?
  # force reload of the voucher items as sometimes it's referring to already loaded stale objects
  voucher_items.reload.all?(&:fully_paid?)
end

#all_payments_voided?Boolean

Returns:

  • (Boolean)


200
201
202
# File 'app/models/voucher.rb', line 200

def all_payments_voided?
  outgoing_payment_items.reload.all?(&:voided?)
end

#amount_to_distributeBigDecimal

Net amount AP needs to spread across GL offset accounts. Subtracts
included VAT (V), adds use tax (U), and treats other categories
at gross — the inverse of #distribute_to_gl's tax-account splits.

Returns:

  • (BigDecimal)


226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'app/models/voucher.rb', line 226

def amount_to_distribute
  amount = BigDecimal(0)
  voucher_items.each do |vi|
    amount += case vi.tax_type
              when 'V'
                (vi.gross_amount - vi.tax_amount)
              when 'U'
                (vi.gross_amount + vi.tax_amount)
              else
                vi.gross_amount
              end
  end
  amount
end

#balanceBigDecimal

Outstanding amount still owed to the #supplier. Voided vouchers
always return zero.

Returns:

  • (BigDecimal)


181
182
183
184
185
# File 'app/models/voucher.rb', line 181

def balance
  return 0 if voided?

  total - voucher_items.all.to_a.sum { |vi| vi.outgoing_payment_items.applied.sum(:amount) }
end

#business_unitBusinessUnit



53
# File 'app/models/voucher.rb', line 53

belongs_to :business_unit, optional: true

#can_be_voided?Boolean

Returns:

  • (Boolean)


204
205
206
# File 'app/models/voucher.rb', line 204

def can_be_voided?
  unpaid? or paid?
end

#companyCompany

Returns:

See Also:



51
# File 'app/models/voucher.rb', line 51

belongs_to :company, optional: true

#currency_symbolString

Returns symbol for the voucher's transaction currency.

Returns:

  • (String)

    symbol for the voucher's transaction currency



383
384
385
# File 'app/models/voucher.rb', line 383

def currency_symbol
  Money::Currency.new(currency).symbol
end

#distribute_to_gl(_temporary_account_id = nil, ledger_entries = []) ⇒ Object

Post the voucher to the GL: build a VOUCHER ledger transaction
crediting the supplier's offset account (or AP catch-all),
accumulating per-line offset accounts, and splitting tax into the
use/output-tax sub-accounts. Extra distribution lines from the AP
GL form are appended via ledger_entries. Advances state via
gl_completed! and runs intercompany project tagging.

Parameters:

  • _temporary_account_id (Integer, nil) (defaults to: nil)

    unused, retained for legacy callers

  • ledger_entries (Array<Hash>) (defaults to: [])

    additional GL lines from the
    distribute-to-GL form (account_id, amount, remark,
    project_id, business_unit_id)

Raises:

  • (RuntimeError)

    when offset/tax accounts cannot be resolved



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'app/models/voucher.rb', line 253

def distribute_to_gl( = nil, ledger_entries = [])
  transaction do
    case category
    when 'voucher_logging'

      # trade_accounts_payable_company_account = LedgerCompanyAccount.for_company_and_account(company.id, TRADE_ACCOUNTS_PAYABLE_ACCOUNT)
      # temporary_company_account = LedgerCompanyAccount.find(temporary_account_id)
      #
      # raise "Unable to find all necessary accounts to post to" if trade_accounts_payable_company_account.nil? or temporary_company_account.nil?
      #
      # ap_amount = -total
      # temp_amount = BigDecimal("0")
      # tax_accounts = {}
      #
      # voucher_items.each do |vi|
      #   case vi.tax_type
      #   when "N/A"
      #     temp_amount += vi.gross_amount
      #   when "S"
      #     temp_amount += vi.gross_amount
      #   when "U"
      #     tax_account = LedgerCompanyAccount.where(:company_id => company.id, :ledger_detail_account_id => vi.tax_rate.use_tax_account_id).first
      #     raise "Cannot find use tax account for tax rate id: #{vi.tax_rate_id}" if tax_account.nil?
      #     tax_accounts[tax_account.id] = tax_accounts[tax_account.id].nil? ? -vi.tax_amount : (tax_accounts[tax_account.id] += -vi.tax_amount)
      #     temp_amount += (vi.gross_amount + vi.tax_amount)
      #   when "V"
      #     tax_account = LedgerCompanyAccount.where(:company_id => company.id, :ledger_detail_account_id => vi.tax_rate.sales_tax_credit_account_id).first
      #     raise "Cannot find output tax account for tax rate id: #{vi.tax_rate_id}" if tax_account.nil?
      #     tax_accounts[tax_account.id] = tax_accounts[tax_account.id].nil? ? vi.tax_amount : (tax_accounts[tax_account.id] += vi.tax_amount)
      #     # add the (gross_amount - tax_amount) to be distributed
      #     # for tax only lines this would be 0
      #     temp_amount += vi.gross_amount - vi.tax_amount
      #   end
      # end
      #
      # transaction = LedgerTransaction.new(:company => company, :transaction_type => "VOUCHER", :transaction_date => gl_date, :currency => currency, :voucher => self, :exchange_rate => exchange_rate)
      # transaction.ledger_entries << LedgerEntry.new(:ledger_company_account => trade_accounts_payable_company_account, :currency => currency, :amount => ap_amount)
      # transaction.ledger_entries << LedgerEntry.new(:ledger_company_account => temporary_company_account, :currency => currency, :amount => temp_amount)
      # tax_accounts.each do |account_id, amount|
      #   transaction.ledger_entries << LedgerEntry.new(:ledger_company_account_id => account_id, :currency => currency, :amount => amount)
      # end
      #
      # transaction.save!
      #
      # # need to

    when 'voucher', 'debit_memo', 'commission'
       = if supplier..present?
                                 LedgerCompanyAccount.(company.id, supplier..number)
                               else
                                 LedgerCompanyAccount.(company.id, TRADE_ACCOUNTS_PAYABLE_ACCOUNT)
                               end

      raise 'Unable to find all necessary accounts to post to' if .nil?

      accounts = {  => { amount: 0, business_unit: nil } }
      tax_accounts = {}

      voucher_items.each do |vi|
        case vi.tax_type
        when 'U'
           = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: vi.tax_rate.).first
          raise "Cannot find use tax account for tax rate id: #{vi.tax_rate_id}" if .nil?

          tax_accounts[.id] = tax_accounts[.id].nil? ? -vi.tax_amount : (tax_accounts[.id] += -vi.tax_amount)
        when 'V'
           = LedgerCompanyAccount.where(company_id: company.id,
                                                   ledger_detail_account_id: vi.tax_rate.).first
          raise "Cannot find output tax account for tax rate id: #{vi.tax_rate_id}" if .nil?

          tax_accounts[.id] = tax_accounts[.id].nil? ? vi.tax_amount : (tax_accounts[.id] += vi.tax_amount)
        end
        if vi..present?
          accounts[vi.] ||= { amount: 0, business_unit: vi.business_unit }
          accounts[vi.][:amount] += -vi.gross_amount
        else
          accounts[][:amount] += -vi.gross_amount
        end
      end

      transaction = LedgerTransaction.new(company: company, transaction_type: 'VOUCHER', transaction_date: gl_date, currency: currency,
                                          voucher: self, exchange_rate: exchange_rate)
      accounts.each do |, details|
        unless details[:amount].zero?
          transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: currency, amount: details[:amount],
                                                        business_unit: details[:business_unit])
        end
      end
      tax_accounts.each do |, amount|
        transaction.ledger_entries << LedgerEntry.new(ledger_company_account_id: , currency: currency, amount: amount)
      end

      ledger_entries.each do |le|
        transaction.ledger_entries << LedgerEntry.new(ledger_company_account_id: le['account_id'], currency: currency,
                                                      amount: le['amount'], description: le['remark'], ledger_detail_project_id: le['project_id'], business_unit_id: le['business_unit_id'])
      end
      transaction.save!
      intercompany_posting_assign_ledger_project
      gl_completed!
    end
  end
end

#editing_locked?Boolean

Returns:

  • (Boolean)


208
209
210
# File 'app/models/voucher.rb', line 208

def editing_locked?
  !draft? or voided?
end

#intercompany_posting_assign_ledger_projectObject

When the GL transaction includes two intercompany-transfer entries,
tag each side with the other company's ledger project so reporting
can pair the postings. No-op for non-intercompany vouchers.



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'app/models/voucher.rb', line 359

def intercompany_posting_assign_ledger_project
  itercompany_postings_entries = []
  ledger_transactions.first.ledger_entries.each do |le|
    if le.. && le...number == INTERCOMPANY_TRANSFERS_ACCOUNT
      itercompany_postings_entries << { ledger_company_account_id: le.,
company_id: le..company_id }
    end
  end
  return unless itercompany_postings_entries.length > 1

  ledger_transactions.first.ledger_entries.each do |le|
    le.update(ledger_detail_project_id: Company.find(itercompany_postings_entries.last[:company_id]).ledger_project_id) if le. == itercompany_postings_entries.first[:ledger_company_account_id]
    le.update(ledger_detail_project_id: Company.find(itercompany_postings_entries.first[:company_id]).ledger_project_id) if le. == itercompany_postings_entries.last[:ledger_company_account_id]
  end
end

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

Returns:

See Also:



57
# File 'app/models/voucher.rb', line 57

has_many :ledger_transactions

#orderOrder

Returns:

See Also:



54
# File 'app/models/voucher.rb', line 54

belongs_to :order, optional: true

#order_refObject



187
188
189
# File 'app/models/voucher.rb', line 187

def order_ref
  order.try(:reference_number)
end

#order_ref=(ref) ⇒ Object



191
192
193
# File 'app/models/voucher.rb', line 191

def order_ref=(ref)
  self.order = Order.find_by(reference_number: ref) if ref.present?
end

#outgoing_payment_itemsActiveRecord::Relation<OutgoingPaymentItem>

Returns:

See Also:



58
# File 'app/models/voucher.rb', line 58

has_many :outgoing_payment_items, through: :voucher_items

#reverse(date) ⇒ Object

Reverse this voucher and any outgoing payments that touched it.
Stamps reversal_date on self and on each related OutgoingPayment
before transitioning everything to voided. Wrapped in a transaction
so a failed payment void rolls back the voucher reversal.

Parameters:

  • date (Date)

    GL date to post the reversal under

Raises:

  • (RuntimeError)

    when date is nil



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'app/models/voucher.rb', line 159

def reverse(date)
  raise 'Reversal date required' if date.nil?

  Voucher.transaction do
    self.reversal_date = date
    save!
    payments = outgoing_payment_items.map(&:payment).uniq
    payments.each do |p|
      p.reversal_date = reversal_date
      p.state_event = 'void'
      p.save!
    end
    reload
    self.state_event = 'void'
    save!
  end
end

#supplierParty

Returns:

See Also:



52
# File 'app/models/voucher.rb', line 52

belongs_to :supplier, class_name: 'Party', inverse_of: :vouchers, optional: true

#supplier_nameString?

Returns full name of the vendor Party listed on the voucher.

Returns:

  • (String, nil)

    full name of the vendor Party listed on the voucher



213
214
215
# File 'app/models/voucher.rb', line 213

def supplier_name
  supplier.try(:full_name)
end

#supplier_typeObject



217
218
219
# File 'app/models/voucher.rb', line 217

def supplier_type
  supplier.try(:class).try(:to_s)
end

#to_sString

Returns "Voucher #PV1234".

Returns:

  • (String)

    "Voucher #PV1234"



126
127
128
# File 'app/models/voucher.rb', line 126

def to_s
  "Voucher ##{reference_number}"
end

#totalBigDecimal

Sum of every line's gross amount — the vendor invoice face value.

Returns:

  • (BigDecimal)


378
379
380
# File 'app/models/voucher.rb', line 378

def total
  voucher_items.sum(:gross_amount)
end

#uploadsActiveRecord::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



59
# File 'app/models/voucher.rb', line 59

has_many :uploads, as: :resource, dependent: :destroy

#voucher_itemsActiveRecord::Relation<VoucherItem>

Returns:

See Also:



56
# File 'app/models/voucher.rb', line 56

has_many :voucher_items, inverse_of: :voucher, dependent: :destroy