Class: LedgerTransaction

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

Overview

== Schema Information

Table name: ledger_transactions
Database name: primary

id :integer not null, primary key
currency :string(3) not null
description :string(255)
exchange_rate :float
transaction_date :date not null
transaction_number :integer not null
transaction_type :string(255) not null
created_at :datetime
updated_at :datetime
company_id :integer
creator_id :integer
credit_memo_id :integer
delivery_id :integer
invoice_id :integer
landed_cost_id :integer
outgoing_payment_id :integer
receipt_id :integer
reversed_transaction_id :integer
shipment_receipt_id :integer
supplier_id :integer
updater_id :integer
voucher_id :integer

Indexes

index_ledger_transactions_on_credit_memo_id (credit_memo_id)
index_ledger_transactions_on_delivery_id (delivery_id)
index_ledger_transactions_on_invoice_id (invoice_id)
index_ledger_transactions_on_landed_cost_id (landed_cost_id)
index_ledger_transactions_on_outgoing_payment_id (outgoing_payment_id)
index_ledger_transactions_on_receipt_id (receipt_id)
index_ledger_transactions_on_reversed_transaction_id (reversed_transaction_id)
index_ledger_transactions_on_shipment_receipt_id (shipment_receipt_id)
index_ledger_transactions_on_supplier_id (supplier_id)
index_ledger_transactions_on_transaction_date_and_id (transaction_date,id)
index_ledger_transactions_on_transaction_number (transaction_number)
index_ledger_transactions_on_voucher_id (voucher_id)

Constant Summary collapse

LOCKED_ATTRIBUTES =

Header-level attributes the Models::Auditable concern refuses
to update once a transaction is posted — protects the audit
trail. To change one of these, reverse the transaction and
re-post.

%w[transaction_number transaction_type transaction_date currency exchange_rate company_id
invoice_id receipt_id shipment_receipt_id landed_cost_id voucher_id credit_memo_id outgoing_payment_id reversed_transaction_id].freeze
TYPE_ABBREVIATIONS =

Short codes shown in the GL register and on document headers,
keyed by transaction_type. Used by #type_abbreviation
and the CSV/journal exports.

{ 'JOURNAL_ENTRY' => 'JE',
'ISSUE_TO_ACCOUNT' => 'ITA',
'INVENTORY_ADJUSTMENT' => 'IA',
'LOCATION_TRANSFER' => 'LT',
'ITEM_RECLASSIFICATION' => 'IR',
'SALES_ORDER' => 'SO',
'STORE_TRANSFER' => 'ST',
'RECEIPT' => 'RE',
'PO_RECEIPT' => 'POR',
'PO_LANDED_COST' => 'POLC',
'VOUCHER' => 'VCR',
'RMA_RECEIPT' => 'RMA',
'CREDIT_MEMO' => 'CM',
'MISC_INVOICE' => 'MI',
'PAYMENT' => 'PAY',
'PO_SERVICE_FULFILLMENT' => 'POSF',
'TOTAL_COST_ADJUSTMENT' => 'TCA',
'COGS_ADJUSTMENT' => 'COGS',
'CYCLE_COUNT' => 'CC',
'CONSIGNMENT_INVOICE' => 'CI' }.freeze
SUPPORTED_CSV_ENCODINGS =

Encoding pipelines accepted by the bank-statement CSV importer,
keyed by user-facing label. The value is the source/target spec
passed to CSV.foreach(..., encoding:)bom|utf-8 strips the
BOM written by Excel/Google Sheets.

{
  'UTF-8' => 'bom|utf-8',
  'Windows-1252' => 'windows-1252:utf-8',
  'ISO-8859-1' => 'iso-8859-1:utf-8'
}.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 one collapse

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

#currencyObject (readonly)



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

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

#do_not_set_transaction_numberObject

Returns the value of attribute do_not_set_transaction_number.



55
56
57
# File 'app/models/ledger_transaction.rb', line 55

def do_not_set_transaction_number
  @do_not_set_transaction_number
end

#overrideObject

Returns the value of attribute override.



55
56
57
# File 'app/models/ledger_transaction.rb', line 55

def override
  @override
end

#transaction_dateObject (readonly)



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

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

#transaction_numberObject (readonly)



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

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

#transaction_typeObject (readonly)



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

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

Class Method Details

.build_from_csv(file_path) ⇒ LedgerTransaction

Deprecated.

Legacy alias for backwards compatibility

Parameters:

  • file_path (String)

Returns:



156
157
158
# File 'app/models/ledger_transaction.rb', line 156

def self.build_from_csv(file_path)
  build_from_spreadsheet(file_path)
end

.build_from_spreadsheet(file_path, original_filename: nil, encoding: nil) ⇒ LedgerTransaction

Build a draft LedgerTransaction from a CSV/XLSX upload (manual
journal-entry import). Each row maps to one LedgerEntry with
company / account / project / business-unit looked up by number.
If every row targets the same company we pin the transaction to
it; otherwise the JE is flagged as a multi-company override.

Parameters:

  • file_path (String)
  • original_filename (String, nil) (defaults to: nil)

    used to detect .xlsx vs CSV

  • encoding (String, nil) (defaults to: nil)

Returns:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/models/ledger_transaction.rb', line 128

def self.build_from_spreadsheet(file_path, original_filename: nil, encoding: nil)
  lt = LedgerTransaction.new
  companies = []

  each_spreadsheet_row(file_path, original_filename: original_filename, encoding: encoding) do |row|
    companies << row['company']
    lt.ledger_entries.build(
      ledger_company_account: LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['company'] }, ledger_accounts: { number: row['account'] }).first,
      ledger_detail_project: LedgerDetailProject.where(project_number: row['project']).first,
      amount: row['amount'],
      business_unit: BusinessUnit.where(number: row['business_unit']).first,
      description: row['description']
    )
  end

  companies.uniq!
  if companies.length == 1
    lt.company = Company.where(number: companies[0]).first
  else
    lt.override = '1'
  end
  lt
