module Skr # A transaction is a record of a business event that has financial consequences. # It consists of an at least one credit and at least one debit # Transactions can be nested, with each level compacting all the entries that were made on it # # require 'skr/core' # customer = Customer.find_by_code "MONEYBAGS" # GlTransaction.record( source: invoice, description: "Invoice Example" ) do | transaction | # transaction.location = Location.default # <- could also specify in record's options # Sku.where( code: ['HAT','STRING'] ).each do | sku | # transaction.add_posting( amount: sku.default_price, # debit: sku.gl_asset_account, # credit: customer.gl_receivables_account ) # end # end # class GlTransaction < Skr::Model is_immutable # A Transaction must refer to another record, # such as Invoice, Inventory Adjustment, or a Manual Posting. belongs_to :source, :polymorphic=>true # Each transaction belongs to an accounting period belongs_to :period, :class_name=>'Skr::GlPeriod', export: true has_many :postings, class_name: 'Skr::GlPosting', inverse_of: :gl_transaction, export: { writable: true } has_many :credits, ->{ where({ is_debit: false }) }, class_name: 'Skr::GlPosting', extend: Concerns::GlTran::Postings, inverse_of: :gl_transaction, export: { writable: true } # Must equal credits, checked by the {#ensure_postings_correct} validation has_many :debits, ->{ where({ is_debit: true }) }, class_name: 'Skr::GlPosting', extend: Concerns::GlTran::Postings, inverse_of: :gl_transaction, export: { writable: true } before_validation :set_defaults validate :ensure_postings_correct validates :source, :period, :set=>true validates :description, :presence=>true scope :with_details, lambda { | acct = nil | query = compose_query_using_detail_view(view: 'skr_gl_transaction_details', join_to: :gl_transaction_id) acct.blank? ? query : query.joins(:postings).merge( GlPosting.matching(acct) ) }, export: true # Add a debit/credit pair to the transaction with amount # @param amount [BigDecimal] the amount to apply to each posting # @param debit [GlAccount] # @param credit [GlAccount] def add_posting( amount: nil, debit: nil, credit: nil ) Lanes.logger.debug "GlTransaction add_posting #{debit} : #{credit}" self.credits.build( location: @location, is_debit: false, account: credit, amount: amount ) self.debits.build( location: @location, is_debit: true, account: debit, amount: amount * -1 ) end # Passes the location onto the postings. # @param location [Location] def location=(location) @location = location each_posting do | posting | posting.location = location end end # @yield [GlPosting] each posting associated with the Transaction def each_posting self.credits.each{ |posting| yield posting } self.debits.each{ |posting| yield posting } end # @return [GlTransaction] the current transaction that's in progress def self.current glt = Thread.current[:gl_transaction] glt ? glt.last : nil end # Start a new nested GlTransaction # When a transaction is created, it can have # @return [GlTransaction] new transaction # @yield [GlTransaction] new transaction def self.record( attributes={} ) Thread.current[:gl_transaction] ||= [] glt = GlTransaction.new( attributes ) Thread.current[:gl_transaction].push( glt ) Lanes.logger.debug "B4 GlTransaction" results = yield glt Thread.current[:gl_transaction].pop if results if results.is_a?(Hash) && results.has_key?(:attributes) glt.assign_attributes( results[:attributes] ) end glt._save_recorded Lanes.logger.debug "AF GlTransaction new=#{glt.new_record?} #{glt.errors.full_messages}" end return glt end # @param owner [Skr::Model] # @param amount [BigDecimal] # @param debit [GlAccount] # @param credit [GlAccount] # @param options [Hash] options to pass to the [GlTransaction] if one is created def self.push_or_save( owner: nil, amount: nil, debit:nil, credit:nil, options:{} ) if (glt = self.current) # we push glt.add_posting( amount: amount, debit: debit, credit: credit ) else options.merge!(source: owner, location: options[:location] || owner.location) glt = GlTransaction.new( options ) glt.add_posting( amount: amount, debit: debit, credit: credit ) glt.save! end glt end # @private def _save_recorded compact( 'debits' ) compact( 'credits' ) self.save if self.credits.any? || self.debits.any? self end private def compact( assoc_name ) accounts = self.send( assoc_name ).to_a self.send( assoc_name + "=", [] ) account_numbers = accounts.group_by{ |posting| posting.account_number } account_numbers.each do | number, matching | amount = matching.sum(&:amount) self.send( assoc_name ).build({ account_number: number, is_debit: ( assoc_name == "debits" ), amount: amount, }) end end def ensure_postings_correct if debits.total != ( -1 * credits.total ) self.errors.add(:credits, "must equal debits") self.errors.add(:debits, "must equal credits") return false end true end def set_defaults self.period ||= GlPeriod.current end end end