lib/invoicing/ledger_item.rb
require "active_support/concern"
require "invoicing/ledger_item/render_html"
require "invoicing/ledger_item/render_ubl"
module Invoicing
# = Ledger item objects
#
# This module implements a simple ledger, i.e. the record of all of the business transactions
# which are handled by your application. Each transaction is recorded as one +LedgerItem+ object,
# each of which may have one of the following three types:
#
# +Invoice+::
# When you send an invoice to someone (= a customer), this is a record of the fact
# that you have sold them something (a product, a service etc.), and how much you expect to be
# paid for it. An invoice can consist of a list of individual charges, but it is considered as
# one document for legal purposes. You can also create invoices from someone else to yourself,
# if you owe someone else money -- for example, if you need to pay commissions to a reseller of
# your application.
# +CreditNote+::
# This is basically a invoice for a negative amount; you should use it if you have previously
# sent a customer an invoice with an amount which was too great (i.e. you have overcharged them).
# The numeric values stored in the database for a credit note are negative, to make it easier to
# calculate account summaries, but they may be formatted as positive values when presented to
# users if that is customary in your country. For example, if you send a customer an invoice
# with a +total_amount+ of $20 and a credit note with a +total_amount+ of -$10, that means that
# overall you're asking them to pay $10.
# +Payment+::
# This is a record of the fact that a payment has been made. It's a simple object, in effect just
# saying that party A paid amount X to party B on date Y. This module does not implement any
# particular payment mechanism such as credit card handling, although it could be implemented on
# top of a +Payment+ object.
#
# == Important principles
#
# Note the distinction between Invoices/Credit Notes and Payments; to keep your accounts clean,
# it is important that you do not muddle these up.
#
# * <b>Invoices and Credit Notes</b> are the important documents for tax purposes in most
# jurisdictions. They record the date on which the sale is officially made, and that date
# determines which tax rates apply. An invoice often also represents the transfer of ownership from
# the supplier to the customer; for example, if you ask your customers to send payment in
# advance (such as 'topping up' their account), that money still belongs to your customer
# until the point where they have used your service, and you have charged them for your
# service by sending them an invoice. You should only invoice them for what they have actually
# used, then the remaining balance will automatically be retained on their account.
# * <b>Payments</b> are just what it says on the tin -- the transfer of money from one hand
# to another. A payment may occur before an invoice is issued (payment in advance), or
# after/at the same time as an invoice is issued to settle the debt (payment in arrears, giving
# your customers credit). You can choose whatever makes sense for your business.
# Payments may often be associated one-to-one with invoices, but not necessarily -- an invoice
# may be paid in instalments, or several invoices may be lumped together to one payment. Your
# customer may even refuse to pay some charges, in which case there is an invoice but no payment
# (until at some point you either reverse it with a credit note, or write it off as bad debt,
# but that's beyond our scope right now).
#
# Another very important principle is that once a piece of information has been added to the
# ledger, you <b>should not modify or delete it</b>. Particularly when you have 'sent' one of your
# customers or suppliers a document (which may mean simply that they have seen it on the web) you should
# not change it again, because they might have added that information to their own accounting system.
# Changing any information is guaranteed to lead to confusion. (The model objects do not restrict your
# editing capabilities because they might be necessary in specific circumstances, but you should be
# extremely careful when changing anything.)
#
# Of course you make mistakes or change your mind, but please deal with them cleanly:
# * If you create an invoice whose value is too small, don't amend the invoice, but send them
# another invoice to cover the remaining amount.
# * If you create an invoice whose value is too great (for example because you want to offer one
# customer a special discount), don't amend the invoice, but send them a credit note to waive
# your claim to the difference.
# * If you create a payment, mark it as +pending+ until it the money has actually arrived.
# If it never arrives, keep the record but mark it as +failed+ in case you need to investigate
# it later.
#
# The exception to the 'no modifications' rule are invoices on which you accumulate charges
# (e.g. over the course of a month)
# and then officially 'send' the invoice at the end of the period. In this gem we call such
# invoices +open+ while they may still be changed. It's ok to add charges to +open+ invoices
# as you go along; while it is +open+ it is not legally an invoice, but only a statement
# of accumulated charges. If you display it to users, make sure that you don't call it "invoice",
# to avoid confusion. Only when you set it to +closed+ at the end of the month does the
# statement become an invoice for legal purposes. Once it's +closed+ you must not add
# any further charges to it.
#
# Finally, each ledger item has a sender and a recipient; typically one of the two will be
# <b>you</b> (the person/organsation who owns/operates the application):
# * For invoices, credit notes and payments between you and your customers, set the sender
# to be yourself and the recipient to be your customer;
# * If you use this system to record suppliers, set the sender to be your supplier and the
# recipient to be yourself.
# (See below for details.) It is perfectly ok to have documents which are sent between your
# users, where you are neither sender nor recipient; this may be useful if you want to allow
# users to trade directly with each other.
#
# == Using invoices, credit notes and payments in your application
#
# All invoices, credit notes and payments (collectively called 'ledger items') are stored in a
# single database table. We use <b>single table inheritance</b> to distinguish the object types.
# You need to create at least the following four model classes in your application:
#
# class LedgerItem < ActiveRecord::Base
# acts_as_ledger_item
# end
#
# class Invoice < LedgerItem # Base class for all types of invoice
# acts_as_ledger_item :subtype => :invoice
# end
#
# class CreditNote < LedgerItem # Base class for all types of credit note
# acts_as_ledger_item :subtype => :credit_note
# end
#
# class Payment < LedgerItem # Base class for all types of payment
# acts_as_ledger_item :subtype => :payment
# end
#
# You may give the classes different names than these, and you can package them in modules if
# you wish, but they need to have the <tt>:subtype => ...</tt> option parameters as above.
#
# You can create as many subclasses as you like of each of Invoice, CreditNote and Payment. This
# provides a convenient mechanism for encapsulating different types of functionality which you
# may need for different types of transactions, but still keeping the accounts in one place. You
# may start with only one subclass of +Invoice+ (e.g. <tt>class MonthlyChargesInvoice < Invoice</tt>
# to bill users for their use of your application; but as you want to do more clever things, you
# can add other subclasses of +Invoice+ as and when you need them (such as +ConsultancyServicesInvoice+
# and +SalesCommissionInvoice+, for example). Similarly for payments, you may have subclasses
# representing credit card payments, cash payments, bank transfers etc.
#
# Please note that the +Payment+ ledger item type does not itself implement any particular
# payment methods such as credit card handling; however, for third-party libraries providing
# credit card handling, this would be a good place to integrate.
#
# The model classes must have a certain minimum set of columns and a few common methods, documented
# below (although you may rename any of them if you wish). Beyond those, you may add other methods and
# database columns for your application's own needs, provided they don't interfere with names used here.
#
# == Required methods/database columns
#
# The following methods/database columns are <b>required</b> for +LedgerItem+ objects (you may give them
# different names, but then you need to tell +acts_as_ledger_item+ about your custom names):
#
# +type+::
# String to store the class name, for ActiveRecord single table inheritance.
#
# +sender_id+::
# Integer-valued foreign key, used to refer to some other model object representing the party
# (person, company etc.) who is the sender of the transaction.
# - In the case of an invoice or credit note, the +sender_id+ identifies the supplier of the product or service,
# i.e. the person who is owed the amount specified on the invoice, also known as the creditor.
# - In the case of a payment record, the +sender_id+ identifies the payee, i.e. the person who sends the note
# confirming that they received payment.
# - This field may be +NULL+ to refer to yourself (i.e. the company/person who owns or
# operates this application), but you may also use non-+NULL+ values to refer to yourself. It's just
# important that you consistently refer to the same party by the same value in different ledger items.
#
# +recipient_id+::
# The counterpart to +sender_id+: foreign key to a model object which represents the
# party who is the recipient of the transaction.
# - In the case of an invoice or credit note, the +recipient_id+ identifies the customer/buyer of the product or
# service, i.e. the person who owes the amount specified on the invoice, also known as the debtor.
# - In the case of a payment record, the +recipient_id+ identifies the payer, i.e. the recipient of the
# payment receipt.
# - +NULL+ may be used as in +sender_id+.
#
# +sender_details+::
# A method (does not have to be a database column) which returns a hash with information
# about the party identified by +sender_id+. See the documentation of +sender_details+ for the expected
# contents of the hash. Must always return valid details, even if +sender_id+ is +NULL+.
#
# +recipient_details+::
# A method (does not have to be a database column) which returns a hash with information
# about the party identified by +recipient_id+. See the documentation of +sender_details+ for the expected
# contents of the hash (+recipient_details+ uses the same format as +sender_details+). Must always
# return valid details, even if +recipient_id+ is +NULL+.
#
# +identifier+::
# A number or string used to identify this record, i.e. the invoice number, credit note number or
# payment receipt number as appropriate.
# - There may be legal requirements in your country concerning its format, but as long as it uniquely identifies
# the document within your organisation you should be safe.
# - It's possible to simply make this an alias of the primary key, but it's strongly recommended that you use a
# separate database column. If you ever need to generate invoices on behalf of other people (i.e. where
# +sender_id+ is not you), you need to give the sender of the invoice the opportunity to enter their own
# +identifier+ (because it then must be unique within the sender's organisation, not yours).
#
# +issue_date+::
# A datetime column which indicates the date on which the document is issued, and which may also
# serve as the tax point (the date which determines which tax rate is applied). This should be a separate
# column, because it won't necessarily be the same as +created_at+ or +updated_at+. There may be business
# reasons for choosing particular dates, but the date at which you send the invoice or receive the payment
# should do unless your accountant advises you otherwise.
#
# +currency+::
# The 3-letter code which identifies the currency used in this transaction; must be one of the list
# of codes in ISO-4217[http://en.wikipedia.org/wiki/ISO_4217]. (Even if you only use one currency throughout
# your site, this is needed to format monetary amounts correctly.)
#
# +total_amount+::
# A decimal column containing the grand total monetary sum (of the invoice or credit note), or the monetary
# amount paid (of the payment record), including all taxes, charges etc. For invoices and credit notes, a
# +before_validation+ filter is automatically invoked, which adds up the +net_amount+ and +tax_amount+ values
# of all line items and assigns that sum to +total_amount+. For payment records, which do not usually have
# line items, you must assign the correct value to this column. See the documentation of the +CurrencyValue+
# module for notes on suitable datatypes for monetary values. +acts_as_currency_value+ is automatically applied
# to this attribute.
#
# +tax_amount+::
# If you're a small business you maybe don't need to add tax to your invoices; but if you are successful,
# you almost certainly will need to do so eventually. In most countries this takes the form of Value Added
# Tax (VAT) or Sales Tax. For invoices and credit notes, you must store the amount of tax in this table;
# a +before_validation+ filter is automatically invoked, which adds up the +tax_amount+ values of all
# line items and assigns that sum to +total_amount+. For payment records this should be zero (unless you
# use a cash accounting scheme, which is currently not supported). See the documentation of the
# +CurrencyValue+ module for notes on suitable datatypes for monetary values. +acts_as_currency_value+ is
# automatically applied to this attribute.
#
# +status+::
# A string column used to keep track of the status of ledger items. Currently the following values are defined
# (but future versions may add further +status+ values):
# +open+:: For invoices/credit notes: the document is not yet finalised, further line items may be added.
# +closed+:: For invoices/credit notes: the document has been sent to the recipient and will not be changed again.
# +cancelled+:: For invoices/credit notes: the document has been declared void and does not count towards accounts.
# (Use this sparingly; if you want to refund an invoice that has been sent, send a credit note.)
# +pending+:: For payments: payment is expected or has been sent, but has not yet been confirmed as received.
# +cleared+:: For payments: payment has completed successfully.
# +failed+:: For payments: payment did not succeed; this record is not counted towards accounts.
#
# +description+::
# A method which returns a short string describing what this invoice, credit note or payment is about.
# Can be a database column but doesn't have to be.
#
# +line_items+::
# You should define an association <tt>has_many :line_items, ...</tt> referring to the +LineItem+ objects
# associated with this ledger item.
#
#
# == Optional methods/database columns
#
# The following methods/database columns are <b>optional, but recommended</b> for +LedgerItem+ objects:
#
# +period_start+, +period_end+::
# Two datetime columns which define the period of time covered by an invoice or credit note. If the thing you
# are selling is a one-off, you can omit these columns or leave them as +NULL+. However, if there is any sort
# of duration associated with an invoice/credit note (e.g. charges incurred during a particular month, or
# an annual subscription, or a validity period of a license, etc.), please store that period here. It's
# important for accounting purposes. (For +Payment+ objects it usually makes most sense to just leave these
# as +NULL+.)
#
# +uuid+::
# A Universally Unique Identifier (UUID)[http://en.wikipedia.org/wiki/UUID] string for this invoice, credit
# note or payment. It may seem unnecessary now, but may help you to keep track of your data later on as
# your system grows. If you have the +uuid+ gem installed and this column is present, a UUID is automatically
# generated when you create a new ledger item.
#
# +due_date+::
# The date at which the invoice or credit note is due for payment. +nil+ on +Payment+ records.
#
# +created_at+, +updated_at+::
# The standard ActiveRecord datetime columns for recording when an object was created and last changed.
# The values are not directly used at the moment, but it's useful information in case you need to track down
# a particular transaction sometime; and ActiveRecord manages them for you anyway.
#
#
# == Generated methods
#
# In return for providing +LedgerItem+ with all the required information as documented above, you are given
# a number of class and instance methods which you will find useful sooner or later. In addition to those
# documented in this module (instance methods) and <tt>Invoicing::LedgerItem::ClassMethods</tt>
# (class methods), the following methods are generated dynamically:
#
# +sent_by+:: Named scope which takes a person/company ID and matches all ledger items whose
# +sender_id+ matches that value.
# +received_by+:: Named scope which takes a person/company ID and matches all ledger items whose
# +recipient_id+ matches that value.
# +sent_or_received_by+:: Union of +sent_by+ and +received_by+.
# +in_effect+:: Named scope which matches all closed invoices/credit notes (not open or cancelled)
# and all cleared payments (not pending or failed). You probably want to use this
# quite often, for all reporting purposes.
# +open_or_pending+:: Named scope which matches all open invoices/credit notes and all pending
# payments.
# +due_at+:: Named scope which takes a +DateTime+ argument and matches all ledger items whose
# +due_date+ value is either +NULL+ or is not after the given time. For example,
# you could run <tt>LedgerItem.due_at(Time.now).account_summaries</tt>
# once a day and process payment for all accounts whose balance is not zero.
# +sorted+:: Named scope which takes a column name as documented above (even if it has been
# renamed), and sorts the query by that column. If the column does not exist,
# silently falls back to sorting by the primary key.
# +exclude_empty_invoices+:: Named scope which excludes any invoices or credit notes which do not
# have any associated line items (payments without line items are
# included though). If you're chaining scopes it would be advantageous
# to put this one close to the beginning of your scope chain.
module LedgerItem
extend ActiveSupport::Concern
module ActMethods
# Declares that the current class is a model for ledger items (i.e. invoices, credit notes and
# payment notes).
#
# This method accepts a hash of options, all of which are optional:
# <tt>:subtype</tt>:: One of <tt>:invoice</tt>, <tt>:credit_note</tt> or <tt>:payment</tt>.
#
# Also, the name of any attribute or method required by +LedgerItem+ (as documented on the
# +LedgerItem+ module) may be used as an option, with the value being the name under which
# that particular method or attribute can be found. This allows you to use names other than
# the defaults. For example, if your database column storing the invoice value is called
# +gross_amount+ instead of +total_amount+:
#
# acts_as_ledger_item :total_amount => :gross_amount
def acts_as_ledger_item(*args)
Invoicing::ClassInfo.acts_as(Invoicing::LedgerItem, self, args)
info = ledger_item_class_info
return unless info.previous_info.nil? # Called for the first time?
# Set the 'amount' columns to act as currency values
acts_as_currency_value(info.method(:total_amount), info.method(:tax_amount),
:currency => info.method(:currency), :value_for_formatting => :value_for_formatting)
extend Invoicing::FindSubclasses
include Invoicing::LedgerItem::RenderHTML
include Invoicing::LedgerItem::RenderUBL
end # def acts_as_ledger_item
# Synonym for <tt>acts_as_ledger_item :subtype => :invoice</tt>. All options other than
# <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
# +acts_as_invoice+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
def acts_as_invoice(options={})
acts_as_ledger_item(options.clone.update({:subtype => :invoice}))
end
# Synonym for <tt>acts_as_ledger_item :subtype => :credit_note</tt>. All options other than
# <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
# +acts_as_credit_note+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
def acts_as_credit_note(options={})
acts_as_ledger_item(options.clone.update({:subtype => :credit_note}))
end
# Synonym for <tt>acts_as_ledger_item :subtype => :payment</tt>. All options other than
# <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
# +acts_as_payment+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
def acts_as_payment(options={})
acts_as_ledger_item(options.clone.update({:subtype => :payment}))
end
end # module ActMethods
included do
before_validation :calculate_total_amount
# Dynamically created named scopes
scope :sent_by, lambda { |sender_id|
where(ledger_item_class_info.method(:sender_id) => sender_id)
}
scope :received_by, lambda {|recipient_id|
where(ledger_item_class_info.method(:recipient_id) => recipient_id)
}
scope :sent_or_received_by, lambda { |sender_or_recipient_id|
sender_col = connection.quote_column_name(ledger_item_class_info.method(:sender_id))
recipient_col = connection.quote_column_name(ledger_item_class_info.method(:recipient_id))
where(["#{sender_col} = ? OR #{recipient_col} = ?",
sender_or_recipient_id, sender_or_recipient_id])
}
scope :in_effect, lambda {
where(ledger_item_class_info.method(:status) => ['closed', 'cleared'])
}
scope :open_or_pending, lambda {
where(ledger_item_class_info.method(:status) => ['open', 'pending'])
}
scope :due_at, lambda { |date|
due_date = connection.quote_column_name(ledger_item_class_info.method(:due_date))
where(["#{due_date} <= ? OR #{due_date} IS NULL", date])
}
scope :sorted, lambda { |column|
table = ledger_item_class_info.method(quoted_table_name).to_s
column = ledger_item_class_info.method(column).to_s
if column_names.include?(column)
order("#{table}.#{connection.quote_column_name(column)}, #{table}.#{connection.quote_column_name(primary_key)}")
else
order("#{table}.#{connection.quote_column_name(primary_key)}")
end
}
scope :exclude_empty_invoices, lambda {
line_items_assoc_id = ledger_item_class_info.method(:line_items).to_sym
line_items_refl = reflections[line_items_assoc_id] ||
reflections[line_items_assoc_id.to_s]
line_items_table = line_items_refl.klass.quoted_table_name
# e.g. `ledger_items`.`id`
ledger_items_id = quoted_table_name + "." + connection.quote_column_name(primary_key)
# e.g. `line_items`.`id`
line_items_id = line_items_table + "." +
connection.quote_column_name(line_items_refl.klass.primary_key)
# e.g. `line_items`.`ledger_item_id`
ledger_item_foreign_key = line_items_table + "." + connection.quote_column_name(
line_items_refl.klass.send(:line_item_class_info).method(:ledger_item_id))
quoted_type_column = quoted_table_name + "." + connection.quote_column_name(ledger_item_class_info.method(:type))
payment_classes = select_matching_subclasses(:is_payment, true).map{|c| c.name}
is_payment_class = merge_conditions(["#{quoted_type_column} IN (?)", payment_classes])
joins("LEFT JOIN #{line_items_table} ON #{ledger_item_foreign_key} = #{ledger_items_id}").
where("(#{ledger_item_foreign_key} IS NULL) OR #{is_payment_class}")
}
end
# Overrides the default constructor of <tt>ActiveRecord::Base</tt> when +acts_as_ledger_item+
# is called. If the +uuid+ gem is installed, this constructor creates a new UUID and assigns
# it to the +uuid+ property when a new ledger item model object is created.
def initialize(*args)
super
# Initialise uuid attribute if possible
info = ledger_item_class_info
if self.has_attribute?(info.method(:uuid)) && info.uuid_generator
write_attribute(info.method(:uuid), info.uuid_generator.generate)
end
end
# Calculate sum of net_amount and tax_amount across all line items, and assign it to total_amount;
# calculate sum of tax_amount across all line items, and assign it to tax_amount.
# Called automatically as a +before_validation+ callback. If the LedgerItem subtype is +payment+
# and there are no line items then the total amount is not touched.
def calculate_total_amount
line_items = ledger_item_class_info.get(self, :line_items)
return if self.class.is_payment && line_items.empty?
net_total = tax_total = BigDecimal('0')
line_items.each do |line|
info = line.send(:line_item_class_info)
# Make sure ledger_item association is assigned -- the CurrencyValue
# getters depend on it to fetch the currency
info.set(line, :ledger_item, self)
line.valid? # Ensure any before_validation hooks are called
net_amount = info.get(line, :net_amount)
tax_amount = info.get(line, :tax_amount)
net_total += net_amount unless net_amount.nil?
tax_total += tax_amount unless tax_amount.nil?
end
ledger_item_class_info.set(self, :total_amount, net_total + tax_total)
ledger_item_class_info.set(self, :tax_amount, tax_total)
return net_total
end
# We don't actually implement anything using +method_missing+ at the moment, but use it to
# generate slightly more useful error messages in certain cases.
def method_missing(method_id, *args)
method_name = method_id.to_s
if ['line_items', ledger_item_class_info.method(:line_items)].include? method_name
raise RuntimeError, "You need to define an association like 'has_many :line_items' on #{self.class.name}. If you " +
"have defined the association with a different name, pass the option :line_items => :your_association_name to " +
"acts_as_ledger_item."
else
super
end
end
# The difference +total_amount+ minus +tax_amount+.
def net_amount
total_amount = ledger_item_class_info.get(self, :total_amount)
tax_amount = ledger_item_class_info.get(self, :tax_amount)
(total_amount && tax_amount) ? (total_amount - tax_amount) : nil
end
# +net_amount+ formatted in human-readable form using the ledger item's currency.
def net_amount_formatted
format_currency_value(net_amount)
end
# You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
# details of the party sending the document. See +sender_id+ above for a detailed interpretation of
# sender and receiver.
#
# The methods +sender_details+ and +recipient_details+ are required to return hashes
# containing details about the sender and recipient of an invoice, credit note or payment. The reason we
# do this is that you probably already have your own system for handling users, customers and their personal
# or business details, and this framework shouldn't require you to change any of that.
#
# The invoicing framework currently uses these details only for rendering invoices and credit notes, but
# in future it may serve more advanced purposes, such as determining which tax rate to apply for overseas
# customers.
#
# In the hash returned by +sender_details+ and +recipient_details+, the following keys are recognised --
# please fill in as many as possible:
# <tt>:is_self</tt>:: +true+ if these details refer to yourself, i.e. the person or organsiation who owns/operates
# this application. +false+ if these details refer to any other party.
# <tt>:name</tt>:: The name of the person or organisation whose billing address is defined below.
# <tt>:contact_name</tt>:: The name of a person/department within the organisation named by <tt>:name</tt>.
# <tt>:address</tt>:: The body of the billing address (not including city, postcode, state and country); may be
# a multi-line string, with lines separated by '\n' line breaks.
# <tt>:city</tt>:: The name of the city or town in the billing address.
# <tt>:state</tt>:: The state/region/province/county of the billing address as appropriate.
# <tt>:postal_code</tt>:: The postal code of the billing address (e.g. ZIP code in the US).
# <tt>:country</tt>:: The billing address country (human-readable).
# <tt>:country_code</tt>:: The two-letter country code of the billing address, according to
# ISO-3166-1[http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2].
# <tt>:tax_number</tt>:: The Value Added Tax registration code of this person or organisation, if they have
# one, preferably including the country identifier at the beginning. This is important for
# transactions within the European Union.
def sender_details
raise 'overwrite this method'
end
# You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
# details of the party receiving the document. See +recipient_id+ above for a detailed interpretation of
# sender and receiver. See +sender_details+ for a list of fields to return in the hash.
def recipient_details
raise 'overwrite this method'
end
# Returns +true+ if this document was sent by the user with ID +user_id+. If the argument is +nil+
# (indicating yourself), this also returns +true+ if <tt>sender_details[:is_self]</tt>.
def sent_by?(user_id)
(ledger_item_class_info.get(self, :sender_id) == user_id) ||
!!(user_id.nil? && ledger_item_class_info.get(self, :sender_details)[:is_self])
end
# Returns +true+ if this document was received by the user with ID +user_id+. If the argument is +nil+
# (indicating yourself), this also returns +true+ if <tt>recipient_details[:is_self]</tt>.
def received_by?(user_id)
(ledger_item_class_info.get(self, :recipient_id) == user_id) ||
!!(user_id.nil? && ledger_item_class_info.get(self, :recipient_details)[:is_self])
end
# Returns a boolean which specifies whether this transaction should be recorded as a debit (+true+)
# or a credit (+false+) on a particular ledger. Unless you know what you are doing, you probably
# do not need to touch this method.
#
# It takes an argument +self_id+, which should be equal to either +sender_id+ or +recipient_id+ of this
# object, and which determines from which perspective the account is viewed. The default behaviour is:
# * A sent invoice (<tt>self_id == sender_id</tt>) is a debit since it increases the recipient's
# liability; a sent credit note decreases the recipient's liability with a negative-valued
# debit; a sent payment receipt is a positive-valued credit and thus decreases the recipient's
# liability.
# * A received invoice (<tt>self_id == recipient_id</tt>) is a credit because it increases your own
# liability; a received credit note decreases your own liability with a negative-valued credit;
# a received payment receipt is a positive-valued debit and thus decreases your own liability.
#
# Note that accounting practices differ with regard to credit notes: some think that a sent
# credit note should be recorded as a positive credit (hence the name 'credit note'); others
# prefer to use a negative debit. We chose the latter because it allows you to calculate the
# total sale volume on an account simply by adding up all the debits. If there is enough demand
# for the positive-credit model, we may add support for it sometime in future.
def debit?(self_id)
sender_is_self = sent_by?(self_id)
recipient_is_self = received_by?(self_id)
raise ArgumentError, "self_id #{self_id.inspect} is neither sender nor recipient" unless sender_is_self || recipient_is_self
raise ArgumentError, "self_id #{self_id.inspect} is both sender and recipient" if sender_is_self && recipient_is_self
self.class.debit_when_sent_by_self ? sender_is_self : recipient_is_self
end
# Invoked internally when +total_amount_formatted+ or +tax_amount_formatted+ is called. Allows
# you to specify options like <tt>:debit => :negative, :self_id => 42</tt> meaning that if this
# ledger item is a debit as regarded from the point of view of +self_id+ then it should be
# displayed as a negative number. Note this only affects the output formatting, not the actual
# stored values.
def value_for_formatting(value, options={})
value = -value if (options[:debit] == :negative) && debit?(options[:self_id])
value = -value if (options[:credit] == :negative) && !debit?(options[:self_id])
value
end
module ClassMethods
# Returns +true+ if this type of ledger item should be recorded as a debit when the party
# viewing the account is the sender of the document, and recorded as a credit when
# the party viewing the account is the recipient. Returns +false+ if those roles are
# reversed. This method implements default behaviour for invoices, credit notes and
# payments (see <tt>Invoicing::LedgerItem#debit?</tt>); if you define custom ledger item
# subtypes (other than +invoice+, +credit_note+ and +payment+), you should override this
# method accordingly in those subclasses.
def debit_when_sent_by_self
case ledger_item_class_info.subtype
when :invoice then true
when :credit_note then true
when :payment then false
else nil
end
end
# Returns +true+ if this type of ledger item is a +invoice+ subtype, and +false+ otherwise.
def is_invoice
ledger_item_class_info.subtype == :invoice
end
# Returns +true+ if this type of ledger item is a +credit_note+ subtype, and +false+ otherwise.
def is_credit_note
ledger_item_class_info.subtype == :credit_note
end
# Returns +true+ if this type of ledger item is a +payment+ subtype, and +false+ otherwise.
def is_payment
ledger_item_class_info.subtype == :payment
end
# Returns a summary of the customer or supplier account between two parties identified
# by +self_id+ (the party from whose perspective the account is seen, 'you') and +other_id+
# ('them', your supplier/customer). The return value is a hash with ISO 4217 currency codes
# as keys (as symbols), and summary objects as values. An account using only one currency
# will have only one entry in the hash, but more complex accounts may have several.
#
# The summary object has the following methods:
#
# currency => symbol # Same as the key of this hash entry
# sales => BigDecimal(...) # Sum of sales (invoices sent by self_id)
# purchases => BigDecimal(...) # Sum of purchases (invoices received by self_id)
# sale_receipts => BigDecimal(...) # Sum of payments received from customer
# purchase_payments => BigDecimal(...) # Sum of payments made to supplier
# balance => BigDecimal(...) # sales - purchases - sale_receipts + purchase_payments
#
# The <tt>:balance</tt> fields indicate any outstanding money owed on the account: the value is
# positive if they owe you money, and negative if you owe them money.
#
# In addition, +acts_as_currency_value+ is set on the numeric fields, so you can use its
# convenience methods such as +summary.sales_formatted+.
#
# If +other_id+ is +nil+, this method aggregates the accounts of +self_id+ with *all* other
# parties.
#
# Also accepts options:
#
# <tt>:with_status</tt>:: List of ledger item status strings; only ledger items whose status
# is one of these will be taken into account. Default:
# <tt>["closed", "cleared"]</tt>.
def account_summary(self_id, other_id=nil, options={})
info = ledger_item_class_info
self_id = self_id.to_i
other_id = [nil, ''].include?(other_id) ? nil : other_id.to_i
if other_id.nil?
result = {}
# Sum over all others, grouped by currency
account_summaries(self_id, options).each_pair do |other_id, hash|
hash.each_pair do |currency, summary|
if result[currency]
result[currency] += summary
else
result[currency] = summary
end
end
end
result
else
conditions = {info.method(:sender_id) => [self_id, other_id],
info.method(:recipient_id) => [self_id, other_id]}
where(conditions).account_summaries(self_id, options)[other_id] || {}
end
end
# Returns a summary account status for all customers or suppliers with which a particular party
# has dealings. Takes into account all +closed+ invoices/credit notes and all +cleared+ payments
# which have +self_id+ as their +sender_id+ or +recipient_id+. Returns a hash whose keys are the
# other party of each account (i.e. the value of +sender_id+ or +recipient_id+ which is not
# +self_id+, as an integer), and whose values are again hashes, of the same form as returned by
# +account_summary+ (+summary+ objects as documented on +account_summary+):
#
# LedgerItem.account_summaries(1)
# # => { 2 => { :USD => summary, :EUR => summary },
# # 3 => { :EUR => summary } }
#
# If you want to further restrict the ledger items taken into account in this calculation (e.g.
# include only data from a particular quarter) you can call this method within an ActiveRecord
# scope:
#
# q3_2008 = ['issue_date >= ? AND issue_date < ?', DateTime.parse('2008-07-01'), DateTime.parse('2008-10-01')]
# LedgerItem.scoped(:conditions => q3_2008).account_summaries(1)
#
#
# Also accepts options:
#
# <tt>:with_status</tt>:: List of ledger item status strings; only ledger items whose status
# is one of these will be taken into account. Default:
# <tt>["closed", "cleared"]</tt>.
def account_summaries(self_id, options={})
info = ledger_item_class_info
ext = Invoicing::ConnectionAdapterExt
debit_classes = select_matching_subclasses(:debit_when_sent_by_self, true, self.table_name, self.inheritance_column).map{|c| c.name}
credit_classes = select_matching_subclasses(:debit_when_sent_by_self, false, self.table_name, self.inheritance_column).map{|c| c.name}
sender_id = get_quoted_column_name(info.method(:sender_id))
recipient_id = get_quoted_column_name(info.method(:recipient_id))
status = get_quoted_column_name(info.method(:status))
# rails 3 idiocricies. in case of STI, type of base class is nil. Need special handling
debit_when_sent = merge_conditions(inheritance_condition(debit_classes), ["#{sender_id} = ?", self_id])
debit_when_received = merge_conditions(inheritance_condition(credit_classes), ["#{recipient_id} = ?", self_id])
credit_when_sent = merge_conditions(inheritance_condition(credit_classes), ["#{sender_id} = ?", self_id])
credit_when_received = merge_conditions(inheritance_condition(debit_classes), ["#{recipient_id} = ?", self_id])
cols = {}
[:total_amount, :sender_id, :recipient_id, :status, :currency].each do |col|
cols[col] = connection.quote_column_name(info.method(col))
end
sender_is_self = merge_conditions(["#{sender_id} = ?", self_id])
recipient_is_self = merge_conditions(["#{recipient_id} = ?", self_id])
other_id_column = ext.conditional_function(sender_is_self, cols[:recipient_id], cols[:sender_id])
accept_status = merge_conditions(["#{status} IN (?)", (options[:with_status] || %w(closed cleared))])
filter_conditions = "#{accept_status} AND (#{sender_is_self} OR #{recipient_is_self})"
sql = select("#{other_id_column} AS other_id, #{cols[:currency]} AS currency, " +
"SUM(#{ext.conditional_function(debit_when_sent, cols[:total_amount], 0)}) AS sales, " +
"SUM(#{ext.conditional_function(debit_when_received, cols[:total_amount], 0)}) AS purchase_payments, " +
"SUM(#{ext.conditional_function(credit_when_sent, cols[:total_amount], 0)}) AS sale_receipts, " +
"SUM(#{ext.conditional_function(credit_when_received, cols[:total_amount], 0)}) AS purchases ")
sql = sql.where(filter_conditions)
sql = sql.group("other_id, currency")
# add order, limit, and lock from outside
rows = connection.execute(sql.to_sql).to_a
results = {}
rows.each do |row|
row.symbolize_keys!
other_id = row[:other_id].to_i
currency = row[:currency].to_sym
summary = {:balance => BigDecimal('0'), :currency => currency}
{:sales => 1, :purchases => -1, :sale_receipts => -1, :purchase_payments => 1}.each_pair do |field, factor|
summary[field] = BigDecimal(row[field].to_s)
summary[:balance] += BigDecimal(factor.to_s) * summary[field]
end
results[other_id] ||= {}
results[other_id][currency] = AccountSummary.new summary
end
results
end
# Takes an array of IDs like those used in +sender_id+ and +recipient_id+, and returns a hash
# which maps each of these IDs (typecast to integer) to the <tt>:name</tt> field of the
# hash returned by +sender_details+ or +recipient_details+ for that ID. This is useful as it
# allows +LedgerItem+ to use human-readable names for people or organisations in its output,
# without depending on a particular implementation of the model objects used to store those
# entities.
#
# LedgerItem.sender_recipient_name_map [2, 4]
# => {2 => "Fast Flowers Ltd.", 4 => "Speedy Motors"}
def sender_recipient_name_map(*sender_recipient_ids)
sender_recipient_ids = sender_recipient_ids.flatten.map &:to_i
sender_recipient_to_ledger_item_ids = {}
result_map = {}
info = ledger_item_class_info
# Find the most recent occurrence of each ID, first in the sender_id column, then in recipient_id
[:sender_id, :recipient_id].each do |column|
column = info.method(column)
quoted_column = connection.quote_column_name(column)
sql = "SELECT MAX(#{primary_key}) AS id, #{quoted_column} AS ref FROM #{quoted_table_name} WHERE "
sql << merge_conditions(["#{column} IN (?)", sender_recipient_ids])
sql << " GROUP BY #{quoted_column}"
ActiveRecord::Base.connection.select_all(sql).each do |row|
sender_recipient_to_ledger_item_ids[row['ref'].to_i] = row['id'].to_i
end
sender_recipient_ids -= sender_recipient_to_ledger_item_ids.keys
end
# Load all the ledger items needed to get one representative of each name
find(sender_recipient_to_ledger_item_ids.values.uniq).each do |ledger_item|
sender_id = info.get(ledger_item, :sender_id)
recipient_id = info.get(ledger_item, :recipient_id)
if sender_recipient_to_ledger_item_ids.include? sender_id
details = info.get(ledger_item, :sender_details)
result_map[sender_id] = details[:name]
end
if sender_recipient_to_ledger_item_ids.include? recipient_id
details = info.get(ledger_item, :recipient_details)
result_map[recipient_id] = details[:name]
end
end
result_map
end
def get_quoted_column_name(column_name)
quoted_table_name + "." + connection.quote_column_name(column_name)
end
def inheritance_condition(classes)
segments = []
quoted_inheritance_column = get_quoted_column_name(inheritance_column)
segments << sanitize_sql(["#{quoted_inheritance_column} IN (?)", classes])
if classes.include?(self.to_s) && self.new.send(inheritance_column).nil?
segments << sanitize_sql("#{get_quoted_column_name(:type)} IS NULL")
end
"(#{segments.join(') OR (')})" unless segments.empty?
end
def merge_conditions(*conditions)
segments = []
conditions.each do |condition|
unless condition.blank?
sql = sanitize_sql(condition)
segments << sql unless sql.blank?
end
end
"(#{segments.join(') AND (')})" unless segments.empty?
end
end # module ClassMethods
# Very simple class for representing the sum of all sales, purchases and payments on
# an account.
class AccountSummary #:nodoc:
NUM_FIELDS = [:sales, :purchases, :sale_receipts, :purchase_payments, :balance]
attr_reader *([:currency] + NUM_FIELDS)
def initialize(hash)
@currency = hash[:currency]; @sales = hash[:sales]; @purchases = hash[:purchases]
@sale_receipts = hash[:sale_receipts]; @purchase_payments = hash[:purchase_payments]
@balance = hash[:balance]
end
def method_missing(name, *args)
if name.to_s =~ /(.*)_formatted$/
::Invoicing::CurrencyValue::Formatter.format_value(currency, send($1))
else
super
end
end
def +(other)
hash = {:currency => currency}
NUM_FIELDS.each {|field| hash[field] = send(field) + other.send(field) }
AccountSummary.new hash
end
def to_s
NUM_FIELDS.map do |field|
val = send("#{field}_formatted")
"#{field} = #{val}"
end.join('; ')
end
end
# Stores state in the ActiveRecord class object
class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
attr_reader :subtype, :uuid_generator
def initialize(model_class, previous_info, args)
super
@subtype = all_options[:subtype]
begin # try to load the UUID gem
require 'uuid'
@uuid_generator = UUID.new
rescue LoadError, NameError # silently ignore if gem not found
@uuid_generator = nil
end
end
# Allow methods generated by +CurrencyValue+ to be renamed as well
def method(name)
if name.to_s =~ /^(.*)_formatted$/
"#{super($1)}_formatted"
else
super
end
end
end
end # module LedgerItem
end