Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

trading : Product invoices

See also The trading plugin.

Snippets in this document are tested on the lino_book.projects.cosi2 demo project.

>>> from lino_book.projects.cosi2.startup import *
>>> ses = rt.login('robin')

The plugin

Lino implements product invoices in the lino_xl.lib.trading plugin. The internal codename was “sales” until 20240325, we renamed it because you might generate product invoices for other trade types as well.

The plugin needs and automatically installs the lino_xl.lib.products plugin.

It also needs and installs lino_xl.lib.vat (and not lino_xl.lib.vatless). Which means that if you want product invoices, you cannot not also install the VAT framework. If the site operator is not subject to VAT, you might add lino_xl.lib.bevats which hides most of the VAT functionality.

>>> dd.plugins.trading.needs_plugins
['lino.modlib.memo', 'lino_xl.lib.products', 'lino_xl.lib.vat']

This plugin may be combined with the lino_xl.lib.invoicing plugin which adds automatic generation of such product invoices.

Configuration settings

trading.print_items_table

The name of a data table to use when printing invoices using appypod print method.

This is a plugin setting.

Product invoices

class lino_xl.lib.trading.VatProductInvoice

The Django model representing a product invoice.

Inherits from lino_xl.lib.accounting.Voucher, TradingVoucher, Matching, lino_xl.lib.invoicing.InvoicingTargetVoucher and StorageTransferer.

Virtual fields:

balance_before

The balance of payments or debts that existed already before this voucher.

On a printed invoice, this amount should be mentioned and added to the invoice’s amount in order to get the total amount to pay.

balance_to_pay

The balance of all movements matching this invoice.

Methods:

get_print_items(self, ar):

For usage in an appy template:

do text
from table(obj.get_print_items(ar))
class lino_xl.lib.trading.InvoiceItem

The Django model representing an item of a product invoice.

class lino_xl.lib.trading.InvoiceDetail

The Lino layout representing the detail view of a product invoice.

class lino_xl.lib.trading.Invoices
class lino_xl.lib.trading.InvoicesByJournal

Shows all invoices of a given journal.

The master instance must be a journal having VatProductInvoice as lino_xl.lib.accounting.Journal.voucher_type.

class lino_xl.lib.trading.DueInvoices

Shows all due product invoices.

class lino_xl.lib.trading.TradingVoucherItem

Model mixin for voucher items that potentially refer to a product.

product

The product that is being sold or purchased.

description

A multi-line rich text to be printed in the resulting printable document.

discount

The percentage to subtract from the unit price of this item.

class lino_xl.lib.trading.ItemsByInvoicePrint

The table used to render items in a printable document.

description_print

TODO: write more about it.

class lino_xl.lib.trading.ItemsByInvoicePrintNoQtyColumn

Alternative column layout to be used when printing an invoice.

class lino_xl.lib.trading.SalesPrintable

Inherits from PartnerPrintable and Certifiable.

subject

A single-line text that describes this voucher.

paper_type

The type of paper to use when printing this voucher.

class lino_xl.lib.trading.TradingVoucher

Common base class for lino_xl.lib.orders.Order and VatProductInvoice.

Inherits from SalesPrintable and VatVoucher

Subclasses must either add themselves a date field (as does Order) or inherit it from Voucher (as does VatProductInvoice).

This model mixin sets edit_totals to False.

intro

An optional introduction text to be printed on the document.

default_discount

Default value for discount fields in the items of this voucher.

print_items_table

The table (column layout) to use in the printed document.

ItemsByInvoicePrint ItemsByInvoicePrintNoQtyColumn

Paper types

class lino_xl.lib.trading.PaperType

Describes a paper type (document template) to be used when printing an invoice.

A sample use case is to differentiate between invoices to get printed either on a company letterpaper for expedition via paper mail or into an email-friendly pdf file.

Inherits from lino.utils.mldbc.mixins.BabelNamed.

templates_group = 'trading/VatProductInvoice'

A class attribute.

template

Trade types

The plugin updates your lino_xl.lib.accounting.TradeTypes.sales, causing two additional database fields to be injected to lino_xl.lib.products.Product.