end

.each_csv_row(file_path, encoding: nil) {|row| ... } ⇒ void

This method returns an undefined value.

Iterate a CSV upload, retrying with Windows-1252 if UTF-8
decoding fails — accountants frequently export from Excel
without picking an encoding.

Parameters:

  • file_path (String)
  • encoding (String, nil) (defaults to: nil)

    explicit override; one of SUPPORTED_CSV_ENCODINGS keys

Yield Parameters:

  • row (Hash{String=>String,nil})


220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'app/models/ledger_transaction.rb', line 220

def self.each_csv_row(file_path, encoding: nil)
  require 'csv'

  if encoding.present? && SUPPORTED_CSV_ENCODINGS[encoding]
    # User specified encoding
    CSV.foreach(file_path, headers: true, return_headers: false, encoding: SUPPORTED_CSV_ENCODINGS[encoding]) do |row|
      yield row.to_h
    end
  else
    # Auto-detect: try UTF-8 first, then Windows-1252
    CSV.foreach(file_path, headers: true, return_headers: false, encoding: 'bom|utf-8') do |row|
      yield row.to_h
    end
  end
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
  CSV.foreach(file_path, headers: true, return_headers: false, encoding: 'windows-1252:utf-8') do |row|
    yield row.to_h
  end
end

.each_spreadsheet_row(file_path, original_filename: nil, encoding: nil) {|row| ... } ⇒ void

This method returns an undefined value.

Yield each row as a Hash keyed by the spreadsheet's header row.
Dispatches to each_xlsx_row for .xlsx and each_csv_row
otherwise.

Parameters:

  • file_path (String)
  • original_filename (String, nil) (defaults to: nil)
  • encoding (String, nil) (defaults to: nil)

Yield Parameters:

  • row (Hash{String=>String,nil})


169
170
171
172
173
174
175
176
177
# File 'app/models/ledger_transaction.rb', line 169

def self.each_spreadsheet_row(file_path, original_filename: nil, encoding: nil, &block)
  extension = original_filename ? File.extname(original_filename).downcase : File.extname(file_path).downcase

  if extension == '.xlsx'
    each_xlsx_row(file_path, &block)
  else
    each_csv_row(file_path, encoding: encoding, &block)
  end
end

.each_xlsx_row(file_path) {|row| ... } ⇒ void

This method returns an undefined value.

Iterate an .xlsx upload using Roo. First row is treated as
headers and stripped.

Parameters:

  • file_path (String)

Yield Parameters:

  • row (Hash{String=>String,nil})


185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'app/models/ledger_transaction.rb', line 185

def self.each_xlsx_row(file_path)
  require 'roo'
  xlsx = Roo::Spreadsheet.open(file_path, extension: :xlsx)
  headers = xlsx.row(1).map(&:to_s).map(&:strip)
  (2..xlsx.last_row).each do |row_idx|
    row_data = xlsx.row(row_idx)
    row_hash = headers.zip(row_data.map { |v| v.to_s.presence }).to_h
    yield row_hash
  end
ensure
  # Roo extracts the xlsx archive into a tempdir and holds file
  # handles to it until #close is called. Without this, the temp
  # files leak (and on Windows the parent Tempfile.create cleanup
  # would fail because Roo still has the handle open).
  xlsx&.close
end

.filter(params) ⇒ ActiveRecord::Relation<LedgerTransaction>

Search scope for the GL transactions index page. Joins
ledger_entries plus optional receipts / outgoing_payments
so the same query can filter on entry-level fields
(reconciled, bank_date, company_account_id) and
transaction-level fields together.

Parameters:

  • params (Hash, ActionController::Parameters)

Returns:



705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
# File 'app/models/ledger_transaction.rb', line 705

def self.filter(params)
  params[:transaction_type] = params[:transaction_type].map(&:presence).compact if params[:transaction_type].present?
  params[:reconciled] = params[:reconciled].to_b if params[:reconciled].present?
  params[:payment_type] = params[:payment_type].map(&:presence).compact if params[:payment_type].present?
  params[:company_account_id] = params[:company_account_id].map(&:presence).compact.map!(&:to_i) if params[:company_account_id].present?
  ledger_transactions = LedgerTransaction.left_outer_joins(:ledger_entries, :receipt, :outgoing_payment)
                                         .select('ledger_transactions.*')
                                         .order(transaction_date: :desc, transaction_number: :desc, ledger_entries: { id: :desc })
  ledger_transactions = ledger_transactions.where(transaction_type: params[:transaction_type]) if params[:transaction_type].present?
  ledger_transactions = ledger_transactions.where(ledger_entries: { reconciled: params[:reconciled] }) if params[:reconciled] == false || params[:reconciled] == true
  ledger_transactions = ledger_transactions.where(OutgoingPayment[:category].in(params[:payment_type].collect { |pt| pt.downcase.underscore }).or(Receipt[:category].in(params[:payment_type]))) if params[:payment_type].present?
  if params[:transaction_date_gteq].present? or params[:transaction_date_lteq].present?
    start_date = params[:transaction_date_gteq].presence || '2000-01-01'
    end_date = params[:transaction_date_lteq].presence || Time.current.utc.to_date.to_fs(:db)
    ledger_transactions = ledger_transactions.where(transaction_date: start_date.to_date..end_date.to_date)
  end
  if params[:bank_date_gteq].present? or params[:bank_date_lteq].present?
    bank_start_date = params[:bank_date_gteq].presence || '2000-01-01'
    bank_end_date = params[:bank_date_lteq].presence || Time.current.utc.to_date.to_fs(:db)
    ledger_transactions = ledger_transactions.where(ledger_entries: { bank_date: bank_start_date.to_date..bank_end_date.to_date })
  end
  if params[:reconciled_date_gteq].present? or params[:reconciled_date_lteq].present?
    reconciled_start_date = params[:reconciled_date_gteq].presence || '2000-01-01'
    reconciled_end_date = params[:reconciled_date_lteq].presence || Time.current.utc.to_date.to_fs(:db)
    ledger_transactions = ledger_transactions.where(ledger_entries: { reconciled_at: reconciled_start_date.to_date..reconciled_end_date.to_date })
  end
  ledger_transactions = ledger_transactions.where(ledger_entries: { ledger_company_account_id: params[:company_account_id] }) if params[:company_account_id].present?
  ledger_transactions
