vat : Adding VAT (Value-added tax) functionality

The lino_xl.lib.vat plug-in adds functionality for handling sales and purchase invoices in a context where the site operator is subject to value-added tax (VAT). It provides a framework for handling VAT declarations. When using this plugin, you will probably also install one of the national VAT modules.

This is a tested document. The following instructions are used for initialization:

>>> from lino import startup
>>> startup('lino_book.projects.pierre.settings.doctests')
>>> from lino.api.doctest import *

The VAT number

Every sales or purchase business partner can have a VAT identification number (VATIN).

The following function validate_vat_id is used in this document to demonstrate the capabilities and roles of Lino in managing a VAT number.

>>> from django.core.exceptions import ValidationError
>>> def validate_vat_id(partner):
...     try:
...         partner.full_clean()
...         print(partner.vat_id)
...     except ValidationError as e:
...         print("ValidationError:\n" + e.message)

A partner that has a vat_id (VAT number) field, by default implementation, also has a vid_manager property which essentially a pointer to an instance of a VatNumberManager that validates an inserted VAT number and do some other things that we will see below.

Below example displays an error message for an invalid VAT number.

>>> Country = rt.models.countries.Country
>>> belgium = Country(isocode="BE")
>>> Company = rt.models.contacts.Company
>>> obj = Company(name="foo", country=belgium)
>>> obj.vat_id = "5468523548"
>>> validate_vat_id(obj)
ValidationError:
Modulo 97 check failed for VAT identification number in BE

The validation error message is a little more self explanatory when the inserted VAT number does not match any format explained here. An example:

>>> obj = Company(name="bar Ltd.", vat_id="NL56465")
>>> validate_vat_id(obj)
ValidationError:
You have entered an invalid VAT identification number.
The general format follows: NLxxxxxxxxxBxx
Where each 'x' is a digit.

When the country field is empty, the ISO 3166-1-alpha-2 country code must be given at the beginning of the VAT identification number otherwise lino will treat the VAT identification number as a dummy and will not do any validation check. The following example shows two possibilities where in first case there's no country associated with the partner but has a valid vat_id format and in later case there's no known country associated with the partner not in country field neither in the vat_id. And differences are distinguishable from the returned VAT number, where the valid VAT number returns are formatted with dots (.) and a whitespace.

>>> obj = Company(name="foo Ltd.", vat_id="EE100041561")
>>> validate_vat_id(obj)
EE 100.041.561
>>> obj = Company(name="foo Inc.", vat_id="100041561")
>>> validate_vat_id(obj)
100041561

If the partner's country is different from the country code given in the VAT number, Lino will also raise a ValidationError.

>>> estonia = Country(isocode="EE")
>>> obj = Company(name="foo Inc.", country=estonia, vat_id="BE 100041561")
>>> validate_vat_id(obj)
ValidationError:
Country code (EE) does not match with BE.

By default a VatNumberManager instance includes the validation for the countries with ISO code shown in the following code output.

>>> list(obj.vid_manager._vat_origins._items.keys())
['BE', 'AT', 'NL', 'HR', 'DK', 'EE', 'FI', 'FR', 'DE', 'EL', 'HU', 'IT', 'LV', 'LT', 'LU']

If the given country code is not included in the above example, lino will accept any reasonable format of the VAT number as valid.

>>> obj = Company(name="HyD Inc.", vat_id="BD 545454656484b6563")
>>> validate_vat_id(obj)
BD 54.545.465.648.4B6.563

Adding a VatOrigin:

>>> from lino_xl.lib.vat.choicelists import vat_origins, VatOrigin
>>> cyprus_origin = VatOrigin('CY', 8, dummy_suffix="L",
...     pattern="^(?P<country_code>CY) (?P<number>[0-9]{8})(?P<suffix>L)$")
>>> vat_origins.add_item('CY', cyprus_origin)

When lino_xl.lib.vat.Plugin.use_online_check is True, the checkdata command will do online verification of every VAT id.

class lino_xl.lib.vat.VatIdChecker

Validate VAT id from online registry.

Since the demo database contains some fictive VAT ids, we have a series of corresponding data problem:

