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:

<attr>

Returns the amount of money excluding tax, as stored in the database, subject to the model object's currency rounding conventions.

<attr>=

Assigns a new value (exclusive of tax) to the attribute.

<attr>_taxed

Returns the amount of money including tax, as computed by the tax logic, subject to the model object's currency rounding conventions.

<attr>_taxed=

Assigns a new value (including tax) to the attribute.

<attr>_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. nil if the _taxed= attribute has not been assigned.

<attr>_tax_info

Returns a short string to inform a user about the tax status of the value returned by <attr>_taxed; this could be “inc. VAT”, for example, if the _taxed attribute includes VAT.

<attr>_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%”.

<attr>_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_info appended. For example, “AU$1,234.00 inc. GST”.

<attr>_with_tax_details

Like _with_tax_info, but using _tax_details. For example, “AU$1,234.00 including 10% Goods and Services Tax”.

<attr>_taxed_before_type_cast

Returns any value which you assign to <attr>_taxed= without converting it first. This means you to can use _taxed attributes 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 <attr>_taxed attributes. This means you get automatic currency-specific rounding behaviour as documented in the CurrencyValue module, and you get two additional methods for free: <attr>_formatted and <attr>_taxed_formatted, which return the untaxed and taxed amounts respectively, formatted as a nice human-friendly string.

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 <attr>_taxed, and the value you assign to <attr>_taxed=, do not necessarily have to include tax; for a given customer and product, the tax may be zero-rated or not applicable, in which case their numeric value will be the same as the untaxed attributes. The attributes are called _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:

<h1>Products</h1>
<table>
  <tr>
    <th>Name</th>
    <th>Price</th>
  </tr>
<% for product in @products %>
  <tr>
    <td><%=h product.name %></td>
    <td><%=h product.price_with_tax_info %></td>                                # e.g. "$25.80 (inc. tax)"
  </tr>
<% end %>
</table>

<h1>New product</h1>
<% form_for(@product) do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :name, "Product name:" %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :price_taxed, "Price #{h(@product.price_tax_info)}:" %><br />   # e.g. "Price (inc. tax):"
    <%= f.text_field :price_taxed %>
  </p>
<% end %>

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 <attr>_taxed= attribute, the <attr>_tax_rounding_error method can tell you whether and by how much the value has changed as a result of removing and re-applying tax. A negative number indicates that the converted amount is less than the input; a positive number indicates that it is more than entered by the user; and zero means that there was no difference.

Defined Under Namespace

Modules: ActMethods, ClassMethods Classes: ClassInfo