end

.process_credit_memo(credit_memo) ⇒ void

This method returns an undefined value.

Post the GL entry for a CreditMemo: per-line return account
by cm_category, taxes, returned-goods/services/freight
discount accounts, with the A/R leg computed as the balancing
amount to absorb sub-cent rounding between stored totals and
summed line items.

Parameters:

Raises:

  • (RuntimeError)

    on missing accounts or unknown cm_category



565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
# File 'app/models/ledger_transaction.rb', line 565

def self.process_credit_memo(credit_memo)
  company = credit_memo.company

   = LedgerCompanyAccount.(company.id, RETURN_FREIGHT_COUPONS_ACCOUNT)
   = if credit_memo.category == 'standalone'
                             LedgerCompanyAccount.(company.id, RETURN_COUPONS_STANDALONE_ACCOUNT)
                           else
                             LedgerCompanyAccount.(company.id, RETURN_COUPONS_ACCOUNT)
                           end
   = LedgerCompanyAccount.(company.id, RETURN_SERVICE_COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, TRADE_ACCOUNTS_RECEIVABLE_ACCOUNT)

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

  sales_business_unit = company.sales_business_unit
  raise 'Unable to find sales business account' if sales_business_unit.nil?

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  # postings should be
  # + Tax accounts (summarised)
  # + for every line_item's linked cm_category account
  # + 6272 (return freight coupons account)
  # + 5310 (return coupons account) for any linked discounts
  # - 1210 (Trade Accounts Receivable)

  LedgerTransaction.transaction do
    transaction = LedgerTransaction.new(company:, transaction_type: 'CREDIT_MEMO', transaction_date: credit_memo.gl_date, currency: credit_memo.currency, credit_memo:, description: "Credit Memo ##{credit_memo.reference_number}")

    # line items
    credit_memo.line_items.parents_only.each do |li|
      if li.cm_category == 'Item'
        line_item_details = if li.item.tax_class == 'svc'
                              { name: 'Item', account_number: SERVICE_CANCELLATIONS_ACCOUNT, business_unit: 'default' }
                            else
                              { name: 'Item', account_number: CUSTOMER_RETURNS, business_unit: 'default' }
                            end
      elsif li.cm_category == 'Misc'
         = li.
        business_unit = li.business_unit
      else
        line_item_details = begin
          CreditMemo::LINE_ITEM_CATEGORIES.select { |cat| cat[:name] == li.cm_category }[0]
        rescue StandardError
          nil
        end
      end
      raise "Can't find details for credit memo category #{li.cm_category}" if (li.cm_category != 'Misc') && line_item_details.nil?

       ||= LedgerCompanyAccount.(company.id, line_item_details[:account_number])
      raise "Unable to find matching account for credit memo category #{li.cm_category}" if .nil?

      business_unit ||= company.business_unit_for(line_item_details[:business_unit])
      raise "Unable to find matching business unit for credit memo category #{li.cm_category}" if business_unit.nil?

      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: credit_memo.currency, amount: -li.price_total, description: li.ledger_description, business_unit:)
    end

    # taxes
    unless credit_memo.taxes_grouped_by_rate.empty?
      credit_memo.taxes_grouped_by_rate.each do |tax_type, amount|
        if credit_memo.customer.country.eu_country?
           = credit_memo.resource_tax_rate&.tax_rate&.
        else
           = TAXABLE_STATES.select { |_state, details| details[:tax_type] == tax_type }.first[1][:account]
           = LedgerDetailAccount.where(number: ).first
        end

        raise "Unable to find ledger account for tax type: #{tax_type}, account number: #{}" if .nil?

         = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: .id).first
        raise "Unable to find company account for tax type: #{tax_type}, account number: #{}" if .nil?

        transaction.ledger_entries << LedgerEntry.new(ledger_company_account_id: .id, currency: credit_memo.currency, amount: -amount)
      end
    end

    # Discount entries - calculate coupon_amount for each category
    # Note: Using cm_category-aware filtering rather than tax_class scopes
    # because some line items (e.g., 'Coupon (Goods)') can have tax_class='none'
    # which would be missed by the .goods scope (tax_class='g')
    freight_categories = ['Freight', 'Coupon (Freight)']

    # Goods discount (items that are not freight and not services)
    goods_discount = credit_memo.line_items
                                .where.not(cm_category: freight_categories)
                                .where.not(tax_class: 'svc')
                                .to_a.sum(&:coupon_amount)
    unless goods_discount.zero?
      transaction.ledger_entries << LedgerEntry.new(
        ledger_company_account: ,
        currency: credit_memo.currency,
        amount: -goods_discount,
        business_unit: default_business_unit
      )
    end

    # Services discount
    services_discount = credit_memo.line_items.services.to_a.sum(&:coupon_amount)
    unless services_discount.zero?
      transaction.ledger_entries << LedgerEntry.new(
        ledger_company_account: ,
        currency: credit_memo.currency,
        amount: -services_discount,
        business_unit: default_business_unit
      )
    end

    # Freight/shipping discount
    shipping_discount = credit_memo.line_items
                                   .where(cm_category: freight_categories)
                                   .to_a.sum(&:coupon_amount)
    unless shipping_discount.zero?
      transaction.ledger_entries << LedgerEntry.new(
        ledger_company_account: ,
        currency: credit_memo.currency,
        amount: -shipping_discount,
        business_unit: sales_business_unit
      )
    end

    # AR entry: calculate as balancing amount to ensure ledger always balances
    # This avoids floating-point precision issues between stored totals and calculated sums
    other_entries_sum = transaction.ledger_entries.sum(&:amount)
    ar_amount = -other_entries_sum
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: credit_memo.currency, amount: ar_amount)

    transaction.save!
  end