>>> chk = checkdata.Checkers.get_by_value('vat.VatIdChecker')
>>> rt.show(checkdata.ProblemsByChecker, chk)
... 
+-------------------------+---------------------------------+------------------------------------------------------+
| Utilisateur responsable | Database object                 | Message                                              |
+=========================+=================================+======================================================+
| Robin Rood              | *Bäckerei Ausdemwald*           | Invalid VAT identification number BE 7088.996.857:   |
|                         |                                 | Not registered in BE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Bäckerei Mießen*               | Invalid VAT identification number BE 4685.739.309:   |
|                         |                                 | Not registered in BE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Bäckerei Schmitz*              | Invalid VAT identification number BE 4181.505.692:   |
|                         |                                 | Not registered in BE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Garage Mergelsberg*            | Invalid VAT identification number BE 9045.438.159:   |
|                         |                                 | Not registered in BE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Donderweer BV*                 | Invalid VAT identification number NL 220.876.686B01: |
|                         |                                 | Not registered in NL.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Van Achter NV*                 | Invalid VAT identification number NL 451.948.587B01: |
|                         |                                 | Not registered in NL.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Hans Flott & Co*               | Invalid VAT identification number DE 143.956.862:    |
|                         |                                 | Not registered in DE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Bernd Brechts Bücherladen*     | Invalid VAT identification number DE 135.079.295:    |
|                         |                                 | Not registered in DE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Reinhards Baumschule*          | Invalid VAT identification number DE 138.433.397:    |
|                         |                                 | Not registered in DE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Moulin Rouge*                  | Invalid VAT identification number FR 86.915.334.564: |
|                         |                                 | Not registered in FR.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Auto École Verte*              | Invalid VAT identification number FR 66.435.589.280: |
|                         |                                 | Not registered in FR.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Maksu- ja Tolliamet*           | Invalid VAT identification number EE 848.217.541:    |
|                         |                                 | Not registered in EE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+
| Robin Rood              | *Electrabel Customer Solutions* | Invalid VAT identification number BE 4018.258.949:   |
|                         |                                 | Not registered in BE.                                |
+-------------------------+---------------------------------+------------------------------------------------------+

Plugin configuration

A Lino site that uses this plugin will usually specify the national VAT module for their VAT declarations by setting the declaration_plugin plugin attribute.

National VAT modules

A national VAT module is a normal Lino plugin that implements the VAT declaration and rules for a given country. It is activated by setting the vat.declaration_plugin plugin attribute.

Currently we have three national VAT modules:

Accounting applications to be used by site operators who don't care about VAT might use lino_xl.lib.vatless instead (though this plugin might become deprecated). The modules lino_xl.lib.vatless and lino_xl.lib.vat can theoretically both be installed though obviously this wouldn't make sense.

VAT regimes

>>> rt.show(vat.VatRegimes, language="en")
... 
======= ======== ======== ========== ==============
 value   name     text     VAT area   Needs VAT id
------- -------- -------- ---------- --------------
 10      normal   Normal              No
======= ======== ======== ========== ==============
class lino_xl.lib.vat.VatRegime

Base class for the items of VatRegimes. Each VAT regime is an instance of this and has two properties:

vat_area

In which VAT area this regime is available.

item_vat

Whether unit prices are VAT included or not. No longer used. See Plugin.item_vat instead.

needs_vat_id

Whether this VAT regime requires that partner to have a vat_id.

class lino_xl.lib.vat.VatRegimes

The global list of VAT regimes. Each item of this list is an instance of VatRegime.

Three VAT regimes are considered standard minimum:

normal
subject
intracom

Two additional regimes are defined in lino_xl.lib.bevat:

de
lu

VAT classes

See also VAT classes.

>>> rt.show(vat.VatClasses, language="en")
======= ============= ===========================
 value   name          text
------- ------------- ---------------------------
 010     goods         Goods at normal VAT rate
 020     reduced       Goods at reduced VAT rate
 030     exempt        Goods exempt from VAT
 100     services      Services
 200     investments   Investments
 210     real_estate   Real estate
 220     vehicles      Vehicles
 300     vatless       Without VAT
======= ============= ===========================
class lino_xl.lib.vat.VatClasses

The global list of VAT classes.

Default classes are:

exempt
reduced
normal

VAT rules

When no national VAT module is installed, we have only one default VAT rule with no condition and zero rate.

>>> rt.show(vat.VatRules, language="en")
... 
+-------+------------------+
| value | Description      |
+=======+==================+
| 1     | VAT rule 1:      |
|       | apply 0 %        |
|       | and book to None |
+-------+------------------+
class lino_xl.lib.vat.VatRule

A rule which defines how VAT is to be handled for a given invoice item.

Example data see lino_xl.lib.vat.fixtures.euvatrates.

Database fields:

seqno

The sequence number.

country
vat_class
vat_regime

The regime for which this rule applies.

Pointer to VatRegimes.

rate

