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

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

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#currencyObject (readonly)

Validates presence of number, type, date, currency, and company.

Validations:



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

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)

Validates presence of number, type, date, currency, and company.

Validations:



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

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

#transaction_numberObject (readonly)

Validates presence of number, type, date, currency, and company.

Validations:



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

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

#transaction_typeObject (readonly)

Validates presence of number, type, date, currency, and company.

Validations:



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

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:



170
171
172
# File 'app/models/ledger_transaction.rb', line 170

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:



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'app/models/ledger_transaction.rb', line 142

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


234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'app/models/ledger_transaction.rb', line 234

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


183
184
185
186
187
188
189
190
191
# File 'app/models/ledger_transaction.rb', line 183

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


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'app/models/ledger_transaction.rb', line 199

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:



719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
# File 'app/models/ledger_transaction.rb', line 719

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



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
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'app/models/ledger_transaction.rb', line 579

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



541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# File 'app/models/ledger_transaction.rb', line 541

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:



349
350
351
352
353
354
355
356
357
# File 'app/models/ledger_transaction.rb', line 349

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



757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
# File 'app/models/ledger_transaction.rb', line 757

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:



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
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'app/models/ledger_transaction.rb', line 368

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



497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'app/models/ledger_transaction.rb', line 497

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)


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

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.



834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
# File 'app/models/ledger_transaction.rb', line 834

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.



821
822
823
824
825
826
827
# File 'app/models/ledger_transaction.rb', line 821

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)


970
971
972
973
974
975
976
# File 'app/models/ledger_transaction.rb', line 970

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.



878
879
880
881
882
883
884
885
886
887
# File 'app/models/ledger_transaction.rb', line 878

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.



895
896
897
898
899
900
901
902
903
904
905
# File 'app/models/ledger_transaction.rb', line 895

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

Company the transaction posts to.

Returns:

See Also:

Validations:



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

belongs_to :company, optional: true

#company_balanceBigDecimal

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

Returns:

  • (BigDecimal)


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

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)


282
283
284
285
286
287
288
# File 'app/models/ledger_transaction.rb', line 282

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

Credit memo that originated the transaction.

Returns:

See Also:



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

belongs_to :credit_memo, optional: true

#deliveryDelivery

Delivery that originated the transaction.

Returns:

See Also:



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

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.



1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
# File 'app/models/ledger_transaction.rb', line 1029

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)


984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
# File 'app/models/ledger_transaction.rb', line 984

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.



810
811
812
813
814
# File 'app/models/ledger_transaction.rb', line 810

def has_two_ledger_entries
  return unless ledger_entries.length < 2

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

#invoiceInvoice

Invoice that originated the transaction.

Returns:

See Also:



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

belongs_to :invoice, optional: true

#item_ledger_entriesActiveRecord::Relation<ItemLedgerEntry>

Inventory ledger entries attached to this transaction.

Returns:

See Also:



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

has_many :item_ledger_entries

#landed_costLandedCost

Landed cost record that originated the transaction.

Returns:

See Also:



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

belongs_to :landed_cost, optional: true

#ledger_entriesActiveRecord::Relation<LedgerEntry>

GL entries making up this transaction (debits/credits).

Returns:

See Also:



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

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

#outgoing_paymentOutgoingPayment

Outgoing payment that originated the transaction.



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

belongs_to :outgoing_payment, optional: true

#receiptReceipt

Receipt (incoming payment) that originated the transaction.

Returns:

See Also:



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

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)


940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
# File 'app/models/ledger_transaction.rb', line 940

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:



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

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

Transaction this transaction reverses.



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

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



918
919
920
921
922
# File 'app/models/ledger_transaction.rb', line 918

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.



330
331
332
333
334
335
336
337
338
339
340
# File 'app/models/ledger_transaction.rb', line 330

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.



928
929
930
931
932
# File 'app/models/ledger_transaction.rb', line 928

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.



909
910
911
# File 'app/models/ledger_transaction.rb', line 909

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.



1048
1049
1050
1051
# File 'app/models/ledger_transaction.rb', line 1048

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.



799
800
801
802
803
804
805
# File 'app/models/ledger_transaction.rb', line 799

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

Shipment receipt (PO receipt) that originated the transaction.



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

belongs_to :shipment_receipt, optional: true

#to_sString

Returns:

  • (String)


128
129
130
# File 'app/models/ledger_transaction.rb', line 128

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.



1008
1009
1010
1011
1012
# File 'app/models/ledger_transaction.rb', line 1008

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

Reversal transaction pointing back at this transaction.



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

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

#voucherVoucher

Voucher that originated the transaction.

Returns:

See Also:



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

belongs_to :voucher, optional: true