Module: Invoicing::Taxable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/invoicing/taxable.rb
Overview
Computation of tax on prices
This module provides a general-purpose framework for calculating tax. Its most common application will probably be for computing VAT/sales tax on the price of your product, but since you can easily attach your own tax computation logic, it can apply in a broad variety of different situations.
Computing the tax on a price may be as simple as multiplying it with a constant factor, but in most
cases it will be more complicated. The tax rate may change over time (see TimeDependent), may vary
depending on the customer currently viewing the page (and the country in which they are located),
and may depend on properties of the object to which the price belongs. This module does not implement
any specific computation, but makes easy to implement specific tax regimes with minimal code duplication.
Using taxable attributes in a model
If you have a model object (a subclass of ActiveRecord::Base) with a monetary quantity (such as a price) in one or more of its database columns, you can declare that those columns/attributes are taxable, for example:
class MyProduct < ActiveRecord::Base
acts_as_taxable :normal_price, :promotion_price, :tax_logic => Invoicing::Countries::UK::VAT.new
end
In the taxable columns (+normal_price+ and promotion_price in this example) you must always
store values excluding tax. The option :tax_logic is mandatory, and you must give it
an instance of a 'tax logic' object; you may use one of the tax logic implementations provided with
this framework, or write your own. See below for details of what a tax logic object needs to do.
Your database table should also contain a column currency, in which you store the ISO 4217
three-letter upper-case code identifying the currency of the monetary amounts in the same table row.
If your currency code column has a name other than currency, you need to specify the name of that
column to acts_as_taxable using the :currency => '...' option.
For each attribute which you declare as taxable, several new methods are generated on your model class:
Returns the amount of money excluding tax, as stored in the database, subject to the model object's currency rounding conventions.
= Assigns a new value (exclusive of tax) to the attribute.
_taxed Returns the amount of money including tax, as computed by the tax logic, subject to the model object's currency rounding conventions.
_taxed= Assigns a new value (including tax) to the attribute.
_tax_rounding_error Returns a number indicating how much the tax-inclusive value of the attribute has changed as a result of currency rounding. See the section 'currency rounding errors' below.
nilif the_taxed=attribute has not been assigned._tax_info Returns a short string to inform a user about the tax status of the value returned by
_taxed ; this could be "inc. VAT", for example, if the_taxedattribute includes VAT._tax_details Like
_tax_info, but a longer string for places in the user interface where more space is available. For example, "including VAT at 15%"._with_tax_info Convenience method for views: returns the attribute value including tax, formatted as a human-friendly currency string in UTF-8, with the return value of
_tax_infoappended. For example, "AU$1,234.00 inc. GST"._with_tax_details Like
_with_tax_info, but using_tax_details. For example, "AU$1,234.00 including 10% Goods and Services Tax"._taxed_before_type_cast Returns any value which you assign to
_taxed= without converting it first. This means you to can use_taxedattributes as fields in Rails forms and get the expected behaviour of form validation.
acts_as_currency is automatically called for all attributes given to acts_as_taxable, as well as all
generated CurrencyValue module, and you get two additional methods for free:
The Taxable module automatically converts between taxed and untaxed attributes. This works as you would
expect: you can assign to a taxed attribute and immediately read from an untaxed attribute, or vice versa.
When you store the object, only the untaxed value is written to the database. That way, if the tax rate
changes or you open your business to overseas customers, nothing changes in your database.
Using taxable attributes in views and forms
The tax logic object allows you to have one single place in your application where you declare which products are seen by which customers at which tax rate. For example, if you are a VAT registered business in an EU country, you always charge VAT at your home country's rate to customers within your home country; however, to a customer in a different EU country you do not charge any VAT if you have received a valid VAT registration number from them. You see that this logic can easily become quite complicated. This complexity should be encapsulated entirely within the tax logic object, and not require any changes to your views or controllers if at all possible.
The way to achieve this is to always use the _taxed attributes in views and forms, unless you have a
very good reason not to. The value returned by _taxed because they may be taxed, not because they necessarily always are. It is
up to the tax logic to decide whether to return the same number, or one modified to include tax.
The purpose of the _tax_info and _tax_details methods is to clarify the tax status of a given number to the
user; if the number returned by the _taxed attribute does not contain tax for whatever reason, _tax_info for
the same attribute should say so.
Using these attributes, views can be kept very simple:
Products
| Name | Price |
|---|---|
| <%=h product.name %> | <%=h product.price_with_tax_info %> | # e.g. "$25.80 (inc. tax)"
New product
<% form_for(@product) do |f| %> <%= f.error_messages %>
<%= f.label :name, "Product name:" %>
<%= f.text_field :name %>
<%= f.label :price_taxed, "Price #h(@producth(@product.price_tax_info):" %>
# e.g. "Price (inc. tax):"
<%= f.text_field :price_taxed %>
If this page is viewed by a user who shouldn't be shown tax, the numbers in the output will be different, and it might say "excl. tax" instead of "inc. tax"; but none of that clutters the view. Moreover, any price typed into the form will of course be converted as appropriate for that user. This is important, for example, in an auction scenario, where you may have taxed and untaxed bidders bidding in the same auction; their input and output is personalised depending on their account information, but for purposes of determining the winning bidder, all bidders are automatically normalised to the untaxed value of their bids.
Tax logic objects
A tax logic object is an instance of a class with the following structure:
class MyTaxLogic
def apply_tax(params)
# Called to convert a value without tax into a value with tax, as applicable. params is a hash:
# :model_object => The model object whose attribute is being converted
# :attribute => The name of the attribute (without '_taxed' suffix) being converted
# :value => The untaxed value of the attribute as a BigDecimal
# Should return a number greater than or equal to the input value. Don't worry about rounding --
# CurrencyValue deals with that.
end
def remove_tax(params)
# Does the reverse of apply_tax -- converts a value with tax into a value without tax. The params
# hash has the same format. First applying tax and then removing it again should always result in the
# starting value (for the the same object and the same environment -- it may depend on time,
# global variables, etc).
end
def tax_info(params, *args)
# Should return a short string to explain to users which operation has been performed by apply_tax
# (e.g. if apply_tax has added VAT, the string could be "inc. VAT"). The params hash is the same as
# given to apply_tax. Additional parameters are optional; if any arguments are passed to a call to
# model_object.<attr>_tax_info then they are passed on here.
end
def tax_details(params, *args)
# Like tax_info, but should return a longer string for use in user interface elements which are less
# limited in size.
end
def mixin_methods
# Optionally you can define a method mixin_methods which returns a list of method names which should
# be included in classes which use this tax logic. Methods defined here become instance methods of
# model objects with acts_as_taxable attributes. For example:
[:some_other_method]
end
def some_other_method(params, *args)
# some_other_method was named by mixin_methods to be included in model objects. For example, if the
# class MyProduct uses MyTaxLogic, then MyProduct.find(1).some_other_method(:foo, 'bar') will
# translate into MyTaxLogic#some_other_method({:model_object => MyProduct.find(1)}, :foo, 'bar').
# The model object on which the method is called is passed under the key :model_object in the
# params hash, and all other arguments to the method are simply passed on.
end
end
Currency rounding errors
Both the taxed and the untaxed value of an attribute are currency values, and so they must both be rounded
to the accuracy which is conventional for the currency in use (see the discussion of precision and rounding
in the CurrencyValue module). If we are always storing untaxed values and outputting taxed values to the
user, this is not a problem. However, if we allow users to input taxed values (like in the form example
above), something curious may happen: The input value has its tax removed, is rounded to the currency's
conventional precision and stored in the database in untaxed form; then later it is loaded, tax is added
again, it is again rounded to the currency's conventional precision, and displayed to the user. If the
rounding steps have rounded the number upwards twice, or downwards twice, it may happen that the value
displayed to the user differs slightly from the one they originally entered.
We believe that storing untaxed values and performing currency rounding are the right things to do, and this
apparent rounding error is a natural consequence. This module therefore tries to deal with the error
elegantly: If you assign a value to a taxed attribute and immediately read it again, it will return the
same value as if it had been stored and loaded again (i.e. the number you read has been rounded twice --
make sure the currency code has been assigned to the object beforehand, so that the CurrencyValue module
knows which precision to apply).
Moreover, after assigning a value to a
Defined Under Namespace
Modules: ActMethods, ClassMethods Classes: ClassInfo