The VAT rate to be applied. Note that a VAT rate of 20 percent is stored as 0.20 (not 20).

vat_account

The general account where VAT is to be booked.

vat_returnable

Whether VAT is returnable. Returnable VAT does not increase the total amount of the voucher, it causes an additional movement into the vat_returnable_account. See About returnable VAT.

vat_returnable_account

Where to book returnable VAT. If this field is empty and vat_returnable is True, then VAT will be added to the base account. See About returnable VAT.

get_vat_rule(cls, trade_type, vat_regime,
vat_class=None, country=None, date=None)

Return the VAT rule to be applied for the given criteria.

Lino loops through all rules (ordered by their seqno) and returns the first object which matches.

class lino_xl.lib.vat.VatRules

The table of all VatRule objects.

This table is accessible via Explorer ‣ VAT ‣ VAT rules.

>>> show_menu_path(vat.VatRules, language='en')
Explorer --> VAT --> VAT rules

This table is filled by

VAT areas

The VatAreas choice list contains the list of available VAT areas.

>>> rt.show(vat.VatAreas, language="en")
======= =============== ===============
 value   name            text
------- --------------- ---------------
 10      national        National
 20      eu              EU
 30      international   International
======= =============== ===============

The plugin property eu_country_codes defines which countries are considered part of the EU.

Available VAT regimes

The declaration plugin controls which VAT regimes are available for selection on a partner or on a voucher.

The available VAT regimes vary depending on which VAT declaration plugin is installed. When no declaration module is installed, we have only one default regime.

The list of available VAT regimes for a partner depends on the VAT area and on whether the partner has a VAT id or not.

get_vat_regime_choices(country=None, vat_id=None):

Used for the choosers of the vat_regime field of a partner and a voucher.

class lino_xl.lib.vat.VatAreas

The global list of VAT areas.

classmethod get_for_country(cls, country)

Return the VAT area for this country.

Why differentiate between VAT regimes and VAT classes?

You might ask why we use two sets of categories for specifying the VAT rate. Some other accounting programs do not have two different categories for the subtle difference between "exempt from VAT" and "VAT 0%", they have just a category "VAT rate" which you can set per invoice item (and a default value per provider).

The problem with this simplified vision is that at least for Belgian VAT declarations there is a big difference between having 0% of VAT because the provider is a private person and having 0% of VAT because you are buying post stamps or flight tickets (which are exempt from VAT).

Another thing to consider is that in Lino we want to be able to have partners who are both a provider and a customer. Their VAT regime remains the same for both trade types (sales and purchase) while the default VAT class to use in invoice items depends on the account or the product.

Account invoices

The lino_xl.lib.vat plugin provides the VatAccountInvoice voucher type. It is implemented in two database models:

class lino_xl.lib.vat.VatAccountInvoice

Django model for storing account vouchers.

A VAT-capable of account voucher. It is one of the most basic voucher types, which can be used even in accounting applications that don't have lino_xl.lib.sales.

class lino_xl.lib.vat.InvoiceItem

Django model for representing items of an account voucher.

There are several views:

class lino_xl.lib.vat.Invoices

The table of all VatAccountInvoice objects.

class lino_xl.lib.vat.InvoicesByJournal

Shows all invoices of a given journal (whose voucher_type must be VatAccountInvoice)

class lino_xl.lib.vat.PrintableInvoicesByJournal

Purchase journal

class lino_xl.lib.vat.InvoiceDetail

The detail layout used by Invoices.

class lino_xl.lib.vat.ItemsByInvoice
class lino_xl.lib.vat.VouchersByPartner

Utilites

The lino_xl.lib.vat.utils module contains some utility functions.

>>> from lino_xl.lib.vat.utils import add_vat, remove_vat
>>> add_vat(100, 21)
121.0
>>> remove_vat(121, 21)
100.0
>>> add_vat(10, 21)
12.1
>>> add_vat(1, 21)
1.21

Showing the invoices covered by a VAT declaration

The plugin defines two tables that show the invoices covered by a VAT declaration, IOW the invoices that have contributed to the numbers in the declaration.

class lino_xl.lib.vat.SalesByDeclaration

Show a list of all sales invoices whose VAT regime is Intra-Community.

class lino_xl.lib.vat.PurchasesByDeclaration

Show a list of all purchase invoices whose VAT regime is Intra-Community.

class lino_xl.lib.vat.VatInvoices

Common base class for SalesByDeclaration and PurchasesByDeclaration

Intracom sales and purchases

