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 Models::Schedulable

Models::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 Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#amountObject (readonly)

Validates presence of the core payment fields.

Validations:



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

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

#bank_account_idObject (readonly)

Validates presence of the core payment fields.

Validations:



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

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

#categoryObject (readonly)

Validates presence of the core payment fields.

Validations:



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

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

#company_idObject (readonly)

Validates presence of the core payment fields.

Validations:



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

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.



123
124
125
# File 'app/models/outgoing_payment.rb', line 123

def credit_memo_id
  @credit_memo_id
end

#currencyObject (readonly)

Validates presence of the core payment fields.

Validations:



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

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

#payment_dateObject (readonly)

Validates presence of the core payment fields.

Validations:



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

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

#supplier_idObject (readonly)

Validates presence of the core payment fields.

Validations:



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

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:



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

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:



379
380
381
382
383
384
385
# File 'app/models/outgoing_payment.rb', line 379

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)>)


241
242
243
# File 'app/models/outgoing_payment.rb', line 241

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)


464
465
466
467
# File 'app/models/outgoing_payment.rb', line 464

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)


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

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



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

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.



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

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.



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

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).



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

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)>)


233
234
235
# File 'app/models/outgoing_payment.rb', line 233

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

Instance Method Details

#addressAddress

Address a check is mailed to.

Returns:

See Also:



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

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

#approved_byEmployee

Employee who approved the payment.

Returns:

See Also:



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

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

#background_jobBackgroundJobStatus?

Returns in-flight check generation job, if any.

Returns:



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

def background_job
  return unless job_id

  BackgroundJobStatus.find(job_id)
end

#bank_accountBankAccount

Bank account the payment is drawn from.



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

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)



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

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)


193
194
195
# File 'app/models/outgoing_payment.rb', line 193

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

#checksActiveRecord::Relation<Check>

Printed checks for this payment.

Returns:

  • (ActiveRecord::Relation<Check>)

See Also:



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

has_many :checks

#companyCompany

Company making the payment.

Returns:

See Also:



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

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)


306
307
308
309
310
# File 'app/models/outgoing_payment.rb', line 306

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

#editing_locked?Boolean

Whether the payment is locked from editing (no longer a draft).

Returns:

  • (Boolean)


298
299
300
# File 'app/models/outgoing_payment.rb', line 298

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.



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

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:



358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'app/models/outgoing_payment.rb', line 358

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?

Whether the payment has a background job currently running.

Returns:

  • (Boolean, nil)


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

def has_active_background_job?
  background_job&.active?
end

#ledger_entriesActiveRecord::Relation<LedgerEntry>

GL entries posted for this payment.

Returns:

See Also:



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

has_many :ledger_entries, through: :ledger_transactions

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

GL transactions posted for this payment.

Returns:

See Also:



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

has_many :ledger_transactions

#outgoing_payment_itemsActiveRecord::Relation<OutgoingPaymentItem>

Individual voucher/payment applications making up this payment.

Returns:

See Also:



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

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:



274
275
276
# File 'app/models/outgoing_payment.rb', line 274

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

#payment_items_applied?Boolean

Whether the payment items sum exactly to the payment amount.

Returns:

  • (Boolean)


292
293
294
# File 'app/models/outgoing_payment.rb', line 292

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



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'app/models/outgoing_payment.rb', line 395

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



448
449
450
451
452
453
454
455
456
# File 'app/models/outgoing_payment.rb', line 448

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

Supplier (party) being paid.

Returns:

See Also:



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

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



313
314
315
# File 'app/models/outgoing_payment.rb', line 313

def supplier_name
  supplier.try(:full_name)
end

#supplier_typeString?

Class name of the supplier party (e.g. 'Supplier').

Returns:

  • (String, nil)


286
287
288
# File 'app/models/outgoing_payment.rb', line 286

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

#to_sString

Returns "OutgoingPayment #PVnnn".

Returns:

  • (String)

    "OutgoingPayment #PVnnn"



186
187
188
# File 'app/models/outgoing_payment.rb', line 186

def to_s
  "OutgoingPayment ##{reference_number}"
end