end

.process_intracompany_st_delivery(delivery) ⇒ void

This method returns an undefined value.

Move COGS into an intracompany-transit account at the moment a
store-transfer Delivery ships, when both stores belong to the
same Company. Raises if from/to companies differ — those
transfers are invoiced via process_st_invoice instead.

Parameters:

Raises:

  • (RuntimeError)

    on cross-company delivery or missing accounts



527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'app/models/ledger_transaction.rb', line 527

def self.process_intracompany_st_delivery(delivery)
  order = delivery.order

  consignor = order.from_store.consignee_party
  consignee = order.to_store.consignee_party

  company = order.from_store.company
  to_company = order.to_store.company
  raise 'Unable to process intracompany ST delivery as from/to companies are different' if company != to_company

   = LedgerCompanyAccount.(company.id, order.from_store.)

  # we put the cogs value into an intracompany transit account while in transit
   = LedgerCompanyAccount.(company.id, INTRACOMPANY_TRANSIT_ACCOUNT)

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

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  LedgerTransaction.transaction do
    cogs = delivery.calculate_all_cogs
    transaction = LedgerTransaction.new(company:, transaction_type: 'STORE_TRANSFER', transaction_date: delivery.shipped_date, currency: order.currency, delivery:)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: order.currency, amount: cogs, business_unit: default_business_unit)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: order.currency, amount: -cogs, business_unit: default_business_unit)
    transaction.save!
  end
end

.process_invoice(invoice) ⇒ void

This method returns an undefined value.

Post the GL transaction for an Invoice. Routes to
process_st_invoice for store-transfer invoices and
process_so_invoice for everything else (sales orders, misc,
consignment).

Parameters:



335
336
337
338
339
340
341
342
343
# File 'app/models/ledger_transaction.rb', line 335

def self.process_invoice(invoice)
  return if invoice.ledger_transactions.any?

  if invoice.invoice_type == Invoice::ST
    LedgerTransaction.process_st_invoice(invoice)
  else
    LedgerTransaction.process_so_invoice(invoice)
  end
end

.process_payment(payment) ⇒ void

This method returns an undefined value.

Post the GL entry for an OutgoingPayment: debit the bank
account and credit the appropriate offset account per item
(voucher → AP, credit-memo / receipt → trade A/R, supplier-
specific override when present).

Parameters:

Raises:

  • (RuntimeError)

    on missing bank or offset account



743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# File 'app/models/ledger_transaction.rb', line 743

def self.process_payment(payment)
  LedgerTransaction.transaction do
    company = payment.company
    currency = payment.currency

    transaction = LedgerTransaction.new(company:, transaction_type: 'PAYMENT', transaction_date: payment.payment_date, currency:, exchange_rate: payment.exchange_rate, outgoing_payment: payment)

     = payment..
     = LedgerCompanyAccount.(company.id, TRADE_ACCOUNTS_RECEIVABLE_ACCOUNT)

     = if payment.supplier..present?
                               LedgerCompanyAccount.(company.id, payment.supplier..number)
                             else
                               LedgerCompanyAccount.(company.id, TRADE_ACCOUNTS_PAYABLE_ACCOUNT)
                             end

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

    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency:, amount: -payment.amount)

    payment.outgoing_payment_items.each do |pi|
      if pi.voucher_item.present?
        transaction.ledger_entries << if pi.voucher_item..present?
                                        LedgerEntry.new(ledger_company_account: pi.voucher_item., currency:, amount: pi.amount, business_unit: pi.voucher_item.business_unit)
                                      else
                                        LedgerEntry.new(ledger_company_account: , currency:, amount: pi.amount)
                                      end
      elsif pi.credit_memo.present? || pi.receipt.present?
        transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency:, amount: pi.amount)
      end
    end

    transaction.save!
  end
end

.process_so_invoice(invoice) ⇒ void

This method returns an undefined value.

Post the full sales-order / misc / consignment GL entry for
invoice: sales (goods + services + misc + fees), freight,
taxes (US per-state account or EU per-rate account), discounts
broken out by category, A/R total, and COGS legs. Wrapped in a
transaction; raises if any chart-of-accounts lookup misses.

Parameters:

Raises:



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'app/models/ledger_transaction.rb', line 354