The plugin defines two reports accessible via the Reports ‣ Accounting menu and integrated in the printout of a VAT declaration:

class lino_xl.lib.vat.IntracomSales

Show a list of all sales invoices whose VAT regime is Intra-Community.

class lino_xl.lib.vat.IntracomPurchases

Show a list of all purchase invoices whose VAT regime is Intra-Community.

class lino_xl.lib.vat.IntracomInvoices

Common base class for IntracomSales and IntracomPurchases

These reports are empty when you have no national VAT module installed:

>>> rt.show(vat.IntracomSales, language='en')
... 
No data to display
>>> rt.show(vat.IntracomPurchases, language='en')
... 
No data to display

Model mixins

class lino_xl.lib.vat.VatSubjectable

Model mixin that defines the database fields vat_regime, and vat_id and related behaviour.

This is inherited by lino_xl.lib.contacts.Partner.

This mixin does nothing when lino_xl.lib.vat is not installed.

vat_regime

The default VAT regime to use on invoices for this partner.

vat_id

The VAT id used to identify this partner. Lino verifies validity based on the partner's country field.

class lino_xl.lib.vat.VatTotal

Model mixin that defines the database fields total_incl, total_base and total_vat and some related behaviour.

Used for both the voucher (VatDocument) and for each item (VatItemBase).

total_incl

The amount VAT included.

total_base

The amount VAT excluded.

total_vat

The amount of VAT.

All three total fields are lino.core.fields.PriceField instances.

The fields are editable by default, but implementing models can call lino.core.fields.update_field() to change this behaviour. A model that sets all fields to non-editable should also set edit_totals to False.

get_trade_type()

Subclasses of VatTotal must implement this method.

get_vat_rule()

Return the VAT rule for this voucher or voucher item. Called when user edits a total field in the document header when edit_totals is True.

total_base_changed()

Called when user has edited the total_base field. If total_base has been set to blank, then Lino fills it using reset_totals(). If user has entered a value, compute total_vat and total_incl from this value using the vat rate. If there is no VatRule, total_incl and total_vat are set to None.

If there are rounding differences, total_vat will get them.

total_vat_changed()

Called when user has edited the total_vat field. If it has been set to blank, then Lino fills it using reset_totals(). If user has entered a value, compute total_incl. If there is no VatRule, total_incl is set to None.

total_incl_changed()

Called when user has edited the total_incl field. If total_incl has been set to blank, then Lino fills it using reset_totals(). If user enters a value, compute total_base and total_vat from this value using the vat rate. If there is no VatRule, total_incl should be disabled, so this method will never be called.

If there are rounding differences, total_vat will get them.

class lino_xl.lib.vat.VatDocument

Abstract base class for invoices, offers and other vouchers.

Inherited by VatAccountInvoice as well as in other plugins (e.g. lino_xl.lib.sales.VatProductInvoice and lino_xl.lib.ana.AnaAccountInvoice).

Models that inherit this mixin can set the following class attribute:

edit_totals

Whether the user usually wants to edit the total amount or not.

The total fields of an invoice are not automatically updated each time an item is modified. Users must click the Σ ("Compute sums") button (or Save or the Register button) to see the invoice's totals.

Inherits the following database fields from VatTotal:

total_base
total_vat
total_incl

Adds the following database fields:

project

Pointer to a lino_xl.lib.ledger.Plugin.project_model.

partner

Mandatory field to be defined in the implementing class.

items_edited

An automatically managed boolean field which says whether the user has manually edited the items of this document. If this is False and edit_totals is True, Lino will automatically update the only invoice item according to partner and vat_regime and total_incl.

vat_regime

The VAT regime to be used in this document.

A pointer to VatRegimes.

Adds an action:

compute_sums

Calls ComputeSums for this document.

class lino_xl.lib.vat.ComputeSums

Compute the sum fields of a VatDocument based on its items.

Represented by a "Σ" button.

class lino_xl.lib.vat.VatItemBase

Model mixin for items of a VatDocument.

Abstract Base class for lino_xl.lib.ledger.InvoiceItem, i.e. the lines of invoices without unit prices and quantities.

Subclasses must define a field called "voucher" which must be a ForeignKey with related_name="items" to the "owning document", which in turn must be a subclass of VatDocument).

vat_class

The VAT class to be applied for this item. A pointer to VatClasses.

get_vat_rule(self, tt)

Return the VatRule which applies for this item.