The first injected field is the sales price of a product:

>>> translation.activate('en')
>>> print(accounting.TradeTypes.sales.price_field_name)
sales_price
>>> print(accounting.TradeTypes.sales.price_field_label)
Sales price
>>> products.Product._meta.get_field('sales_price')
<lino.core.fields.PriceField: sales_price>

The other injected field is the sales base account of a product:

>>> print(accounting.TradeTypes.sales.base_account_field_name)
sales_account
>>> print(accounting.TradeTypes.sales.base_account_field_label)
Sales account
>>> products.Product._meta.get_field('sales_account')
<django.db.models.fields.related.ForeignKey: sales_account>

The sales journal

The cosi2 demo site has no VAT declarations, no purchase journals, no financial journals, just a single sales journal.

>>> rt.show('accounting.Journals', column_names="ref name trade_type")
=========== ================ ================== ============
 Reference   Designation      Designation (en)   Trade type
----------- ---------------- ------------------ ------------
 SLS         Factures vente   Sales invoices     Sales
=========== ================ ================== ============

Invoices are sorted by number and year. The entry date should normally never “go back”. Lino supports exceptional situations, e.g. starting to issue invoices at a given number or entering a series of sales invoices from a legacy system afterwards.

>>> jnl = rt.models.accounting.Journal.get_by_ref("SLS")
>>> rt.show('trading.InvoicesByJournal', jnl)
...
===================== ============ =========================== ========= =============== ================
 No.                   Date         Partner                     Subject   TotIncl         Workflow