def self.process_so_invoice(invoice)
  company = invoice.company

  # goods and services account dependent on Store selected
  if invoice.store && (invoice.store.owner != 'warmlyyours')
    # it's a consignment order, so use the consignment account
     = LedgerCompanyAccount.(company.id, CONSIGNMENT_ACCOUNT)
     = LedgerCompanyAccount.(company.id, CONSIGNMENT_ACCOUNT)
  else
     = LedgerCompanyAccount.(company.id, PURCHASED_FINISHED_GOODS_ACCOUNT)
     = LedgerCompanyAccount.(company.id, SERVICES_PENDING_FULFILLMENT_DEBIT_ACCOUNT)
  end
   = LedgerCompanyAccount.(company.id, PRODUCT_SALES_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SERVICE_SALES_ACCOUNT)
   = LedgerCompanyAccount.(company.id, COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SERVICE_COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, FREIGHT_COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, FREIGHT_ACCOUNT)
   = invoice.
   = LedgerCompanyAccount.(company.id, PRODUCT_COGS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, PRODUCT_COSS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SALES_EXPENSES_ACCOUNT)

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

  sales_business_unit = company.sales_business_unit
  raise 'Unable to find sales business account' if sales_business_unit.nil?

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  LedgerTransaction.transaction do
    t_type = case invoice.invoice_type
             when Invoice::MI
               'MISC_INVOICE'
             when Invoice::CI
               'CONSIGNMENT_INVOICE'
             else
               'SALES_ORDER'
             end

    transaction = LedgerTransaction.new(company:, transaction_type: t_type, transaction_date: invoice.gl_date, currency: invoice.currency, invoice:)
    # non service items
    non_service_item_total = invoice.non_service_line_items.to_a.sum(&:price_total)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -non_service_item_total, business_unit: default_business_unit) unless non_service_item_total == 0

    # service items
    service_item_total = invoice.service_line_items.to_a.sum(&:price_total)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -service_item_total, business_unit: default_business_unit) unless service_item_total == 0

    # misc items
    invoice.line_items.where(cm_category: 'Misc').each do |li|
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: li., currency: invoice.currency, amount: -li.price_total, business_unit: li.business_unit)
    end

    # fees
    invoice.line_items.where(cm_category: 'Fee').each do |li|
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -li.price_total, business_unit: sales_business_unit)
    end

    # shipping
    freight_total = BigDecimal(invoice.line_items.where("cm_category = 'Freight'").to_a.sum(&:price_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -freight_total, business_unit: sales_business_unit) unless freight_total == 0

    # taxes
    customer_ledger_project = invoice&.customer&.ledger_detail_project
    invoice.taxes_grouped_by_rate.each do |tax_type, tax_amount|
      if invoice.customer.country.eu_country?
        # if european, we search for the account number directly in the DB
         = LedgerDetailAccount.find(invoice.resource_tax_rate.tax_rate.)
      else
         = TAXABLE_STATES.select { |_state, details| details[:tax_type] == tax_type }.first[1][:account]
         = LedgerDetailAccount.where(number: ).first
      end

      raise "Unable to find ledger account for tax type: #{tax_type}, account number: #{}" if .nil?

       = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: .id).first
      raise "Unable to find company account for tax type: #{tax_type}, account number: #{}" if .nil?

      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: tax_amount * -1, ledger_detail_project: customer_ledger_project)
    end

    # goods discount
    goods_discount = invoice.line_items.goods.to_a.sum(&:coupon_amount) + BigDecimal(invoice.line_items.where("cm_category = 'Coupon (Goods)'").to_a.sum(&:discounted_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -goods_discount, business_unit: default_business_unit) unless goods_discount == 0

    # services discount
    services_discount = invoice.line_items.services.to_a.sum(&:coupon_amount) + BigDecimal(invoice.line_items.where("cm_category = 'Coupon (Services)'").to_a.sum(&:discounted_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -services_discount, business_unit: default_business_unit) unless services_discount == 0

    # shipping discount
    freight_discount = invoice.line_items.where("cm_category = 'Freight'").to_a.sum(&:coupon_amount) + BigDecimal(invoice.line_items.where("cm_category = 'Coupon (Freight)'").to_a.sum(&:discounted_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -freight_discount, business_unit: sales_business_unit) unless freight_discount == 0

    # total into a/r
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: invoice.total, business_unit: invoice.business_unit)

    # cogs
    non_service_cogs = invoice.calculate_cogs(['g'])
    service_only_cogs = invoice.calculate_cogs(['svc'])

    unless (non_service_cogs == 0) && invoice.non_service_line_items.empty?
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: non_service_cogs, business_unit: default_business_unit)
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -non_service_cogs)
    end
    unless (service_only_cogs == 0) && invoice.service_line_items.empty?
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: service_only_cogs, business_unit: default_business_unit)
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -service_only_cogs)
    end

    unless transaction.valid?
      transaction.errors.add :base, "ENTRIES DETAIL: #{transaction.ledger_entries.collect { |le| "#{le..name} #{le.amount}" }.join(', ')}"
      raise "Cannot save ledger transaction, errors: #{transaction.errors.full_messages}"
    end

    transaction.save!
  end
end

.process_st_invoice(invoice) ⇒ void

This method returns an undefined value.

Post the GL entry for a Store-Transfer (Invoice::ST) invoice:
intercompany sales / receivables / COGS instead of the regular
sales chart. No taxes, freight, or coupons are expected on STs.

Parameters:

Raises:

  • (RuntimeError)

    if intercompany accounts are missing



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
# File 'app/models/ledger_transaction.rb', line 483

