Class: OutgoingPayment

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

Overview

== Schema Information

Table name: outgoing_payments
Database name: primary

id :integer not null, primary key
amount :decimal(10, 2)
approved_at :datetime
category :string(255)
check_state :string(255)
currency :string(255)
exchange_rate :float
payment_date :date
print_reminder_sent :boolean default(FALSE)
reference_number :string(255) not null
remark :text
reversal_date :date
review_request_sent :boolean default(FALSE)
state :string(255)
created_at :datetime
updated_at :datetime
approved_by_id :integer
bank_account_id :integer
company_id :integer
creator_id :integer
job_id :string(255)
mailing_address_id :integer
supplier_id :integer
updater_id :integer

Indexes

category_state (category,state)
index_outgoing_payments_on_amount (amount)
index_outgoing_payments_on_bank_account_id (bank_account_id)
index_outgoing_payments_on_check_state (check_state)
index_outgoing_payments_on_company_id (company_id)
index_outgoing_payments_on_currency (currency)
index_outgoing_payments_on_job_id (job_id)
index_outgoing_payments_on_mailing_address_id (mailing_address_id)
index_outgoing_payments_on_payment_date (payment_date)
index_outgoing_payments_on_reference_number (reference_number)
index_outgoing_payments_on_state (state)
index_outgoing_payments_on_supplier_id (supplier_id)

Constant Summary collapse

CATEGORIES =

Categories.

%w[ach cash check debit_card e_billpay non_cash paypal wire_transfer].freeze
DEFAULT_BANK_ACCOUNTS =

default bank account per country and payment method, using => instead of : to force it to use strings for the keys

{
  1 => {
    'ach' => 1,
    'check' => 1,
    'debit_card' => 1,
    'e_billpay' => 1,
    'paypal' => 7,
    'wire_transfer' => 1
  },
  2 => {
    'ach' => 2,
    'check' => 2,
    'debit_card' => 2,
    'e_billpay' => 2,
    'paypal' => 8,
    'wire_transfer' => 2
  },
  3 => {},
  4 => {}
}.freeze

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

#amountObject (readonly)



88
# File 'app/models/outgoing_payment.rb', line 88

validates :company_id, :supplier_id, :bank_account_id, :category, :amount, :payment_date, :currency, presence: true

#bank_account_idObject (readonly)



88
# File 'app/models/outgoing_payment.rb', line 88

validates :company_id, :supplier_id, :bank_account_id, :category, :amount, :payment_date, :currency, presence: true

#categoryObject (readonly)



88
# File 'app/models/outgoing_payment.rb', line 88

validates :company_id, :supplier_id, :bank_account_id, :category, :amount, :payment_date, :currency, presence: true

#company_idObject (readonly)



88
# File 'app/models/outgoing_payment.rb', line 88

validates :company_id, :supplier_id, :bank_account_id, :category, :amount, :payment_date, :currency, presence: true

#credit_memo_idObject

Returns the value of attribute credit_memo_id.



113
114
115
# File 'app/models/outgoing_payment.rb', line 113

def credit_memo_id
  @credit_memo_id
end

#currencyObject (readonly)



88
# File 'app/models/outgoing_payment.rb', line 88

validates :company_id, :supplier_id, :bank_account_id, :category, :amount, :payment_date, :currency, presence: true

#payment_dateObject (readonly)



88
# File 'app/models/outgoing_payment.rb', line 88

validates :company_id, :supplier_id, :bank_account_id, :category, :amount, :payment_date, :currency, presence: true

#supplier_idObject (readonly)



88
# File 'app/models/outgoing_payment.rb', line 88

validates :company_id, :supplier_id, :bank_account_id, :category, :amount, :payment_date, :currency, presence: true

Class Method Details

.appliedActiveRecord::Relation<OutgoingPayment>

A relation of OutgoingPayments that are applied. Active Record Scope

Returns:

See Also:



111
# File 'app/models/outgoing_payment.rb', line 111

scope :applied, -> { where(state: 'applied') }

.billing_address(payee, outgoing_payment_items) ⇒ Address?

Resolve the cheque mailing address: SPIFF enrollment override
first (commission programs may direct payouts elsewhere), then the
payee's default billing address.

Parameters:

Returns:



361
362
363
364
365
366
367
# File 'app/models/outgoing_payment.rb', line 361

def self.billing_address(payee, outgoing_payment_items)
  billing_address = outgoing_payment_items&.first&.spiff_enrollment&.mailing_address

  billing_address ||= payee&.billing_address

  billing_address