--------------------- ------------ --------------------------- --------- --------------- ----------------
 1/2016                07/01/2016   Rumma & Ko OÜ                         2 999,85        **Registered**
 2/2016                08/01/2016   Bäckerei Ausdemwald                   2 039,82        **Registered**
 3/2016                09/01/2016   Bäckerei Mießen                       679,81          **Registered**
 4/2016                10/01/2016   Bäckerei Schmitz                      280,00          **Registered**
 5/2016                11/01/2016   Garage Mergelsberg                    535,00          **Registered**
 6/2016                07/02/2016   Donderweer BV                         1 110,16        **Registered**
 7/2016                08/02/2016   Van Achter NV                         1 499,85        **Registered**
 8/2016                09/02/2016   Hans Flott & Co                       1 939,82        **Registered**
 9/2016                10/02/2016   Bernd Brechts Bücherladen             815,96          **Registered**
 10/2016               07/03/2016   Reinhards Baumschule                  320,00          **Registered**
 11/2016               07/04/2016   Moulin Rouge                          548,50          **Registered**
 12/2016               08/04/2016   Auto École Verte                      2 013,88        **Registered**
 13/2016               09/04/2016   Adam Abel                             1 949,85        **Registered**
 14/2016               10/04/2016   Adami Adélaïde                        831,82          **Registered**
 15/2016               11/04/2016   Adriaen Aimé                          1 045,00        **Registered**
 16/2016               12/04/2016   Adriaen Aimé                          200,00          **Registered**
 17/2016               13/04/2016   Adriaensen Alfred                     140,60          **Registered**
 18/2016               14/04/2016   Adriaenssen Alphonse                  3 319,78        **Registered**
 19/2016               07/05/2016   Adriaenssens Albanie                  1 199,85        **Registered**
 20/2016               08/05/2016   Adriencense Alexine                   279,90          **Registered**
 21/2016               09/05/2016   Adriensence Anastase                  990,00          **Registered**
 22/2016               10/05/2016   Adrienssens Anatole                   239,20          **Registered**
 23/2016               11/05/2016   Aelter Alix                           3 005,45        **Registered**
 24/2016               12/05/2016   Aelter Alix                           2 359,78        **Registered**
 25/2016               07/06/2016   Aelterman Alma                        59,85           **Registered**
 26/2016               08/06/2016   Aelters Angèle                        580,00          **Registered**
 27/2016               09/06/2016   Aerens Appoline                       834,00          **Registered**
 28/2016               10/06/2016   Aerts Augustin                        11,20           **Registered**
 29/2016               11/06/2016   Aertsens Aymeric                      2 299,81        **Registered**
 30/2016               07/07/2016   Albumazard Barthélémy                 1 955,78        **Registered**
 31/2016               08/07/2016   Alloo Béranger                        450,00          **Registered**
 32/2016               09/07/2016   Alsteen Cyprien                       670,00          **Registered**
 33/2016               10/07/2016   Andersson Béatrice                    562,50          **Registered**
 34/2016               07/08/2016   Andries Edgar                         1 599,92        **Registered**
 35/2016               07/09/2016   Andriessen Céleste                    2 349,81        **Registered**
 36/2016               08/09/2016   André Ernest                          951,82          **Registered**
 37/2016               09/09/2016   Anthon Célie                          525,00          **Registered**
 38/2016               10/09/2016   Antoine Faustin                       600,00          **Registered**
 39/2016               11/09/2016   Appelbaum Elia                        2 140,50        **Registered**
 40/2016               12/09/2016   Appelbaum Elia                        799,92          **Registered**
 41/2016               13/09/2016   Applaer Félix                         1 719,81        **Registered**
 42/2016               14/09/2016   Arimont Gaston                        419,90          **Registered**
 43/2016               07/10/2016   Arquin Geoffroy                       600,00          **Registered**
 44/2016               08/10/2016   Arteman Grégoire                      489,20          **Registered**
 45/2016               09/10/2016   Baert Guillaume                       4 005,35        **Registered**
 46/2016               10/10/2016   Bartholomeeus Félicie                 1 039,92        **Registered**
 47/2016               11/10/2016   Bastien Gracianne                     379,81          **Registered**
 48/2016               12/10/2016   Bastien Gracianne                     740,00          **Registered**
 49/2016               07/11/2016   Bastin Honorine                       375,00          **Registered**
 50/2016               08/11/2016   Baugnet Louis                         310,20          **Registered**
 51/2016               09/11/2016   Baugniet Hélène                       3 599,71        **Registered**
 52/2016               10/11/2016   Baugniez Léopold                      639,92          **Registered**
 53/2016               11/11/2016   Bauwens Isabeau                       465,96          **Registered**
 54/2016               07/12/2016   Beauve Marius                         770,00          **Registered**
 55/2016               08/12/2016   Beck Max                              448,50          **Registered**
 56/2016               09/12/2016   Beckers Joséphine                     1 613,92        **Registered**
 57/2016               10/12/2016   Bernard Oscar                         3 149,71        **Registered**
 1/2017                07/01/2017   Bertrand Louise                       31,92           **Registered**
 2/2017                07/02/2017   Bietmé Rubens                         645,00          **Registered**
 3/2017                08/02/2017   Blaas Léona                           719,60          **Registered**
 4/2017                09/02/2017   Blankaert Thibaut                     21,00           **Registered**
 5/2017                10/02/2017   Blanquaert Théodore                   2 799,82        **Registered**
 6/2017                11/02/2017   Blondeel Théophile                    1 759,71        **Registered**
 7/2017                12/02/2017   Blondeel Théophile                    240,00          **Registered**
 8/2017                13/02/2017   Blondeeuw Tristan                     740,00          **Registered**
 9/2017                14/02/2017   Blondoo Mahaut                        494,80          **Registered**
 10/2017               07/03/2017   Bodart Margot                         2 999,85        **Registered**
 11/2017               08/03/2017   Bodson Virgile                        2 039,82        **Registered**
 12/2017               09/03/2017   Boeck Wilhem                          679,81          **Registered**
 13/2017               10/03/2017   Boesmans Aabdeen                      280,00          **Registered**
 14/2017               11/03/2017   Bogaert Aabid                         535,00          **Registered**
 15/2017               12/03/2017   Bogaert Aabid                         1 110,16        **Registered**
 **Total (72 rows)**                                                      **82 597,39**
