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 =
%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 =
{ '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 =
{ '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
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) ⇒ Object
Legacy alias for backwards compatibility.
- .build_from_spreadsheet(file_path, original_filename: nil, encoding: nil) ⇒ Object
- .each_csv_row(file_path, encoding: nil) ⇒ Object
- .each_spreadsheet_row(file_path, original_filename: nil, encoding: nil, &block) ⇒ Object
- .each_xlsx_row(file_path) ⇒ Object
- .filter(params) ⇒ Object
- .process_credit_memo(credit_memo) ⇒ Object
- .process_intracompany_st_delivery(delivery) ⇒ Object
- .process_invoice(invoice) ⇒ Object
- .process_payment(payment) ⇒ Object
- .process_so_invoice(invoice) ⇒ Object
- .process_st_invoice(invoice) ⇒ Object
Instance Method Summary collapse
- #balance ⇒ Object
- #balance_inter_company_transaction ⇒ Object
- #balances_are_zero ⇒ Object
- #cat_for_report ⇒ Object
- #check_for_discrepancies ⇒ Object
- #clear_exchange_rates ⇒ Object
- #company_balance ⇒ Object
- #consolidated_balance ⇒ Object
- #edit_allowed ⇒ Object
- #explanation_for_report ⇒ Object
- #has_two_ledger_entries ⇒ Object
- #resource_ref ⇒ Object
- #reverse(reversal_date) ⇒ Object
- #set_company_amount ⇒ Object
- #set_company_and_override ⇒ Object
- #set_consolidated_amount ⇒ Object
- #set_currency ⇒ Object
- #set_supplier_id ⇒ Object
- #set_transaction_number ⇒ Object
- #to_s ⇒ Object
- #transaction_date_not_before_original ⇒ Object
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::EventPublishable
Instance Attribute Details
#currency ⇒ Object (readonly)
75 |
# File 'app/models/ledger_transaction.rb', line 75 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.
51 52 53 |
# File 'app/models/ledger_transaction.rb', line 51 def do_not_set_transaction_number @do_not_set_transaction_number end |
#override ⇒ Object
Returns the value of attribute override.
51 52 53 |
# File 'app/models/ledger_transaction.rb', line 51 def override @override end |
#transaction_date ⇒ Object (readonly)
75 |
# File 'app/models/ledger_transaction.rb', line 75 validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true |
#transaction_number ⇒ Object (readonly)
75 |
# File 'app/models/ledger_transaction.rb', line 75 validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true |
#transaction_type ⇒ Object (readonly)
75 |
# File 'app/models/ledger_transaction.rb', line 75 validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true |
Class Method Details
.build_from_csv(file_path) ⇒ Object
Legacy alias for backwards compatibility
135 136 137 |
# File 'app/models/ledger_transaction.rb', line 135 def self.build_from_csv(file_path) build_from_spreadsheet(file_path) end |
.build_from_spreadsheet(file_path, original_filename: nil, encoding: nil) ⇒ Object
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'app/models/ledger_transaction.rb', line 110 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) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'app/models/ledger_transaction.rb', line 166 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, &block) ⇒ Object
139 140 141 142 143 144 145 146 147 |
# File 'app/models/ledger_transaction.rb', line 139 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) ⇒ Object
149 150 151 152 153 154 155 156 157 158 |
# File 'app/models/ledger_transaction.rb', line 149 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 end |
.filter(params) ⇒ Object
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 |
# File 'app/models/ledger_transaction.rb', line 576 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.joins('left join ledger_entries on ledger_entries.ledger_transaction_id = ledger_transactions.id') .joins('left join receipts on ledger_transactions.receipt_id = receipts.id') .joins('left join outgoing_payments on ledger_transactions.outgoing_payment_id = outgoing_payments.id') .select('ledger_transactions.*') .order('ledger_transactions.transaction_date desc, ledger_transactions.transaction_number desc, ledger_entries.id desc') ledger_transactions = ledger_transactions.where('ledger_transactions.transaction_type in (?)', 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('outgoing_payments.category in (?) or receipts.category in (?)', params[:payment_type].collect { |pt| pt.downcase.underscore }, 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('ledger_transactions.transaction_date between ? and ?', 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 between ? and ?', 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 between ? and ?', reconciled_start_date.to_date, reconciled_end_date.to_date) end ledger_transactions = ledger_transactions.where('ledger_entries.ledger_company_account_id in (?)', params[:company_account_id]) if params[:company_account_id].present? ledger_transactions end |
.process_credit_memo(credit_memo) ⇒ Object
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 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 518 519 520 521 522 523 524 525 526 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 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 |
# File 'app/models/ledger_transaction.rb', line 444 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) ⇒ Object
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 |
# File 'app/models/ledger_transaction.rb', line 415 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) ⇒ Object
249 250 251 252 253 254 255 |
# File 'app/models/ledger_transaction.rb', line 249 def self.process_invoice(invoice) if invoice.invoice_type == Invoice::ST LedgerTransaction.process_st_invoice(invoice) else LedgerTransaction.process_so_invoice(invoice) end end |
.process_payment(payment) ⇒ Object
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 |
# File 'app/models/ledger_transaction.rb', line 608 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) ⇒ Object
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 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 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 |
# File 'app/models/ledger_transaction.rb', line 257 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) ⇒ Object
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 |
# File 'app/models/ledger_transaction.rb', line 379 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 ⇒ Object
186 187 188 189 190 191 192 |
# File 'app/models/ledger_transaction.rb', line 186 def balance balance = BigDecimal(0) ledger_entries.each do |entry| balance += entry.amount unless entry.amount.nil? end balance end |
#balance_inter_company_transaction ⇒ Object
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 |
# File 'app/models/ledger_transaction.rb', line 666 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 ⇒ Object
658 659 660 661 662 663 664 |
# File 'app/models/ledger_transaction.rb', line 658 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 ⇒ Object
770 771 772 773 774 775 776 |
# File 'app/models/ledger_transaction.rb', line 770 def cat_for_report if outgoing_payment.present? outgoing_payment.category elsif receipt.present? receipt.category end end |
#check_for_discrepancies ⇒ Object
705 706 707 708 709 710 711 712 713 714 |
# File 'app/models/ledger_transaction.rb', line 705 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 ⇒ Object
716 717 718 719 720 721 722 723 724 725 726 |
# File 'app/models/ledger_transaction.rb', line 716 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:
53 |
# File 'app/models/ledger_transaction.rb', line 53 belongs_to :company, optional: true |
#company_balance ⇒ Object
194 195 196 197 198 199 200 |
# File 'app/models/ledger_transaction.rb', line 194 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 ⇒ Object
202 203 204 205 206 207 208 |
# File 'app/models/ledger_transaction.rb', line 202 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
59 |
# File 'app/models/ledger_transaction.rb', line 59 belongs_to :credit_memo, optional: true |
#delivery ⇒ Delivery
61 |
# File 'app/models/ledger_transaction.rb', line 61 belongs_to :delivery, optional: true |
#edit_allowed ⇒ Object
803 804 805 806 807 808 809 810 811 812 813 814 |
# File 'app/models/ledger_transaction.rb', line 803 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) } arel = LedgerClosingPeriod.arel_table lcls = LedgerClosingPeriod.where(arel[:companies].overlap([company_id, 'ALL'])) lcls = lcls.where(arel[:transaction_types].overlap(['ALL', transaction_type])) lcls = lcls.where('close_to >= ?', transaction_date.to_fs(:db)) 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 ⇒ Object
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 |
# File 'app/models/ledger_transaction.rb', line 778 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 ⇒ Object
652 653 654 655 656 |
# File 'app/models/ledger_transaction.rb', line 652 def has_two_ledger_entries return unless ledger_entries.length < 2 errors.add(:base, 'At least 2 ledger entries are required.') end |
#invoice ⇒ Invoice
54 |
# File 'app/models/ledger_transaction.rb', line 54 belongs_to :invoice, optional: true |
#item_ledger_entries ⇒ ActiveRecord::Relation<ItemLedgerEntry>
64 |
# File 'app/models/ledger_transaction.rb', line 64 has_many :item_ledger_entries |
#landed_cost ⇒ LandedCost
57 |
# File 'app/models/ledger_transaction.rb', line 57 belongs_to :landed_cost, optional: true |
#ledger_entries ⇒ ActiveRecord::Relation<LedgerEntry>
65 |
# File 'app/models/ledger_transaction.rb', line 65 has_many :ledger_entries, inverse_of: :ledger_transaction, dependent: :destroy |
#outgoing_payment ⇒ OutgoingPayment
60 |
# File 'app/models/ledger_transaction.rb', line 60 belongs_to :outgoing_payment, optional: true |
#receipt ⇒ Receipt
55 |
# File 'app/models/ledger_transaction.rb', line 55 belongs_to :receipt, optional: true |
#resource_ref ⇒ Object
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 |
# File 'app/models/ledger_transaction.rb', line 744 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) ⇒ Object
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'app/models/ledger_transaction.rb', line 210 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
62 |
# File 'app/models/ledger_transaction.rb', line 62 belongs_to :reversed_transaction, class_name: 'LedgerTransaction', optional: true |
#set_company_amount ⇒ Object
732 733 734 735 736 |
# File 'app/models/ledger_transaction.rb', line 732 def set_company_amount return if reversed_transaction_id.present? ledger_entries.each(&:set_company_amount) end |
#set_company_and_override ⇒ Object
237 238 239 240 241 242 243 244 245 246 247 |
# File 'app/models/ledger_transaction.rb', line 237 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 ⇒ Object
738 739 740 741 742 |
# File 'app/models/ledger_transaction.rb', line 738 def set_consolidated_amount return if reversed_transaction_id.present? ledger_entries.each(&:set_consolidated_amount) end |
#set_currency ⇒ Object
728 729 730 |
# File 'app/models/ledger_transaction.rb', line 728 def set_currency ledger_entries.each(&:set_currency) end |
#set_supplier_id ⇒ Object
816 817 818 819 |
# File 'app/models/ledger_transaction.rb', line 816 def set_supplier_id doc = voucher || outgoing_payment self.supplier_id = doc.supplier_id if doc.present? end |
#set_transaction_number ⇒ Object
644 645 646 647 648 649 650 |
# File 'app/models/ledger_transaction.rb', line 644 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
56 |
# File 'app/models/ledger_transaction.rb', line 56 belongs_to :shipment_receipt, optional: true |
#to_s ⇒ Object
106 107 108 |
# File 'app/models/ledger_transaction.rb', line 106 def to_s "Ledger Transaction ##{transaction_number}" end |
#transaction_date_not_before_original ⇒ Object
797 798 799 800 801 |
# File 'app/models/ledger_transaction.rb', line 797 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
63 |
# File 'app/models/ledger_transaction.rb', line 63 has_one :transaction_reversal, class_name: 'LedgerTransaction', foreign_key: 'reversed_transaction_id' |
#voucher ⇒ Voucher
58 |
# File 'app/models/ledger_transaction.rb', line 58 belongs_to :voucher, optional: true |