end

.check_states_for_selectArray<Array(String, Symbol)>

Hard-coded label/value pairs of each check_state. Useful in filter
dropdowns even though the state machine knows the same thing.

Returns:

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


231
232
233
# File 'app/models/outgoing_payment.rb', line 231

def self.check_states_for_select
  [['No Check', :no_check], ['Queued', :queued], ['Printed', :printed], ['Reprinted', :reprinted], ['Generated', :generated]]
end

.get_next_reference_numberString

Pull the next number from payment_reference_numbers_seq for the
reference_number column.

Returns:

  • (String)


446
447
448
449
# File 'app/models/outgoing_payment.rb', line 446

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

.payment_count(company_id = nil, bank_account_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer

Count payments, optionally narrowed by company/bank account and
arbitrary where/where-not predicates. Used by AP dashboard counters.

Parameters:

  • company_id (Integer, nil) (defaults to: nil)
  • bank_account_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)


243
244
245
246
247
248
249
250
# File 'app/models/outgoing_payment.rb', line 243

def self.payment_count(company_id = nil,  = nil, where_conditions = nil, where_not_conditions = nil)
  p = OutgoingPayment.order(:id)
  p = p.where(company_id:) unless company_id.nil?
  p = p.where(bank_account_id:) unless .nil?
  p = p.where(where_conditions) unless where_conditions.nil?
  p = p.where.not(where_not_conditions) unless where_not_conditions.nil?
  p.count
end

Bulk-print a batch of payments and combine each cheque PDF into one
printable file. Used by the AP "print all checks" action.

Parameters:

  • payment_ids (Array<Integer>)
  • current_user (Employee)

Returns:

  • (String)

    path to the combined PDF



413
414
415
416
417
418
419
420
421
422
# File 'app/models/outgoing_payment.rb', line 413

def self.print_all_checks(payment_ids, current_user)
  OutgoingPayment.transaction do
    pdfs = []
    payment_ids.each do |payment_id|
      payment = OutgoingPayment.find(payment_id)
      pdfs << payment.print_check(current_user)
    end
    return PdfTools.combine(pdfs, output_file_path: Upload.temp_location("combined_check_printout_#{Time.current.to_fs(:no_spaces)}.pdf"))
  end
end

.send_checks_pending_review_notificationObject

Mailer hook: alert AP about cheques sitting in pending_review. Only
fires once per payment via the review_request_sent flag.



189
190
191
192
193
194
195
196
# File 'app/models/outgoing_payment.rb', line 189

def self.send_checks_pending_review_notification
  payments = where(category: 'check', state: 'applied', check_state: 'pending_review', review_request_sent: false)
  if payments.any?
    FinancialsMailer.checks_pending_review_notification(payments).deliver
    payments.each { |payment| payment.update(review_request_sent: true) }
  end
  true
end

.send_checks_ready_to_print_notificationObject

Mailer hook: notify AP that approved cheques are queued and ready to
print. Single-shot via print_reminder_sent.



200
201
202
203
204
205
206
207
# File 'app/models/outgoing_payment.rb', line 200

def self.send_checks_ready_to_print_notification
  payments = where(category: 'check', state: 'applied', check_state: 'queued', print_reminder_sent: false)
  if payments.any?
    FinancialsMailer.checks_ready_to_print_notification(payments).deliver
    payments.each { |payment| payment.update(print_reminder_sent: true) }
  end
  true
end

.send_daily_unprinted_checks_digestObject

Daily digest of cheques in queued or generated that have no
background-job in flight (so a stalled batch doesn't get double
counted).



212
213
214
215
216
217
# File 'app/models/outgoing_payment.rb', line 212

def self.send_daily_unprinted_checks_digest
  payments = applied.where(category: 'check', check_state: %w[queued generated], job_id: nil)
                    .includes(:company, :supplier, :outgoing_payment_items)
  FinancialsMailer.daily_unprinted_checks_digest(payments.to_a).deliver if payments.any?
  true
end

.states_for_selectArray<Array(String, Symbol)>

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

Returns:

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


223
224
225
# File 'app/models/outgoing_payment.rb', line 223

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

Instance Method Details

#addressAddress

Returns:

See Also:



80
# File 'app/models/outgoing_payment.rb', line 80

belongs_to :address, foreign_key: :mailing_address_id, primary_key: :id, optional: true

#approved_byEmployee