def self.process_st_invoice(invoice)
  company = invoice.company

   = LedgerCompanyAccount.(company.id, PURCHASED_FINISHED_GOODS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SERVICES_PENDING_FULFILLMENT_DEBIT_ACCOUNT)
   = LedgerCompanyAccount.(company.id, INTERCOMPANY_SALES_ACCOUNT)
   = invoice.
   = LedgerCompanyAccount.(company.id, INTERCOMPANY_COGS_ACCOUNT)

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

  # raise "Subtotal and Total do not match for this ST invoice - may have coupons, shipping or tax which should not be there" if invoice.subtotal != invoice.total

  sales_business_unit = company.sales_business_unit
  raise 'Unable to find sales business account' if sales_business_unit.nil?

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  LedgerTransaction.transaction do
    transaction = LedgerTransaction.new(company:, transaction_type: 'STORE_TRANSFER', transaction_date: invoice.gl_date, currency: invoice.currency, invoice:)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -invoice.total, business_unit: default_business_unit)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: invoice.total, business_unit: invoice.business_unit)

    non_service_cogs = invoice.calculate_cogs(['g'])
    service_only_cogs = invoice.calculate_cogs(['svc'])

    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: invoice.calculate_all_cogs, business_unit: default_business_unit)

    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -non_service_cogs) unless (non_service_cogs == 0) && invoice.non_service_line_items.empty?
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -service_only_cogs) unless (service_only_cogs == 0) && invoice.service_line_items.empty?

    transaction.save!
  end
end

Instance Method Details

#balanceBigDecimal

Sum of amount across all entries in the transaction's native
currency. A balanced JE returns 0.

Returns:

  • (BigDecimal)


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

def balance
  balance = BigDecimal(0)
  ledger_entries.each do |entry|
    balance += entry.amount unless entry.amount.nil?
  end
  balance
end

#balance_inter_company_transactionvoid

This method returns an undefined value.

When a JE balances overall but mixes multiple companies, inject
plug entries to the inter-company-transfers account on each
company so each company's books balance independently.



820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
# File 'app/models/ledger_transaction.rb', line 820

def balance_inter_company_transaction
  return unless balance == 0

  groups = ledger_entries.group_by do |e|
             e.&.company
           end.delete_if { |k, _v| k.nil? }
  return unless groups.length > 1

  # must be intercompany so need to balance them out, unless they already are balanced
   = LedgerDetailAccount.find_by(number: INTERCOMPANY_TRANSFERS_ACCOUNT)
  unless 
    errors.add(:base, 'Cannot find Inter Company Transfers account')
    return
  end

  groups.each do |company, entries|
    balance = BigDecimal(0)
    entries.each { |e| balance += e.amount }
    next unless balance != 0

    # there is a difference so need to add it to the intercompany transfer account
    diff = -balance
     = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: .id).first
    unless 
      errors.add(:base, "Cannot find Inter Company Transfers account for company id: #{company.id}")
      return
    end

    entry = LedgerEntry.new(ledger_transaction: self, ledger_company_account: , amount: diff, currency:)
    unless entry.valid?
      entry.errors.full_messages.each do |msg|
        errors.add(:base, "Inter-company transfer entry invalid: #{msg}")
      end
      return
    end
    # ledger_entries << entry
  end
end

#balances_are_zerovoid

This method returns an undefined value.

Validation: native, company, and consolidated balances must
each net to zero. Adds detailed error messages listing every
entry to help operators eyeball the imbalance.



807
808
809
810
811
812
813
# File 'app/models/ledger_transaction.rb', line 807

