Class: LedgerTransaction
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- LedgerTransaction
- 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 bytransaction_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 toCSV.foreach(..., encoding:)—bom|utf-8strips 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
-
#currency ⇒ Object
readonly
Validates presence of number, type, date, currency, and company.
-
#do_not_set_transaction_number ⇒ Object
Returns the value of attribute do_not_set_transaction_number.
-
#override ⇒ Object
Returns the value of attribute override.
-
#transaction_date ⇒ Object
readonly
Validates presence of number, type, date, currency, and company.
-
#transaction_number ⇒ Object
readonly
Validates presence of number, type, date, currency, and company.
-
#transaction_type ⇒ Object
readonly
Validates presence of number, type, date, currency, and company.
Belongs to collapse
-
#company ⇒ Company
Company the transaction posts to.
-
#credit_memo ⇒ CreditMemo
Credit memo that originated the transaction.
-
#delivery ⇒ Delivery
Delivery that originated the transaction.
-
#invoice ⇒ Invoice
Invoice that originated the transaction.
-
#landed_cost ⇒ LandedCost
Landed cost record that originated the transaction.
-
#outgoing_payment ⇒ OutgoingPayment
Outgoing payment that originated the transaction.
-
#receipt ⇒ Receipt
Receipt (incoming payment) that originated the transaction.
-
#reversed_transaction ⇒ LedgerTransaction
Transaction this transaction reverses.
-
#shipment_receipt ⇒ ShipmentReceipt
Shipment receipt (PO receipt) that originated the transaction.
-
#voucher ⇒ Voucher
Voucher that originated the transaction.
Methods included from Models::Auditable
Has one collapse
-
#transaction_reversal ⇒ LedgerTransaction
Reversal transaction pointing back at this transaction.
Has many collapse
-
#item_ledger_entries ⇒ ActiveRecord::Relation<ItemLedgerEntry>
Inventory ledger entries attached to this transaction.
-
#ledger_entries ⇒ ActiveRecord::Relation<LedgerEntry>
GL entries making up this transaction (debits/credits).
Class Method Summary collapse
- .build_from_csv(file_path) ⇒ LedgerTransaction deprecated Deprecated.
-
.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_csv_row(file_path, encoding: nil) {|row| ... } ⇒ void
Iterate a CSV upload, retrying with
Windows-1252if UTF-8 decoding fails — accountants frequently export from Excel without picking an encoding. -
.each_spreadsheet_row(file_path, original_filename: nil, encoding: nil) {|row| ... } ⇒ void
Yield each row as a
Hashkeyed by the spreadsheet's header row. -
.each_xlsx_row(file_path) {|row| ... } ⇒ void
Iterate an
.xlsxupload usingRoo. -
.filter(params) ⇒ ActiveRecord::Relation<LedgerTransaction>
Search scope for the GL transactions index page.
-
.process_credit_memo(credit_memo) ⇒ void
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. - .process_intracompany_st_delivery(delivery) ⇒ void
-
.process_invoice(invoice) ⇒ void
Post the GL transaction for an Invoice.
-
.process_payment(payment) ⇒ void
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).
-
.process_so_invoice(invoice) ⇒ void
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. -
.process_st_invoice(invoice) ⇒ void
Post the GL entry for a Store-Transfer (Invoice::ST) invoice: intercompany sales / receivables / COGS instead of the regular sales chart.
Instance Method Summary collapse
-
#balance ⇒ BigDecimal
Sum of
amountacross all entries in the transaction's native currency. -
#balance_inter_company_transaction ⇒ void
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.
-
#balances_are_zero ⇒ void
Validation: native, company, and consolidated balances must each net to zero.
-
#cat_for_report ⇒ String?
Category column for the cash-activity report — payment or receipt category, whichever applies.
-
#check_for_discrepancies ⇒ void
Absorb up to
±0.05of FX-conversion drift on the company / consolidated balances by adjusting the last entry, so that rounding artefacts don't fail the balance validations. -
#clear_exchange_rates ⇒ void
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.
-
#company_balance ⇒ BigDecimal
Sum of
company_amountacross all entries (the per-Company functional currency leg). -
#consolidated_balance ⇒ BigDecimal
Sum of
consolidated_amount(USD reporting currency) across all entries — what shows up in consolidated financials. -
#edit_allowed ⇒ void
Validation: block edits to LOCKED_ATTRIBUTES on transactions whose date falls inside a LedgerClosingPeriod for the same company + transaction type.
-
#explanation_for_report ⇒ String?
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. -
#has_two_ledger_entries ⇒ void
Validation: every JE must have at least the debit + credit pair.
-
#resource_ref ⇒ String, Integer
Human-readable reference for the underlying source document (invoice number, voucher number, payment reference, etc.) used by the GL transactions report.
-
#reverse(reversal_date) ⇒ LedgerTransaction
Build, save, and return a reversing JE that nets this transaction to zero on
reversal_date. -
#set_company_amount ⇒ void
Trigger LedgerEntry#set_company_amount on every entry so each converts to its company's functional currency.
-
#set_company_and_override ⇒ void
Inspect every LedgerEntry and either pin
company_id(single- company JE) or flip theoverrideflag (multi-company JE that bypasses the single-company validation). -
#set_consolidated_amount ⇒ void
Trigger LedgerEntry#set_consolidated_amount on every entry to populate the USD reporting-currency leg.
-
#set_currency ⇒ void
Cascade the transaction
currencyonto every entry. -
#set_supplier_id ⇒ void
Denormalize
supplier_idfrom the linked voucher / outgoing payment so the GL transactions search index can filter by supplier without an extra join. -
#set_transaction_number ⇒ void
Pull the next number from the shared
transaction_numbers_seqPostgreSQL sequence (so all transaction-numbered docs share a contiguous range). - #to_s ⇒ String
-
#transaction_date_not_before_original ⇒ void
Validation: a reversal can't be dated before the transaction it's reversing — that would post the negation in a closed period.
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
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#currency ⇒ Object (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_number ⇒ Object
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 |
#override ⇒ Object
Returns the value of attribute override.
55 56 57 |
# File 'app/models/ledger_transaction.rb', line 55 def override @override end |
#transaction_date ⇒ Object (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_number ⇒ Object (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_type ⇒ Object (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
Legacy alias for backwards compatibility
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.
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.
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.
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.
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.
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.
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 return_freight_coupons_account = LedgerCompanyAccount.for_company_and_account(company.id, RETURN_FREIGHT_COUPONS_ACCOUNT) return_coupons_account = if credit_memo.category == 'standalone' LedgerCompanyAccount.for_company_and_account(company.id, RETURN_COUPONS_STANDALONE_ACCOUNT) else LedgerCompanyAccount.for_company_and_account(company.id, RETURN_COUPONS_ACCOUNT) end return_service_coupons_account = LedgerCompanyAccount.for_company_and_account(company.id, RETURN_SERVICE_COUPONS_ACCOUNT) trade_ar_account = LedgerCompanyAccount.for_company_and_account(company.id, TRADE_ACCOUNTS_RECEIVABLE_ACCOUNT) raise 'Unable to find all necessary accounts to post to' if return_freight_coupons_account.nil? || return_coupons_account.nil? || trade_ar_account.nil? || return_service_coupons_account.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' account = li.ledger_company_account 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? account ||= LedgerCompanyAccount.for_company_and_account(company.id, line_item_details[:account_number]) raise "Unable to find matching account for credit memo category #{li.cm_category}" if account.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: 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? tax_account = credit_memo.resource_tax_rate&.tax_rate&.sales_tax_payable_account else account_number = TAXABLE_STATES.select { |_state, details| details[:tax_type] == tax_type }.first[1][:account] tax_account = LedgerDetailAccount.where(number: account_number).first end raise "Unable to find ledger account for tax type: #{tax_type}, account number: #{account_number}" if tax_account.nil? tax_company_account = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: tax_account.id).first raise "Unable to find company account for tax type: #{tax_type}, account number: #{account_number}" if tax_company_account.nil? transaction.ledger_entries << LedgerEntry.new(ledger_company_account_id: tax_company_account.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: return_coupons_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: return_service_coupons_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: return_freight_coupons_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: trade_ar_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.
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 from_goods_account = LedgerCompanyAccount.for_company_and_account(company.id, order.from_store.goods_account) # we put the cogs value into an intracompany transit account while in transit intracompany_transit_account = LedgerCompanyAccount.for_company_and_account(company.id, INTRACOMPANY_TRANSIT_ACCOUNT) raise 'Unable to find all necessary accounts to post to' if from_goods_account.nil? || intracompany_transit_account.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: intracompany_transit_account, currency: order.currency, amount: cogs, business_unit: default_business_unit) transaction.ledger_entries << LedgerEntry.new(ledger_company_account: from_goods_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).
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).
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) bank_account = payment.bank_account.ledger_company_account trade_ar_account = LedgerCompanyAccount.for_company_and_account(company.id, TRADE_ACCOUNTS_RECEIVABLE_ACCOUNT) default_offset_account = if payment.supplier.gl_offset_account.present? LedgerCompanyAccount.for_company_and_account(company.id, payment.supplier.gl_offset_account.number) else LedgerCompanyAccount.for_company_and_account(company.id, TRADE_ACCOUNTS_PAYABLE_ACCOUNT) end raise 'Unable to find all necessary accounts to post to' if bank_account.nil? || default_offset_account.nil? || trade_ar_account.nil? transaction.ledger_entries << LedgerEntry.new(ledger_company_account: bank_account, currency:, amount: -payment.amount) payment.outgoing_payment_items.each do |pi| if pi.voucher_item.present? transaction.ledger_entries << if pi.voucher_item.gl_offset_account.present? LedgerEntry.new(ledger_company_account: pi.voucher_item.gl_offset_account, currency:, amount: pi.amount, business_unit: pi.voucher_item.business_unit) else LedgerEntry.new(ledger_company_account: default_offset_account, currency:, amount: pi.amount) end elsif pi.credit_memo.present? || pi.receipt.present? transaction.ledger_entries << LedgerEntry.new(ledger_company_account: trade_ar_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.
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 goods_account = LedgerCompanyAccount.for_company_and_account(company.id, CONSIGNMENT_ACCOUNT) services_account = LedgerCompanyAccount.for_company_and_account(company.id, CONSIGNMENT_ACCOUNT) else goods_account = LedgerCompanyAccount.for_company_and_account(company.id, PURCHASED_FINISHED_GOODS_ACCOUNT) services_account = LedgerCompanyAccount.for_company_and_account(company.id, SERVICES_PENDING_FULFILLMENT_DEBIT_ACCOUNT) end sales_company_account = LedgerCompanyAccount.for_company_and_account(company.id, PRODUCT_SALES_ACCOUNT) service_sales_company_account = LedgerCompanyAccount.for_company_and_account(company.id, SERVICE_SALES_ACCOUNT) coupons_company_account = LedgerCompanyAccount.for_company_and_account(company.id, COUPONS_ACCOUNT) service_coupons_company_account = LedgerCompanyAccount.for_company_and_account(company.id, SERVICE_COUPONS_ACCOUNT) freight_coupons_company_account = LedgerCompanyAccount.for_company_and_account(company.id, FREIGHT_COUPONS_ACCOUNT) freight_company_account = LedgerCompanyAccount.for_company_and_account(company.id, FREIGHT_ACCOUNT) ar_company_account = invoice.gl_offset_account cogs_company_account = LedgerCompanyAccount.for_company_and_account(company.id, PRODUCT_COGS_ACCOUNT) coss_company_account = LedgerCompanyAccount.for_company_and_account(company.id, PRODUCT_COSS_ACCOUNT) sales_expenses_account = LedgerCompanyAccount.for_company_and_account(company.id, SALES_EXPENSES_ACCOUNT) if goods_account.nil? || services_account.nil? || sales_company_account.nil? || service_sales_company_account.nil? || coupons_company_account.nil? || service_coupons_company_account.nil? || freight_coupons_company_account.nil? || freight_company_account.nil? || ar_company_account.nil? || cogs_company_account.nil? || coss_company_account.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: sales_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: service_sales_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.ledger_company_account, 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: sales_expenses_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: freight_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 tax_account = LedgerDetailAccount.find(invoice.resource_tax_rate.tax_rate.sales_tax_payable_account_id) else account_number = TAXABLE_STATES.select { |_state, details| details[:tax_type] == tax_type }.first[1][:account] tax_account = LedgerDetailAccount.where(number: account_number).first end raise "Unable to find ledger account for tax type: #{tax_type}, account number: #{account_number}" if tax_account.nil? tax_company_account = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: tax_account.id).first raise "Unable to find company account for tax type: #{tax_type}, account number: #{account_number}" if tax_company_account.nil? transaction.ledger_entries << LedgerEntry.new(ledger_company_account: tax_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: coupons_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: service_coupons_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: freight_coupons_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: ar_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: cogs_company_account, currency: invoice.currency, amount: non_service_cogs, business_unit: default_business_unit) transaction.ledger_entries << LedgerEntry.new(ledger_company_account: goods_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: coss_company_account, currency: invoice.currency, amount: service_only_cogs, business_unit: default_business_unit) transaction.ledger_entries << LedgerEntry.new(ledger_company_account: services_account, currency: invoice.currency, amount: -service_only_cogs) end unless transaction.valid? transaction.errors.add :base, "ENTRIES DETAIL: #{transaction.ledger_entries.collect { |le| "#{le.ledger_company_account.name} #{le.amount}" }.join(', ')}" raise "Cannot save ledger transaction, errors: #{transaction.errors.}" 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.
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 goods_account = LedgerCompanyAccount.for_company_and_account(company.id, PURCHASED_FINISHED_GOODS_ACCOUNT) services_account = LedgerCompanyAccount.for_company_and_account(company.id, SERVICES_PENDING_FULFILLMENT_DEBIT_ACCOUNT) intercompany_sales_account = LedgerCompanyAccount.for_company_and_account(company.id, INTERCOMPANY_SALES_ACCOUNT) intercompany_receivables_account = invoice.gl_offset_account intercompany_cogs_account = LedgerCompanyAccount.for_company_and_account(company.id, INTERCOMPANY_COGS_ACCOUNT) raise 'Unable to find all necessary accounts to post to' if goods_account.nil? || services_account.nil? || intercompany_sales_account.nil? || intercompany_receivables_account.nil? || intercompany_cogs_account.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: intercompany_sales_account, currency: invoice.currency, amount: -invoice.total, business_unit: default_business_unit) transaction.ledger_entries << LedgerEntry.new(ledger_company_account: intercompany_receivables_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: intercompany_cogs_account, currency: invoice.currency, amount: invoice.calculate_all_cogs, business_unit: default_business_unit) transaction.ledger_entries << LedgerEntry.new(ledger_company_account: goods_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: services_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
#balance ⇒ BigDecimal
Sum of amount across all entries in the transaction's native
currency. A balanced JE returns 0.
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_transaction ⇒ void
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.ledger_company_account&.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 transfer_account = LedgerDetailAccount.find_by(number: INTERCOMPANY_TRANSFERS_ACCOUNT) unless transfer_account 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 transfer_company_account = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: transfer_account.id).first unless transfer_company_account 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: transfer_company_account, amount: diff, currency:) unless entry.valid? entry.errors..each do |msg| errors.add(:base, "Inter-company transfer entry invalid: #{msg}") end return end # ledger_entries << entry end end |
#balances_are_zero ⇒ void
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.ledger_company_account.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.ledger_company_account.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.ledger_company_account.try(:name)} #{le.consolidated_amount}" }.join(', ')}") end |
#cat_for_report ⇒ String?
Category column for the cash-activity report — payment or
receipt category, whichever applies.
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_discrepancies ⇒ void
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_rates ⇒ void
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 |
#company ⇒ Company
Company the transaction posts to.
Validations:
58 |
# File 'app/models/ledger_transaction.rb', line 58 belongs_to :company, optional: true |
#company_balance ⇒ BigDecimal
Sum of company_amount across all entries (the per-Company
functional currency leg).
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_balance ⇒ BigDecimal
Sum of consolidated_amount (USD reporting currency) across all
entries — what shows up in consolidated financials.
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_memo ⇒ CreditMemo
Credit memo that originated the transaction.
70 |
# File 'app/models/ledger_transaction.rb', line 70 belongs_to :credit_memo, optional: true |
#delivery ⇒ Delivery
Delivery that originated the transaction.
74 |
# File 'app/models/ledger_transaction.rb', line 74 belongs_to :delivery, optional: true |
#edit_allowed ⇒ void
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_report ⇒ String?
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.
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_entries ⇒ void
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 |
#invoice ⇒ Invoice
Invoice that originated the transaction.
60 |
# File 'app/models/ledger_transaction.rb', line 60 belongs_to :invoice, optional: true |
#item_ledger_entries ⇒ ActiveRecord::Relation<ItemLedgerEntry>
Inventory ledger entries attached to this transaction.
80 |
# File 'app/models/ledger_transaction.rb', line 80 has_many :item_ledger_entries |
#landed_cost ⇒ LandedCost
Landed cost record that originated the transaction.
66 |
# File 'app/models/ledger_transaction.rb', line 66 belongs_to :landed_cost, optional: true |
#ledger_entries ⇒ ActiveRecord::Relation<LedgerEntry>
GL entries making up this transaction (debits/credits).
82 |
# File 'app/models/ledger_transaction.rb', line 82 has_many :ledger_entries, inverse_of: :ledger_transaction, dependent: :destroy |
#outgoing_payment ⇒ OutgoingPayment
Outgoing payment that originated the transaction.
72 |
# File 'app/models/ledger_transaction.rb', line 72 belongs_to :outgoing_payment, optional: true |
#receipt ⇒ Receipt
Receipt (incoming payment) that originated the transaction.
62 |
# File 'app/models/ledger_transaction.rb', line 62 belongs_to :receipt, optional: true |
#resource_ref ⇒ String, 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.
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.
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_transaction ⇒ LedgerTransaction
Transaction this transaction reverses.
76 |
# File 'app/models/ledger_transaction.rb', line 76 belongs_to :reversed_transaction, class_name: 'LedgerTransaction', optional: true |
#set_company_amount ⇒ void
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_override ⇒ void
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.ledger_company_account.company_id unless company_ids.include?(e.ledger_company_account.company_id) end if company_ids.length == 1 self.company_id = company_ids[0] else self.override = true end end |
#set_consolidated_amount ⇒ void
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_currency ⇒ void
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_id ⇒ void
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_number ⇒ void
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_receipt ⇒ ShipmentReceipt
Shipment receipt (PO receipt) that originated the transaction.
64 |
# File 'app/models/ledger_transaction.rb', line 64 belongs_to :shipment_receipt, optional: true |
#to_s ⇒ String
128 129 130 |
# File 'app/models/ledger_transaction.rb', line 128 def to_s "Ledger Transaction ##{transaction_number}" end |
#transaction_date_not_before_original ⇒ void
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_reversal ⇒ LedgerTransaction
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' |
#voucher ⇒ Voucher
Voucher that originated the transaction.
68 |
# File 'app/models/ledger_transaction.rb', line 68 belongs_to :voucher, optional: true |