Returns:

See Also:



79
# File 'app/models/outgoing_payment.rb', line 79

belongs_to :approved_by, class_name: 'Employee', optional: true

#background_jobBackgroundJobStatus?

Returns in-flight check generation job, if any.

Returns:



253
254
255
256
257
# File 'app/models/outgoing_payment.rb', line 253

def background_job
  return unless job_id

  BackgroundJobStatus.find(job_id)
end

#bank_accountBankAccount



78
# File 'app/models/outgoing_payment.rb', line 78

belongs_to :bank_account, optional: true

#build_outgoing_payment_items(voucher_items = {}, credit_memos = {}, receipts = {}) ⇒ Object

Replace this payment's line items with one row per voucher/credit
memo/receipt the operator chose to settle. Each value hash must
carry an 'amount_to_pay' string.

Parameters:

  • voucher_items (Hash{Integer => Hash}) (defaults to: {})

    keyed by VoucherItem id

  • credit_memos (Hash{Integer => Hash}) (defaults to: {})

    keyed by CreditMemo id

  • receipts (Hash{Integer => Hash}) (defaults to: {})

    keyed by Receipt id (refunding unapplied funds)



306
307
308
309
310
311
312
313
314
315
316
317
# File 'app/models/outgoing_payment.rb', line 306

def build_outgoing_payment_items(voucher_items = {}, credit_memos = {}, receipts = {})
  outgoing_payment_items.destroy_all
  voucher_items.each do |voucher_item_id, attrs|
    outgoing_payment_items.build(voucher_item_id:, amount: attrs['amount_to_pay'])
  end
  credit_memos.each do |credit_memo_id, attrs|
    outgoing_payment_items.build(credit_memo_id:, amount: attrs['amount_to_pay'])
  end
  receipts.each do |receipt_id, attrs|
    outgoing_payment_items.build(receipt_id:, amount: attrs['amount_to_pay'])
  end
end

#can_print_check?Boolean

Whether AP is allowed to (re)print this physical cheque now.

Returns:

  • (Boolean)


183
184
185
# File 'app/models/outgoing_payment.rb', line 183

def can_print_check?
  category == 'check' and !voided? and !pending_review?
end

#checksActiveRecord::Relation<Check>

Returns:

  • (ActiveRecord::Relation<Check>)

See Also:



84
# File 'app/models/outgoing_payment.rb', line 84

has_many :checks

#companyCompany

Returns:

See Also:



76
# File 'app/models/outgoing_payment.rb', line 76

belongs_to :company, optional: true

#currency_symbolString

Currency symbol for display, defaulting to USD when no currency is
set yet and falling back to $ for unrecognized codes.

Returns:

  • (String)


288
289
290
291
292
# File 'app/models/outgoing_payment.rb', line 288

def currency_symbol
  Money::Currency.new(currency || 'USD').symbol
rescue Money::Currency::UnknownCurrency
  '$'
end

#editing_locked?Boolean

Returns:

  • (Boolean)


280
281
282
# File 'app/models/outgoing_payment.rb', line 280

def editing_locked?
  !draft?
end

#generate_checkObject

Build one Check per distinct payee on the payment, allocate a new
cheque number from the BankAccount sequence, and render the PDF.
Wrapped in a transaction so a numbering or PDF failure rolls back.



322
323
324
325
326
327
328
329
330
331
332
# File 'app/models/outgoing_payment.rb', line 322

def generate_check
  OutgoingPayment.transaction do
    outgoing_payment_items.group_by(&:payee).each do |payee, outgoing_payment_items|
      check_number = .get_next_check_number
      address = self.address.nil? ? OutgoingPayment.billing_address(payee, outgoing_payment_items).try(:full_address, false, ', ') : self.address.try(:full_address, false, ', ')
      check = Check.create!(outgoing_payment: self, payee: payee.full_name, check_number:, bank_account:, address:, date: payment_date, amount: outgoing_payment_items.sum(&:amount))
      generate_check_pdf(check)
    end
    trigger_check_generated
  end
end

#generate_check_pdf(check) ⇒ Upload

Render the cheque PDF via Pdf::Document::Check and attach it as
the cheque's category: 'check' upload, replacing any prior PDF
(e.g. on regeneration after a corrected mailing address).

Parameters:

Returns:



340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'app/models/outgoing_payment.rb', line 340

