module Skr # Invoices constitute a demand for payment for goods that have been delivered to a {Customer}. # A invoice contains: # # * Customer contact information # * An inventory location that goods will be taken from. # * The customer provided {PurchaseOrder} Number # * The Payment Terms that were extended. This will control how much time the Customer has to pay the invoice in full. # * One or more SKUs, the quantity desired for each and the selling price for them. # # While an {Invoice} often originates with a {SalesOrder}, it does not have to. # Sales that take place in a retail environment where the customer selects # the goods and pays for them immediately do not require a sales order record. # # Once an invoice is saved, it immediately removes the SKUs from the {SkuLoc} # and generates corresponding General Ledger entries debiting the asset account # and crediting the customers receivables account. # # When payment is received against the Invoice, # the receivables account is debited and the payments holding account is credited. # # invoice = Invoice.new( customer: Customer.find_by_code("ACME") # invoice.lines.build({ sku: Sku.find_by_code('LABOR'), qty: 1, price: 8.27 }) # invoice.save class Invoice < Skr::Model has_visible_id has_random_hash_code has_gl_transaction is_order_like belongs_to :sales_order, export: true belongs_to :customer_project, export: true belongs_to :customer, export: true belongs_to :location, export: true belongs_to :pick_ticket, inverse_of: :invoice, export: true belongs_to :terms, class_name: 'Skr::PaymentTerm', export: true belongs_to :billing_address, class_name: 'Skr::Address', export: { writable: true } belongs_to :shipping_address, class_name: 'Skr::Address', export: { writable: true } has_many :gl_transactions, :as=>:source has_many :lines, -> { order(:position) }, class_name: 'Skr::InvLine', inverse_of: :invoice, extend: Concerns::INV::Lines, export: { writable: true } has_many :payments, inverse_of: :invoice, extend: Concerns::INV::Payments, listen: { save: :apply_payment }, export: { writable: true } before_validation :set_defaults, on: :create validates :customer, :location, set: true validate :ensure_unlocked, :ensure_location_matches_so # used to update customer account balances when invoices created / paid scope :open_for_customer, lambda{ | customer | where(state: :open, customer_id: customer.is_a?(Customer) ? customer.id : customer) }, export: true scope :with_sku_id, lambda { | sku_id | joins("join (select skr_sku_inv_xref.invoice_id as invoice_id from skr_sku_inv_xref " + "where sku_id=#{sku_id.to_i} group by skr_sku_inv_xref.invoice_id) as sku_inv " + "on sku_inv.invoice_id = skr_invoices.id") }, export: true scope :with_details, lambda { |should_use=true | compose_query_using_detail_view( view: 'skr_inv_details', join_to: 'invoice_id' ) }, export: true enum state: { open: 1, paid: 5, partialy_paid: 10 } state_machine do state :open, initial: true state :paid state :partialy_paid event :mark_paid do transitions from: [:open,:partialy_paid], to: :paid end event :mark_partialy_paid do transitions from: :open, to: :partialy_paid end end def initialize(attributes = {}) super # date must be set, otherwise things like terms that are based off of it fail self.invoice_date ||= Date.today end # @return [BigDecimal] total - amount_paid def unpaid_amount self.total - self.payments.total end # @return [Boolean] is the invoice paid in full def fully_paid? unpaid_amount <= 0 end # @return [DateTime] when the invoice is due def due_date self.terms.due_date_from(invoice_date) end def is_locked? GlPeriod.is_date_locked?(self.invoice_date) end private # set the state if the amount_paid was changed def apply_payment(pymnt) if self.fully_paid? && self.may_mark_paid? self.mark_paid! elsif self.payments.total > 0 && self.may_mark_partialy_paid? Lanes.logger_debug('paying') #self.mark_partialy_paid! Lanes.logger_debug('paid') end end def set_defaults if pick_ticket self.location = pick_ticket.location self.sales_order = pick_ticket.sales_order end if sales_order self.terms ||= sales_order.terms self.form ||= sales_order.form self.customer = sales_order.customer self.po_num = sales_order.po_num if self.po_num.blank? self.billing_address = sales_order.billing_address if self.billing_address.blank? self.shipping_address = sales_order.shipping_address if self.shipping_address.blank? if self.options && sales_order.options self.options.merge!(sales_order.options) else self.options = sales_order.options end end if customer_project self.customer = customer_project.customer self.po_num = customer_project.po_num if self.po_num.blank? end if customer self.form ||= customer.get_form('invoice') self.billing_address = customer.billing_address if self.billing_address.blank? self.shipping_address = customer.shipping_address if self.shipping_address.blank? end end def ensure_unlocked if is_locked? self.errors.add(:invoice_date, "falls on a locked GL Period") end end def ensure_location_matches_so if sales_order && location != sales_order.location self.errors.add(:location, "#{location.code} must match location that order was taken on (#{sales_order.location.code})") end end end end # Skr module