===================== ============ =========================== ========= =============== ================
>>> mt = contenttypes.ContentType.objects.get_for_model(accounting.Journal).id
>>> obj = trading.VatProductInvoice.objects.get(journal__ref="SLS", number=20)
>>> url = '/api/trading/InvoicesByJournal/{0}'.format(obj.id)
>>> url += '?mt={0}&mk={1}&an=detail&fmt=json'.format(mt, obj.journal.id)
>>> test_client.force_login(rt.login('robin').user)
>>> res = test_client.get(url, REMOTE_USER='robin')
>>> # res.content
>>> r = check_json_result(res, "navinfo data disable_delete id param_values title")
>>> print(r['title'])
<a ...>Sales invoices (SLS)</a> » SLS 20/2016

IllegalText: The <text:section> element does not allow text

The following reproduces a situation which caused above error until 2015-11-11.

TODO: it is currently disabled for different reasons: leaves dangling temporary directories, does not reproduce the problem (probably because we must clear the cache).

>> obj = rt.models.trading.VatProductInvoice.objects.all()[0] >> obj VatProductInvoice #1 (‘SLS#1’) >> from lino.modlib.appypod.appy_renderer import AppyRenderer >> tplfile = rt.find_config_file(‘trading/VatProductInvoice/Default.odt’) >> context = dict() >> outfile = “tmp.odt” >> renderer = AppyRenderer(ses, tplfile, context, outfile) >> ar = rt.models.trading.ItemsByInvoicePrint.create_request(obj) >> print(renderer.insert_table(ar)) #doctest: +ELLIPSIS <table:table …</table:table-rows></table:table>

>> item = obj.items.all()[0] >> item.description = “”” … <p>intro:</p><ol><li>first</li><li>second</li></ol> … <p></p> … “”” >> item.save() >> print(renderer.insert_table(ar)) #doctest: +ELLIPSIS Traceback (most recent call last): … IllegalText: The <text:section> element does not allow text

The language of an invoice

The language of an invoice not necessary that of the user who enters the invoice. It is either the partner’s language or (if this is empty) the Site’s get_default_language.

Discount gets applied on the unit price

Currently the discount of an invoice item (TradingVoucherItem.discount gets applied to the unit price, not to the total amount. This may lead to surprising situations. For example, when the unit price is 0.01 and the quantity is 1000, the total_base remains 10.00 even with a discount of 40%, and it suddenly becomes 0.00 when you give more than 50% discount. TODO: is there a standard business practice for this?

>>> vch = trading.VatProductInvoice.objects.last()
>>> prd = products.Product(name="Nail", sales_price="0.01")
>>> prd.full_clean()
>>> prd.save()
>>> i = vch.add_voucher_item(product=prd, qty=1000)
>>> i.full_clean()
>>> i.product_changed()
>>> print(i.total_base)
10.00
>>> i.discount = 40
>>> i.discount_changed()
>>> print(i.total_base)
10.00
>>> i.discount = 51
>>> i.discount_changed()
>>> print(i.total_base)
0.00
>>> prd.delete()

Some fields of a registered voucher can remain editable

The default behaviour is that a registered voucher is not editable.

>>> UserTypes = rt.models.users.UserTypes
>>> InvoicesByJournal = rt.models.trading.InvoicesByJournal
>>> obj = InvoicesByJournal.model.objects.first()
>>> obj.state
<accounting.VoucherStates.registered:20>
>>> ar = rt.login("robin")
>>> actor = InvoicesByJournal
>>> actor.get_row_permission(
...     obj, ar, actor.get_row_state(obj), actor.update_action)
False

But if you set accounting.VoucherState.is_editable to True for the registered state, then the record itself becomes editable.

>>> accounting.VoucherStates.registered.is_editable = True
>>> actor.get_row_permission(
...     obj, ar, actor.get_row_state(obj), actor.update_action)
True
>>> obj.disabled_fields(obj.get_default_table().request(parent=ar))
{'clear_printed'}

Only the voucher state refuses editing, the actors don’t disable editing for all rows:

>>> rt.models.trading.InvoicesByJournal.editable
True
>>> InvoicesByJournal.hide_editing(UserTypes.admin)
False

TODO: Split is_editable of a voucher state into two booleans: fields_editable and row_editable. For example the partner field of a registered trading invoice must never be editable while the language field or some narration might remain editable.

>>> dbhash.check_virgin()