def balances_are_zero
  errors.add(:base, "Balance of amounts must equal 0, but is #{balance}. ENTRIES DETAIL: #{ledger_entries.collect { |le| "#{le..try(:name)} #{le.amount}" }.join(', ')}") unless balance.zero?
  errors.add(:base, "Balance of company amounts must equal 0, but is #{company_balance}. ENTRIES DETAIL: #{ledger_entries.collect { |le| "#{le..try(:name)} #{le.company_amount}" }.join(', ')}") unless company_balance.zero?
  return if consolidated_balance.zero?

  errors.add(:base, "Balance of consolidated amounts must equal 0, but is #{consolidated_balance}. ENTRIES DETAIL: #{ledger_entries.collect { |le| "#{le..try(:name)} #{le.consolidated_amount}" }.join(', ')}")
end

#cat_for_reportString?

Category column for the cash-activity report — payment or
receipt category, whichever applies.

Returns:

  • (String, nil)


956
957
958
959
960
961
962
# File 'app/models/ledger_transaction.rb', line 956

def cat_for_report
  if outgoing_payment.present?
    outgoing_payment.category
  elsif receipt.present?
    receipt.category
  end
end

#check_for_discrepanciesvoid

This method returns an undefined value.

Absorb up to ±0.05 of FX-conversion drift on the company /
consolidated balances by adjusting the last entry, so that
rounding artefacts don't fail the balance validations.



864
865
866
867
868
869
870
871
872
873
# File 'app/models/ledger_transaction.rb', line 864

def check_for_discrepancies
  if company_balance != 0 && company_balance.abs <= (BigDecimal('0.05'))
    # correct the difference but only if it is +/- 0.05 or less
    ledger_entries.last.company_amount += -company_balance
  end
  return unless consolidated_balance != 0 && consolidated_balance.abs <= (BigDecimal('0.05'))

  # correct the difference but only if it is +/- 0.05 or less
  ledger_entries.last.consolidated_amount += -consolidated_balance
end

#clear_exchange_ratesvoid

This method returns an undefined value.

Wipe cached company/consolidated rates and amounts on every
entry so they get re-derived from the current ExchangeRate
tables — except on reversals, which must keep the original
rates so the reversal nets cleanly.



881
882
883
884
885
886
887
888
889
890
891
# File 'app/models/ledger_transaction.rb', line 881

def clear_exchange_rates
  return if reversed_transaction_id.present?
  return unless ledger_entries.any? { |le| le.new_record? || le.changed? }

  ledger_entries.each do |le|
    le.consolidated_exchange_rate = nil
    le.consolidated_amount = nil
    le.company_exchange_rate = nil
    le.company_amount = nil
  end
end

#companyCompany

Returns:

See Also:

Validations:



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

belongs_to :company, optional: true

#company_balanceBigDecimal

Sum of company_amount across all entries (the per-Company
functional currency leg).

Returns:

  • (BigDecimal)


256
257
258
259
260
261
262
# File 'app/models/ledger_transaction.rb', line 256

def company_balance
  company_balance = BigDecimal(0)
  ledger_entries.each do |entry|
    company_balance += entry.company_amount unless entry.company_amount.nil?
  end
  company_balance
end

#consolidated_balanceBigDecimal

Sum of consolidated_amount (USD reporting currency) across all
entries — what shows up in consolidated financials.

Returns:

  • (BigDecimal)


268
269
270
271
272
273
274
# File 'app/models/ledger_transaction.rb', line 268

def consolidated_balance
  consolidated_balance = BigDecimal(0)
  ledger_entries.each do |entry|
    consolidated_balance += entry.consolidated_amount unless entry.consolidated_amount.nil?
  end
  consolidated_balance
end

#credit_memoCreditMemo

Returns:

See Also:



63
# File 'app/models/ledger_transaction.rb', line 63

belongs_to :credit_memo, optional: true

#deliveryDelivery

Returns:

See Also:



65
# File 'app/models/ledger_transaction.rb', line 65

belongs_to :delivery, optional: true

#edit_allowedvoid

This method returns an undefined value.

Validation: block edits to LOCKED_ATTRIBUTES on transactions
whose date falls inside a LedgerClosingPeriod for the same
company + transaction type. The check matches both
company-specific and 'ALL' rules.

Prevents updates when locked attributes are changed inside a closed ledger period.

If company_id, transaction_type, and transaction_date are present and one or more
attributes in LOCKED_ATTRIBUTES are being changed, the method checks for any
overlapping LedgerClosingPeriod that applies to the company (or 'ALL') and
transaction type (or 'ALL') whose close_to is on or after transaction_date.
When such a period exists, an error is added to the record's base explaining the
closure and the applicable close_to date.



1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
# File 'app/models/ledger_transaction.rb', line 1015

def edit_allowed
  return if company_id.nil? || transaction_type.nil? || transaction_date.nil?
  return unless changed? && changes.keys.any? { |attr| LOCKED_ATTRIBUTES.include?(attr) }

  # `companies` is varchar[]; cast company_id (integer) to string so the bound
  # ARRAY literal has a single element type before PG applies the column cast.
  lcls = LedgerClosingPeriod.where.overlap(companies: [company_id.to_s, 'ALL'])
                            .where.overlap(transaction_types: ['ALL', transaction_type])
                            .where(close_to: transaction_date..)
  return unless lcls.any?

  errors.add :base, "Period is closed for #{transaction_type} for #{company.short_name} up to #{lcls.first.close_to.to_fs(:crm_default)}."
end

#explanation_for_reportString?

Free-form description column for the cash-activity report —
check number + payee for cheque payments, card type + customer
for credit-card receipts, otherwise the transaction
description.

Returns:

  • (String, nil)


970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
# File 'app/models/ledger_transaction.rb', line 970

def explanation_for_report
  if outgoing_payment.present?
    if outgoing_payment.category == 'check'
      check = outgoing_payment.checks.last
      check.nil? ? nil : "#{check.check_number} #{check.payee}"
    else
      "#{outgoing_payment.remark} #{outgoing_payment.supplier.full_name}"
    end
  elsif receipt.present?
    if receipt.category == 'Credit Card'
      "#{receipt.card_type} #{receipt.customer.full_name}"
    else
      "#{receipt.reference} #{receipt.customer.full_name}"
    end
  else
    description
  end
end

#has_two_ledger_entriesvoid

This method returns an undefined value.

Validation: every JE must have at least the debit + credit pair.



796
797
798
799
800
# File 'app/models/ledger_transaction.rb', line 796

def has_two_ledger_entries
  return unless ledger_entries.length < 2

  errors.add(:base, 'At least 2 ledger entries are required.')
end

#invoiceInvoice

Returns:

See Also:



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

belongs_to :invoice, optional: true

#item_ledger_entriesActiveRecord::Relation<ItemLedgerEntry>

Returns:

See Also:



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

has_many :item_ledger_entries

#landed_costLandedCost

Returns:

See Also:



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

belongs_to :landed_cost, optional: true

#ledger_entriesActiveRecord::Relation<LedgerEntry>

Returns:

See Also:



69
# File 'app/models/ledger_transaction.rb', line 69

has_many :ledger_entries, inverse_of: :ledger_transaction, dependent: :destroy

#outgoing_paymentOutgoingPayment



64
# File 'app/models/ledger_transaction.rb', line 64

belongs_to :outgoing_payment, optional: true

#receiptReceipt

Returns:

See Also:



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

belongs_to :receipt, optional: true

#resource_refString, Integer

Human-readable reference for the underlying source document
(invoice number, voucher number, payment reference, etc.) used
by the GL transactions report. Falls back to record id when no
source document is linked.

Returns:

  • (String, Integer)


926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
# File 'app/models/ledger_transaction.rb', line 926

def resource_ref
  if invoice.present?
    invoice.reference_number
  elsif receipt.present?
    receipt.id
  elsif shipment_receipt.present?
    shipment_receipt_id
  elsif landed_cost.present?
    landed_cost_id
  elsif voucher.present?
    voucher.reference_number
  elsif credit_memo.present?
    credit_memo.reference_number
  elsif outgoing_payment.present?
    outgoing_payment.reference_number
  elsif item_ledger_entries.any?
    refs = []
    item_ledger_entries.each do |entry|
      refs << entry.id
    end
    refs.join(' ')
  else
    id
  end
end

#reverse(reversal_date) ⇒ LedgerTransaction

Build, save, and return a reversing JE that nets this
transaction to zero on reversal_date. Each entry is duplicated
with negated amounts and reconciled cleared; the new
transaction's reversed_transaction_id points back at self.

Parameters:

  • reversal_date (Date)

Returns:



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
# File 'app/models/ledger_transaction.rb', line 283

def reverse(reversal_date)
  tran = dup
  tran.description = description.present? ? (description + ' - REVERSAL') : 'REVERSAL'
  tran.transaction_number = nil
  tran.created_at = nil
  tran.updated_at = nil
  tran.creator_id = nil
  tran.updater_id = nil
  tran.transaction_date = reversal_date
  tran.reversed_transaction_id = id
  ledger_entries.each do |le|
    new_le = le.dup
    new_le.ledger_transaction_id = nil
    new_le.created_at = nil
    new_le.updated_at = nil
    new_le.creator_id = nil
    new_le.updater_id = nil
    new_le.amount = -le.amount
    new_le.company_amount = -le.company_amount
    new_le.consolidated_amount = -le.consolidated_amount
    new_le.reconciled = false
    tran.ledger_entries << new_le
  end
  tran.save!
  tran
end

#reversed_transactionLedgerTransaction



66
# File 'app/models/ledger_transaction.rb', line 66

belongs_to :reversed_transaction, class_name: 'LedgerTransaction', optional: true

#set_company_amountvoid

This method returns an undefined value.

Trigger LedgerEntry#set_company_amount on every entry so each
converts to its company's functional currency. Skipped on
reversals (those keep the original transaction's amounts).



904
905
906
907
908
# File 'app/models/ledger_transaction.rb', line 904

def set_company_amount
  return if reversed_transaction_id.present?

  ledger_entries.each(&:set_company_amount)
end

#set_company_and_overridevoid

This method returns an undefined value.

Inspect every LedgerEntry and either pin company_id (single-
company JE) or flip the override flag (multi-company JE that
bypasses the single-company validation). Used by the spreadsheet
importer.



316
317
318
319
320
321
322
323
324
325
326
# File 'app/models/ledger_transaction.rb', line 316

def set_company_and_override
  company_ids = []
  ledger_entries.each do |e|
    company_ids << e..company_id unless company_ids.include?(e..company_id)
  end
  if company_ids.length == 1
    self.company_id = company_ids[0]
  else
    self.override = true
  end
end

#set_consolidated_amountvoid

This method returns an undefined value.

Trigger LedgerEntry#set_consolidated_amount on every entry to
populate the USD reporting-currency leg.



914
915
916
917
918
# File 'app/models/ledger_transaction.rb', line 914

def set_consolidated_amount
  return if reversed_transaction_id.present?

  ledger_entries.each(&:set_consolidated_amount)
end

#set_currencyvoid

This method returns an undefined value.

Cascade the transaction currency onto every entry.



895
896
897
# File 'app/models/ledger_transaction.rb', line 895

def set_currency
  ledger_entries.each(&:set_currency)
end

#set_supplier_idvoid

This method returns an undefined value.

Denormalize supplier_id from the linked voucher / outgoing
payment so the GL transactions search index can filter by
supplier without an extra join.



1034
1035
1036
1037
# File 'app/models/ledger_transaction.rb', line 1034

def set_supplier_id
  doc = voucher || outgoing_payment
  self.supplier_id = doc.supplier_id if doc.present?
end

#set_transaction_numbervoid

This method returns an undefined value.

Pull the next number from the shared transaction_numbers_seq
PostgreSQL sequence (so all transaction-numbered docs share a
contiguous range). Skipped when do_not_set_transaction_number
is set or a number is already assigned.



785
786
787
788
789
790
791
# File 'app/models/ledger_transaction.rb', line 785

def set_transaction_number
  return unless (!do_not_set_transaction_number == true) && transaction_number.blank?

  # getting number from a shared sequence now
  seq = LedgerTransaction.find_by_sql("SELECT nextval('transaction_numbers_seq') AS transaction_number")
  self.transaction_number = seq[0].transaction_number.to_s
end

#shipment_receiptShipmentReceipt



60
# File 'app/models/ledger_transaction.rb', line 60

belongs_to :shipment_receipt, optional: true

#to_sString

Returns:

  • (String)


114
115
116
# File 'app/models/ledger_transaction.rb', line 114

def to_s
  "Ledger Transaction ##{transaction_number}"
end

#transaction_date_not_before_originalvoid

This method returns an undefined value.

Validation: a reversal can't be dated before the transaction
it's reversing — that would post the negation in a closed
period.



994
995
996
997
998
# File 'app/models/ledger_transaction.rb', line 994

def transaction_date_not_before_original
  return unless transaction_date < reversed_transaction.transaction_date

  errors.add(:transaction_date, 'cannot be before the original transaction date')
end

#transaction_reversalLedgerTransaction



67
# File 'app/models/ledger_transaction.rb', line 67

has_one :transaction_reversal, class_name: 'LedgerTransaction', foreign_key: 'reversed_transaction_id'

#voucherVoucher

Returns:

See Also:



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

belongs_to :voucher, optional: true