def generate_check_pdf(check)
  result = Pdf::Document::Check.new(check:, outgoing_payment: self).call
  path = Upload.temp_location(check.file_name)
  File.open(path, 'wb') do |file|
    file.write result.pdf
    file.flush
    file.fsync
  end
  check.uploads.destroy_by(category: 'check')
  upload = Upload.uploadify(path, 'check', check, check.file_name)
  check.uploads.reload
  upload
end

#has_active_background_job?Boolean

Returns:

  • (Boolean)


268
269
270
# File 'app/models/outgoing_payment.rb', line 268

def has_active_background_job?
  background_job&.active?
end

#ledger_entriesActiveRecord::Relation<LedgerEntry>

Returns:

See Also:



83
# File 'app/models/outgoing_payment.rb', line 83

has_many :ledger_entries, through: :ledger_transactions

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

Returns:

See Also:



82
# File 'app/models/outgoing_payment.rb', line 82

has_many :ledger_transactions

#outgoing_payment_itemsActiveRecord::Relation<OutgoingPaymentItem>

Returns:

See Also:



81
# File 'app/models/outgoing_payment.rb', line 81

has_many :outgoing_payment_items, inverse_of: :outgoing_payment, dependent: :destroy

#payeeCustomer, ...

The payee on the first OutgoingPaymentItem. Cheques are restricted
to a single payee by #only_for_one_payee, so this also represents
the cheque payee.

Returns:



264
265
266
# File 'app/models/outgoing_payment.rb', line 264

def payee
  outgoing_payment_items.empty? ? nil : outgoing_payment_items.first.payee
end

#payment_items_applied?Boolean

Returns:

  • (Boolean)


276
277
278
# File 'app/models/outgoing_payment.rb', line 276

def payment_items_applied?
  outgoing_payment_items.sum(:amount) == amount
end

Print (or reprint) every cheque on this payment, regenerating PDFs
if requested, recording the print event on each Check, advancing
the check_state machine, and combining multi-cheque PDFs into one
printable file.

Parameters:

  • current_user (Employee)

    who pressed Print (audited on the Check)

  • regenerate_pdf (Boolean) (defaults to: false)

Returns:

  • (Upload, String)

    the lone upload, or the path to the merged PDF



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'app/models/outgoing_payment.rb', line 377

def print_check(current_user, regenerate_pdf: false)
  OutgoingPayment.transaction do
    # generate the checks if we don't already have them
    if checks.empty?
      generate_check
    elsif regenerate_pdf
      checks.each do |check|
        Rails.logger.info "Regenerating PDF for outgoing_payment id #{id}, check id #{check.id}"
        generate_check_pdf(check)
      end
    end
    printed_checks = checks.reload # forcing a reload here, in case new checks were generated
    pdfs = printed_checks.map { |c| c.uploads.first }
    check_numbers = printed_checks.map(&:check_number)

    printed_checks.each { |c| c.record_print(current_user) }

    trigger_check_printed if printed_checks.any?

    # If more than one check generated, combine them
    if pdfs.length > 1
      # Combine
      check_temp_path = Upload.temp_location("check_#{check_numbers.join('_')}.pdf")
      PdfTools.combine(pdfs, output_file_path: check_temp_path)
    else
      pdfs.first
    end
  end
end

#reverse(date) ⇒ Object

Reverse the payment under a given GL date. Voids the payment, which
cascades through the state machine to also reverse the linked
ledger transaction and unlock the underlying voucher items.

Parameters:

  • date (Date)

Raises:

  • (RuntimeError)

    when date is nil



430
431
432
433
434
435
436
437
438
# File 'app/models/outgoing_payment.rb', line 430

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

  OutgoingPayment.transaction do
    self.reversal_date = date
    self.state_event = 'void'
    save!
  end
end

#supplierParty

Returns:

See Also:



77
# File 'app/models/outgoing_payment.rb', line 77

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

#supplier_nameString?

Returns full name of the vendor Party being paid.

Returns:

  • (String, nil)

    full name of the vendor Party being paid



295
296
297
# File 'app/models/outgoing_payment.rb', line 295

def supplier_name
  supplier.try(:full_name)
end

#supplier_typeObject



272
273
274
# File 'app/models/outgoing_payment.rb', line 272

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

#to_sString

Returns "OutgoingPayment #PVnnn".

Returns:

  • (String)

    "OutgoingPayment #PVnnn"



176
177
178
# File 'app/models/outgoing_payment.rb', line 176

def to_s
  "OutgoingPayment ##{reference_number}"
end