tt is the trade type (which is the same for each item of a voucher, that's why we expect the caller to provide it).

This basically calls the class method VatRule.get_vat_rule() with appropriate arguments.

When selling certain products ("automated digital services") in the EU, you have to pay VAT in the buyer's country at that country's VAT rate. See e.g. How can I comply with VAT obligations?.

TODO: Add a new attribute VatClass.buyers_country or a checkbox Product.buyers_country or some other way to specify this.

class lino_xl.lib.vat.QtyVatItemBase

Model mixin for items of a VatTotal. Extends VatItemBase by adding unit_price and qty.

Abstract Base class for lino_xl.lib.sales.InvoiceItem and lino_xl.lib.sales.OrderItem, i.e. invoice items with unit prices and quantities.

unit_price

The unit price for this item.

qty

Changing the unit_price ot the qty will automatically reset the total amount of this item: the value unit_price * qty will be stored in total_incl if VatRegime.item_vat is True, otherwise in total_base.

VAT columns

class lino_xl.lib.vat.VatColumns

The global list of VAT columns.

The VAT column of a ledger account indicates where the movements on this account are to be collected in VAT declarations.

VAT declarations

class lino_xl.lib.vat.VatDeclaration

Abstract base class for models that represent a VAT declaration.

Inherits from lino_xl.lib.sepa.Payable lino_xl.lib.ledger.Voucher lino_xl.lib.excerpts.Certifiable lino_xl.lib.ledger.PeriodRange

accounting_period
intracom_statement_iterator()

Yield a list of lino_xl.lib.contacts.Partner objects, annotated with a field total_base that contains the sum of intra-community sales operations with this partner during the declared period range.

Usage example in Intra-community clients.

get_payable_sums_dict()

Implements lino_xl.lib.sepa.Payable.get_payable_sums_dict().

As a side effect this updates values in the computed fields of this declaration.

Declaration fields

Defining the declaration fields is responsibility of each national VAT module. But every individual field in every VAT declaration of every country is an instance of one of the following three classes:

class lino_xl.lib.vat.MvtDeclarationField

A declaration field to be computed by analyzing the ledger movements.

class lino_xl.lib.vat.WritableDeclarationField

A declaration field to be entered manually by the end user.

class lino_xl.lib.vat.SumDeclarationField

A declaration field that computes the sum of its observed fields.

All these three declaration field classes have a common ancestor DeclarationField.

class lino_xl.lib.vat.DeclarationField

Base class for all declaration fields.

It is not instantiated directly but by using one of its subclasses

editable

Whether the value of this field is to be manually entered by the end user.

Most fields are not editable, i.e. computed.

both_dc

Whether the value of this field is to be manually entered by the end user.

fieldnames

An optional space-separated list of names of observed fields, i.e. other declaration fields to be observed by this field. If a field name is prefixed by a "-", the observed field will additionally be inverted.

This is used only by sum fields. The values of all observed fields will be added, except inverted fields whose value will be subtracted.

Note that the booking direction (D or C) of the observed fields is ignored when computing the sum.

vat_regimes
vat_classes
vat_columns
exclude_vat_regimes
exclude_vat_classes
exclude_vat_columns
is_payable

Whether the value of this field represents an amount to be paid to the tax office.

class lino_xl.lib.vat.DeclarationFieldsBase
add_mvt_field()
add_sum_field()
add_writable_field()

Configuration

See also lino_xl.lib.vat.Plugin for configuration options.

About returnable VAT

The vat_returnable_account attribute tells Lino whether this is considered returnable VAT.

The VAT columns checker

class lino_xl.lib.vat.VatColumnsChecker

Check VAT columns configuration.

This is an unbound data checker (lino.modlib.checkdata.Checker.model is None), i.e. the messages aren't bound to a particular database object.

Who is member of the European Union?

The plugin attribute eu_country_codes is a set of ISO codes that are to be considered part of the EU. :

>>> pprint(dd.plugins.vat.eu_country_codes, compact=True)
{'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR',
 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PO', 'PT', 'RO', 'SE', 'SI',
 'SK'}

This is used to define the VAT area of a partner, which in turn influences the available VAT regimes. See lino_xl.lib.vat.VatAreas.

When a member state leaves or joins the EU (and you have partners there), you can either update your Lino (we plan to keep this list up to date), or you can change it locally. For example in your layouts_module you may write code like this:

if dd.today() > datetime.date(2025, 4, 11):
    dd.plugins.vat.eu_country_codes.add("GB")

if dd.today() > datetime.date(2025, 4, 11):
    dd.plugins.vat.eu_country_codes.remove("BE")

The isocode fields in your countries.Countries table must match the codes specified in the eu_country_codes plugin attribute.