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 Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
- #currency ⇒ Object readonly
-
#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
- #transaction_number ⇒ Object readonly
- #transaction_type ⇒ Object readonly
Belongs to collapse
- #company ⇒ Company
- #credit_memo ⇒ CreditMemo
- #delivery ⇒ Delivery
- #invoice ⇒ Invoice
- #landed_cost ⇒ LandedCost
- #outgoing_payment ⇒ OutgoingPayment
- #receipt ⇒ Receipt
- #reversed_transaction ⇒ LedgerTransaction
- #shipment_receipt ⇒ ShipmentReceipt
- #voucher ⇒ Voucher
Methods included from Models::Auditable
Has one collapse
Has many collapse
- #item_ledger_entries ⇒ ActiveRecord::Relation<ItemLedgerEntry>
- #ledger_entries ⇒ ActiveRecord::Relation<LedgerEntry>
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 Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#currency ⇒ Object (readonly)
79 |
# File 'app/models/ledger_transaction.rb', line 79 validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true |
#do_not_set_transaction_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)
79 |
# File 'app/models/ledger_transaction.rb', line 79 validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true |
#transaction_number ⇒ Object (readonly)
79 |
# File 'app/models/ledger_transaction.rb', line 79 validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true |
#transaction_type ⇒ Object (readonly)
79 |
# File 'app/models/ledger_transaction.rb', line 79 validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true |
Class Method Details
.build_from_csv(file_path) ⇒ LedgerTransaction
Legacy alias for backwards compatibility
156 157 158 |
# File 'app/models/ledger_transaction.rb', line 156 def self.build_from_csv(file_path) build_from_spreadsheet(file_path) end |
.build_from_spreadsheet(file_path, original_filename: nil, encoding: nil) ⇒ LedgerTransaction
Build a draft LedgerTransaction from a CSV/XLSX upload (manual
journal-entry import). Each row maps to one LedgerEntry with
company / account / project / business-unit looked up by number.
If every row targets the same company we pin the transaction to
it; otherwise the JE is flagged as a multi-company override.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'app/models/ledger_transaction.rb', line 128 def self.build_from_spreadsheet(file_path, original_filename: nil, encoding: nil) lt = LedgerTransaction.new companies = [] each_spreadsheet_row(file_path, original_filename: original_filename, encoding: encoding) do |row| companies << row['company'] lt.ledger_entries.build( ledger_company_account: LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['company'] }, ledger_accounts: { number: row['account'] }).first, ledger_detail_project: LedgerDetailProject.where(project_number: row['project']).first, amount: row['amount'], business_unit: BusinessUnit.where(number: row['business_unit']).first, description: row['description'] ) end companies.uniq! if companies.length == 1 lt.company = Company.where(number: companies[0]).first else lt.override = '1' end lt end |
.each_csv_row(file_path, encoding: nil) {|row| ... } ⇒ void
This method returns an undefined value.
Iterate a CSV upload, retrying with Windows-1252 if UTF-8
decoding fails — accountants frequently export from Excel
without picking an encoding.
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'app/models/ledger_transaction.rb', line 220 def self.each_csv_row(file_path, encoding: nil) require 'csv' if encoding.present? && SUPPORTED_CSV_ENCODINGS[encoding] # User specified encoding CSV.foreach(file_path, headers: true, return_headers: false, encoding: SUPPORTED_CSV_ENCODINGS[encoding]) do |row| yield row.to_h end else # Auto-detect: try UTF-8 first, then Windows-1252 CSV.foreach(file_path, headers: true, return_headers: false, encoding: 'bom|utf-8') do |row| yield row.to_h end end rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError CSV.foreach(file_path, headers: true, return_headers: false, encoding: 'windows-1252:utf-8') do |row| yield row.to_h end end |
.each_spreadsheet_row(file_path, original_filename: nil, encoding: nil) {|row| ... } ⇒ void
This method returns an undefined value.
Yield each row as a Hash keyed by the spreadsheet's header row.
Dispatches to each_xlsx_row for .xlsx and each_csv_row
otherwise.
169 170 171 172 173 174 175 176 177 |
# File 'app/models/ledger_transaction.rb', line 169 def self.each_spreadsheet_row(file_path, original_filename: nil, encoding: nil, &block) extension = original_filename ? File.extname(original_filename).downcase : File.extname(file_path).downcase if extension == '.xlsx' each_xlsx_row(file_path, &block) else each_csv_row(file_path, encoding: encoding, &block) end end |
.each_xlsx_row(file_path) {|row| ... } ⇒ void
This method returns an undefined value.
Iterate an .xlsx upload using Roo. First row is treated as
headers and stripped.
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'app/models/ledger_transaction.rb', line 185 def self.each_xlsx_row(file_path) require 'roo' xlsx = Roo::Spreadsheet.open(file_path, extension: :xlsx) headers = xlsx.row(1).map(&:to_s).map(&:strip) (2..xlsx.last_row).each do |row_idx| row_data = xlsx.row(row_idx) row_hash = headers.zip(row_data.map { |v| v.to_s.presence }).to_h yield row_hash end ensure # Roo extracts the xlsx archive into a tempdir and holds file # handles to it until #close is called. Without this, the temp # files leak (and on Windows the parent Tempfile.create cleanup # would fail because Roo still has the handle open). xlsx&.close end |
.filter(params) ⇒ ActiveRecord::Relation<LedgerTransaction>
Search scope for the GL transactions index page. Joins
ledger_entries plus optional receipts / outgoing_payments
so the same query can filter on entry-level fields
(reconciled, bank_date, company_account_id) and
transaction-level fields together.
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 |
# File 'app/models/ledger_transaction.rb', line 705 def self.filter(params) params[:transaction_type] = params[:transaction_type].map(&:presence).compact if params[:transaction_type].present? params[:reconciled] = params[:reconciled].to_b if params[:reconciled].present? params[:payment_type] = params[:payment_type].map(&:presence).compact if params[:payment_type].present? params[:company_account_id] = params[:company_account_id].map(&:presence).compact.map!(&:to_i) if params[:company_account_id].present? ledger_transactions = LedgerTransaction.left_outer_joins(:ledger_entries, :receipt, :outgoing_payment) .select('ledger_transactions.*') .order(transaction_date: :desc, transaction_number: :desc, ledger_entries: { id: :desc }) ledger_transactions = ledger_transactions.where(transaction_type: params[:transaction_type]) if params[:transaction_type].present? ledger_transactions = ledger_transactions.where(ledger_entries: { reconciled: params[:reconciled] }) if params[:reconciled] == false || params[:reconciled] == true ledger_transactions = ledger_transactions.where(OutgoingPayment[:category].in(params[:payment_type].collect { |pt| pt.downcase.underscore }).or(Receipt[:category].in(params[:payment_type]))) if params[:payment_type].present? if params[:transaction_date_gteq].present? or params[:transaction_date_lteq].present? start_date = params[:transaction_date_gteq].presence || '2000-01-01' end_date = params[:transaction_date_lteq].presence || Time.current.utc.to_date.to_fs(:db) ledger_transactions = ledger_transactions.where(transaction_date: start_date.to_date..end_date.to_date) end if params[:bank_date_gteq].present? or params[:bank_date_lteq].present? bank_start_date = params[:bank_date_gteq].presence || '2000-01-01' bank_end_date = params[:bank_date_lteq].presence || Time.current.utc.to_date.to_fs(:db) ledger_transactions = ledger_transactions.where(ledger_entries: { bank_date: bank_start_date.to_date..bank_end_date.to_date }) end if params[:reconciled_date_gteq].present? or params[:reconciled_date_lteq].present? reconciled_start_date = params[:reconciled_date_gteq].presence || '2000-01-01' reconciled_end_date = params[:reconciled_date_lteq].presence || Time.current.utc.to_date.to_fs(:db) ledger_transactions = ledger_transactions.where(ledger_entries: { reconciled_at: reconciled_start_date.to_date..reconciled_end_date.to_date }) end ledger_transactions = ledger_transactions.where(ledger_entries: { ledger_company_account_id: params[:company_account_id] }) if params[:company_account_id].present? ledger_transactions end |
.process_credit_memo(credit_memo) ⇒ void
This method returns an undefined value.
Post the GL entry for a CreditMemo: per-line return account
by cm_category, taxes, returned-goods/services/freight
discount accounts, with the A/R leg computed as the balancing
amount to absorb sub-cent rounding between stored totals and
summed line items.
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 |
# File 'app/models/ledger_transaction.rb', line 565 def self.process_credit_memo(credit_memo) company = credit_memo.company 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.
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 |
# File 'app/models/ledger_transaction.rb', line 527 def self.process_intracompany_st_delivery(delivery) order = delivery.order consignor = order.from_store.consignee_party consignee = order.to_store.consignee_party company = order.from_store.company to_company = order.to_store.company raise 'Unable to process intracompany ST delivery as from/to companies are different' if company != to_company 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).
335 336 337 338 339 340 341 342 343 |
# File 'app/models/ledger_transaction.rb', line 335 def self.process_invoice(invoice) return if invoice.ledger_transactions.any? if invoice.invoice_type == Invoice::ST LedgerTransaction.process_st_invoice(invoice) else LedgerTransaction.process_so_invoice(invoice) end end |
.process_payment(payment) ⇒ void
This method returns an undefined value.
Post the GL entry for an OutgoingPayment: debit the bank
account and credit the appropriate offset account per item
(voucher → AP, credit-memo / receipt → trade A/R, supplier-
specific override when present).
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 |
# File 'app/models/ledger_transaction.rb', line 743 def self.process_payment(payment) LedgerTransaction.transaction do company = payment.company currency = payment.currency transaction = LedgerTransaction.new(company:, transaction_type: 'PAYMENT', transaction_date: payment.payment_date, currency:, exchange_rate: payment.exchange_rate, outgoing_payment: payment) 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.
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'app/models/ledger_transaction.rb', line 354 def self.process_so_invoice(invoice) company = invoice.company # goods and services account dependent on Store selected if invoice.store && (invoice.store.owner != 'warmlyyours') # it's a consignment order, so use the consignment account 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.
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 |
# File 'app/models/ledger_transaction.rb', line 483 def self.process_st_invoice(invoice) company = invoice.company 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.
244 245 246 247 248 249 250 |
# File 'app/models/ledger_transaction.rb', line 244 def balance balance = BigDecimal(0) ledger_entries.each do |entry| balance += entry.amount unless entry.amount.nil? end balance end |
#balance_inter_company_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.
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 |
# File 'app/models/ledger_transaction.rb', line 820 def balance_inter_company_transaction return unless balance == 0 groups = ledger_entries.group_by do |e| e.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.
807 808 809 810 811 812 813 |
# File 'app/models/ledger_transaction.rb', line 807 def balances_are_zero errors.add(:base, "Balance of amounts must equal 0, but is #{balance}. ENTRIES DETAIL: #{ledger_entries.collect { |le| "#{le.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.
956 957 958 959 960 961 962 |
# File 'app/models/ledger_transaction.rb', line 956 def cat_for_report if outgoing_payment.present? outgoing_payment.category elsif receipt.present? receipt.category end end |
#check_for_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.
864 865 866 867 868 869 870 871 872 873 |
# File 'app/models/ledger_transaction.rb', line 864 def check_for_discrepancies if company_balance != 0 && company_balance.abs <= (BigDecimal('0.05')) # correct the difference but only if it is +/- 0.05 or less ledger_entries.last.company_amount += -company_balance end return unless consolidated_balance != 0 && consolidated_balance.abs <= (BigDecimal('0.05')) # correct the difference but only if it is +/- 0.05 or less ledger_entries.last.consolidated_amount += -consolidated_balance end |
#clear_exchange_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.
881 882 883 884 885 886 887 888 889 890 891 |
# File 'app/models/ledger_transaction.rb', line 881 def clear_exchange_rates return if reversed_transaction_id.present? return unless ledger_entries.any? { |le| le.new_record? || le.changed? } ledger_entries.each do |le| le.consolidated_exchange_rate = nil le.consolidated_amount = nil le.company_exchange_rate = nil le.company_amount = nil end end |
#company ⇒ Company
Validations:
57 |
# File 'app/models/ledger_transaction.rb', line 57 belongs_to :company, optional: true |
#company_balance ⇒ BigDecimal
Sum of company_amount across all entries (the per-Company
functional currency leg).
256 257 258 259 260 261 262 |
# File 'app/models/ledger_transaction.rb', line 256 def company_balance company_balance = BigDecimal(0) ledger_entries.each do |entry| company_balance += entry.company_amount unless entry.company_amount.nil? end company_balance end |
#consolidated_balance ⇒ BigDecimal
Sum of consolidated_amount (USD reporting currency) across all
entries — what shows up in consolidated financials.
268 269 270 271 272 273 274 |
# File 'app/models/ledger_transaction.rb', line 268 def consolidated_balance consolidated_balance = BigDecimal(0) ledger_entries.each do |entry| consolidated_balance += entry.consolidated_amount unless entry.consolidated_amount.nil? end consolidated_balance end |
#credit_memo ⇒ CreditMemo
63 |
# File 'app/models/ledger_transaction.rb', line 63 belongs_to :credit_memo, optional: true |
#delivery ⇒ Delivery
65 |
# File 'app/models/ledger_transaction.rb', line 65 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.
1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 |
# File 'app/models/ledger_transaction.rb', line 1015 def edit_allowed return if company_id.nil? || transaction_type.nil? || transaction_date.nil? return unless changed? && changes.keys.any? { |attr| LOCKED_ATTRIBUTES.include?(attr) } # `companies` is varchar[]; cast company_id (integer) to string so the bound # ARRAY literal has a single element type before PG applies the column cast. lcls = LedgerClosingPeriod.where.overlap(companies: [company_id.to_s, 'ALL']) .where.overlap(transaction_types: ['ALL', transaction_type]) .where(close_to: transaction_date..) return unless lcls.any? errors.add :base, "Period is closed for #{transaction_type} for #{company.short_name} up to #{lcls.first.close_to.to_fs(:crm_default)}." end |
#explanation_for_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.
970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 |
# File 'app/models/ledger_transaction.rb', line 970 def explanation_for_report if outgoing_payment.present? if outgoing_payment.category == 'check' check = outgoing_payment.checks.last check.nil? ? nil : "#{check.check_number} #{check.payee}" else "#{outgoing_payment.remark} #{outgoing_payment.supplier.full_name}" end elsif receipt.present? if receipt.category == 'Credit Card' "#{receipt.card_type} #{receipt.customer.full_name}" else "#{receipt.reference} #{receipt.customer.full_name}" end else description end end |
#has_two_ledger_entries ⇒ void
This method returns an undefined value.
Validation: every JE must have at least the debit + credit pair.
796 797 798 799 800 |
# File 'app/models/ledger_transaction.rb', line 796 def has_two_ledger_entries return unless ledger_entries.length < 2 errors.add(:base, 'At least 2 ledger entries are required.') end |
#invoice ⇒ Invoice
58 |
# File 'app/models/ledger_transaction.rb', line 58 belongs_to :invoice, optional: true |
#item_ledger_entries ⇒ ActiveRecord::Relation<ItemLedgerEntry>
68 |
# File 'app/models/ledger_transaction.rb', line 68 has_many :item_ledger_entries |
#landed_cost ⇒ LandedCost
61 |
# File 'app/models/ledger_transaction.rb', line 61 belongs_to :landed_cost, optional: true |
#ledger_entries ⇒ ActiveRecord::Relation<LedgerEntry>
69 |
# File 'app/models/ledger_transaction.rb', line 69 has_many :ledger_entries, inverse_of: :ledger_transaction, dependent: :destroy |
#outgoing_payment ⇒ OutgoingPayment
64 |
# File 'app/models/ledger_transaction.rb', line 64 belongs_to :outgoing_payment, optional: true |
#receipt ⇒ Receipt
59 |
# File 'app/models/ledger_transaction.rb', line 59 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.
926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 |
# File 'app/models/ledger_transaction.rb', line 926 def resource_ref if invoice.present? invoice.reference_number elsif receipt.present? receipt.id elsif shipment_receipt.present? shipment_receipt_id elsif landed_cost.present? landed_cost_id elsif voucher.present? voucher.reference_number elsif credit_memo.present? credit_memo.reference_number elsif outgoing_payment.present? outgoing_payment.reference_number elsif item_ledger_entries.any? refs = [] item_ledger_entries.each do |entry| refs << entry.id end refs.join(' ') else id end end |
#reverse(reversal_date) ⇒ LedgerTransaction
Build, save, and return a reversing JE that nets this
transaction to zero on reversal_date. Each entry is duplicated
with negated amounts and reconciled cleared; the new
transaction's reversed_transaction_id points back at self.
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'app/models/ledger_transaction.rb', line 283 def reverse(reversal_date) tran = dup tran.description = description.present? ? (description + ' - REVERSAL') : 'REVERSAL' tran.transaction_number = nil tran.created_at = nil tran.updated_at = nil tran.creator_id = nil tran.updater_id = nil tran.transaction_date = reversal_date tran.reversed_transaction_id = id ledger_entries.each do |le| new_le = le.dup new_le.ledger_transaction_id = nil new_le.created_at = nil new_le.updated_at = nil new_le.creator_id = nil new_le.updater_id = nil new_le.amount = -le.amount new_le.company_amount = -le.company_amount new_le.consolidated_amount = -le.consolidated_amount new_le.reconciled = false tran.ledger_entries << new_le end tran.save! tran end |
#reversed_transaction ⇒ LedgerTransaction
66 |
# File 'app/models/ledger_transaction.rb', line 66 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).
904 905 906 907 908 |
# File 'app/models/ledger_transaction.rb', line 904 def set_company_amount return if reversed_transaction_id.present? ledger_entries.each(&:set_company_amount) end |
#set_company_and_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.
316 317 318 319 320 321 322 323 324 325 326 |
# File 'app/models/ledger_transaction.rb', line 316 def set_company_and_override company_ids = [] ledger_entries.each do |e| company_ids << e.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.
914 915 916 917 918 |
# File 'app/models/ledger_transaction.rb', line 914 def set_consolidated_amount return if reversed_transaction_id.present? ledger_entries.each(&:set_consolidated_amount) end |
#set_currency ⇒ void
This method returns an undefined value.
Cascade the transaction currency onto every entry.
895 896 897 |
# File 'app/models/ledger_transaction.rb', line 895 def set_currency ledger_entries.each(&:set_currency) end |
#set_supplier_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.
1034 1035 1036 1037 |
# File 'app/models/ledger_transaction.rb', line 1034 def set_supplier_id doc = voucher || outgoing_payment self.supplier_id = doc.supplier_id if doc.present? end |
#set_transaction_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.
785 786 787 788 789 790 791 |
# File 'app/models/ledger_transaction.rb', line 785 def set_transaction_number return unless (!do_not_set_transaction_number == true) && transaction_number.blank? # getting number from a shared sequence now seq = LedgerTransaction.find_by_sql("SELECT nextval('transaction_numbers_seq') AS transaction_number") self.transaction_number = seq[0].transaction_number.to_s end |
#shipment_receipt ⇒ ShipmentReceipt
60 |
# File 'app/models/ledger_transaction.rb', line 60 belongs_to :shipment_receipt, optional: true |
#to_s ⇒ String
114 115 116 |
# File 'app/models/ledger_transaction.rb', line 114 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.
994 995 996 997 998 |
# File 'app/models/ledger_transaction.rb', line 994 def transaction_date_not_before_original return unless transaction_date < reversed_transaction.transaction_date errors.add(:transaction_date, 'cannot be before the original transaction date') end |
#transaction_reversal ⇒ LedgerTransaction
67 |
# File 'app/models/ledger_transaction.rb', line 67 has_one :transaction_reversal, class_name: 'LedgerTransaction', foreign_key: 'reversed_transaction_id' |
#voucher ⇒ Voucher
62 |
# File 'app/models/ledger_transaction.rb', line 62 belongs_to :voucher, optional: true |