details: https://code.tryton.org/tryton/commit/eed02ddd61b3
branch: default
user: Cédric Krier <[email protected]>
date: Sat Jul 04 11:39:19 2026 +0200
description:
Replace the VIES wizard by an automatic background task
Closes #14930
diffstat:
modules/party/CHANGELOG | 1 +
modules/party/configuration.py | 10 +
modules/party/doc/design.rst | 15 +-
modules/party/doc/usage.rst | 9 -
modules/party/ir.py | 15 +
modules/party/message.xml | 3 +
modules/party/party.py | 207 +++++++++++++++++------
modules/party/party.xml | 26 +-
modules/party/tryton.cfg | 3 +-
modules/party/view/check_vies_result.xml | 7 -
modules/party/view/configuration_form.xml | 2 +
modules/party/view/identifier_form.xml | 8 +
modules/party/view/identifier_list.xml | 2 +
modules/party/view/identifier_list_sequence.xml | 2 +
14 files changed, 206 insertions(+), 104 deletions(-)
diffs (518 lines):
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/CHANGELOG
--- a/modules/party/CHANGELOG Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/CHANGELOG Sat Jul 04 11:39:19 2026 +0200
@@ -1,3 +1,4 @@
+* Replace the VIES wizard by an automatic background task
* Guess the type of contact mechanism
Version 8.0.0 - 2026-04-20
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/configuration.py
--- a/modules/party/configuration.py Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/configuration.py Sat Jul 04 11:39:19 2026 +0200
@@ -1,5 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+
+import datetime as dt
+
from trytond.i18n import gettext
from trytond.model import (
ModelSingleton, ModelSQL, ModelView, MultiValueMixin, ValueMixin, fields)
@@ -28,6 +31,9 @@
IDENTIFIER_TYPES, "Identifier Types", sort=False,
help="Defines which identifier types are available.\n"
"Leave empty for all of them.")
+ identifier_eu_vat_validation_period = fields.TimeDelta(
+ "European VAT Number Validation Period", required=True,
+ help="The period during which the number is still considered valid.")
@classmethod
def __register__(cls, module):
@@ -55,6 +61,10 @@
except KeyError:
return None
+ @classmethod
+ def default_identifier_eu_vat_validation_period(cls):
+ return dt.timedelta(days=365)
+
def get_identifier_types(self):
selection = self.fields_get(
['identifier_types'])['identifier_types']['selection']
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/doc/design.rst
--- a/modules/party/doc/design.rst Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/doc/design.rst Sat Jul 04 11:39:19 2026 +0200
@@ -43,17 +43,6 @@
Wizards
-------
-.. _wizard-party.check_vies:
-
-Check VIES
-^^^^^^^^^^
-
-The *Check* :abbr:`VIES (VAT Information Exchange System)` wizard uses the
-European Commission's `VIES web service`_ to verify that a party's
-VAT-identification number is valid.
-
-.. _VIES web service: https://ec.europa.eu/taxation_customs/vies/
-
.. _wizard-party.replace:
Replace
@@ -90,6 +79,10 @@
tax and vat registration numbers.
Most types of identifiers are checked by Tryton before they get saved to
ensure that they are valid.
+The :guilabel:`European VAT Number`'s are also checked automatically against
+the European Commission's `VIES web service`_.
+
+.. _VIES web service: https://ec.europa.eu/taxation_customs/vies/
.. _model-party.address:
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/doc/usage.rst
--- a/modules/party/doc/usage.rst Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/doc/usage.rst Sat Jul 04 11:39:19 2026 +0200
@@ -78,15 +78,6 @@
This, in turn, can be important as Tryton will automatically generate reports,
such as invoices and delivery notes, in the correct language for each party.
-.. _Checking VAT numbers are valid:
-
-Checking VAT numbers are valid
-==============================
-
-If the `Party <model-party.party>` is in the European Union you can use the
-`Check VIES <wizard-party.check_vies>` on it to check whether its VAT number
-is valid or not.
-
.. _Merging duplicate parties together:
Merging duplicate parties together
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/ir.py
--- a/modules/party/ir.py Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/ir.py Sat Jul 04 11:39:19 2026 +0200
@@ -6,6 +6,21 @@
from trytond.transaction import Transaction
+class Cron(metaclass=PoolMeta):
+ __name__ = 'ir.cron'
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls.method.selection.extend([
+ ('party.identifier|check_vies',
+ "Check European VAT Number with VIES"),
+ ])
+ cls._notifications.update({
+ 'party.identifier|check_vies',
+ })
+
+
class Email(metaclass=PoolMeta):
__name__ = 'ir.email'
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/message.xml
--- a/modules/party/message.xml Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/message.xml Sat Jul 04 11:39:19 2026 +0200
@@ -30,6 +30,9 @@
<record model="ir.message" id="msg_vies_unavailable">
<field name="text">The VIES service is unavailable, try again
later.</field>
</record>
+ <record model="ir.message" id="msg_party_identifier_eu_vat_invalid">
+ <field name="text">The VAT numbers "%(identifiers)s" are not valid
for the VIES service.</field>
+ </record>
<record model="ir.message" id="msg_different_name">
<field name="text">Parties have different names: "%(source_name)s"
vs "%(destination_name)s".</field>
</record>
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/party.py
--- a/modules/party/party.py Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/party.py Sat Jul 04 11:39:19 2026 +0200
@@ -1,12 +1,13 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+import datetime as dt
import logging
import re
from collections import defaultdict
import stdnum.exceptions
-from sql import Column, Literal
+from sql import Column, Literal, Null
from sql.aggregate import Min
from sql.functions import CharLength
from stdnum import get_cc_module
@@ -19,7 +20,7 @@
ValueMixin, convert_from, fields, sequence_ordered)
from trytond.model.exceptions import AccessError
from trytond.pool import Pool
-from trytond.pyson import Bool, Eval
+from trytond.pyson import Bool, Eval, If, PYSONEncoder
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from trytond.wizard import Button, StateTransition, StateView, Wizard
@@ -847,13 +848,47 @@
fields.Boolean("Type of Address"), 'on_change_with_type_address')
code = fields.Char('Code', required=True)
code_compact = fields.Char("Code Compact", readonly=True, required=True)
+ eu_vat_valid = fields.Boolean(
+ "European VAT Number Valid", readonly=True,
+ states={
+ 'invisible': Eval('type') != 'eu_vat',
+ },
+ help="Checked if the number has been validated on VIES service.")
+ eu_vat_validated_at = fields.DateTime(
+ "European VAT Number Validated At", readonly=True,
+ states={
+ 'invisible': Eval('type') != 'eu_vat',
+ })
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
cls.code_compact.search_unaccented = False
super().__setup__()
+ t = cls.__table__()
+ cls._sql_indexes.update({
+ Index(
+ t,
+ (t.active, Index.Equality(cardinality='low')),
+ (t.type, Index.Equality(cardinality='low')),
+ (t.eu_vat_valid, Index.Equality(cardinality='low')),
+ where=(t.type == 'eu_vat')
+ & ((t.eu_vat_valid == Literal(False))
+ | (t.eu_vat_valid == Null))
+ & (t.active == Literal(True))),
+ Index(
+ t,
+ (t.active, Index.Equality(cardinality='low')),
+ (t.type, Index.Equality(cardinality='low')),
+ (t.eu_vat_validated_at, Index.Range()),
+ where=(t.type == 'eu_vat')
+ & (t.active == Literal(True))),
+ })
cls.__access__.add('party')
+ cls._buttons.update(
+ check_vies_button={
+ 'invisible': Eval('type') != 'eu_vat',
+ })
@classmethod
def __register__(cls, module_name):
@@ -965,8 +1000,21 @@
values['code_compact'] = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
+ if mode == 'write':
+ if {'type', 'code'} & values.keys():
+ values['eu_vat_valid'] = None
+ values['eu_vat_validated_at'] = None
return values
+ @classmethod
+ def on_modification(cls, mode, identifiers, field_names=None):
+ super().on_modification(mode, identifiers, field_names=field_names)
+ if (mode == 'create'
+ or (mode == 'write' and (
+ field_names is None
+ or {'type', 'code'} & field_names))):
+ cls.__queue__.check_vies(identifiers)
+
def compute_fields(self, field_names=None):
values = super().compute_fields(field_names=field_names)
if field_names is None or {'type', 'code'} & field_names:
@@ -1015,68 +1063,107 @@
type=other.type_string,
code=other.code))
+ @classmethod
+ def copy(cls, identifiers, default=None):
+ default = default.copy() if default is not None else {}
+ default.setdefault('eu_vat_valid')
+ default.setdefault('eu_vat_validated_at')
+ return super().copy(identifiers, default=default)
-class CheckVIESResult(ModelView):
- __name__ = 'party.check_vies.result'
- parties_succeed = fields.Many2Many('party.party', None, None,
- 'Parties Succeed', readonly=True, states={
- 'invisible': ~Eval('parties_succeed'),
- })
- parties_failed = fields.Many2Many('party.party', None, None,
- 'Parties Failed', readonly=True, states={
- 'invisible': ~Eval('parties_failed'),
- })
+ @classmethod
+ def view_attributes(cls):
+ return super().view_attributes() + [
+ ('/tree', 'visual',
+ If((Eval('type') == 'eu_vat')
+ & ~Eval('eu_vat_valid'),
+ If(Eval('eu_vat_validated_at'),
+ 'danger',
+ 'warning'),
+ '')),
+ ]
-
-class CheckVIES(Wizard):
- __name__ = 'party.check_vies'
- start_state = 'check'
+ @classmethod
+ @ModelView.button
+ def check_vies_button(cls, identifiers):
+ cls.check_vies(identifiers)
- check = StateTransition()
- result = StateView('party.check_vies.result',
- 'party.check_vies_result', [
- Button('OK', 'end', 'tryton-ok', True),
- ])
+ @classmethod
+ def check_vies(cls, identifiers=None):
+ pool = Pool()
+ Configuration = pool.get('party.configuration')
+ Cron = pool.get('ir.cron')
- def transition_check(self):
- parties_succeed = []
- parties_failed = []
- for party in self.records:
- for identifier in party.identifiers:
- if identifier.type != 'eu_vat':
- continue
- eu_vat = get_cc_module('eu', 'vat')
- try:
- if not eu_vat.check_vies(identifier.code)['valid']:
- parties_failed.append(party.id)
- else:
- parties_succeed.append(party.id)
- except Exception as e:
- for msg in e.args:
- if msg == 'INVALID_INPUT':
- parties_failed.append(party.id)
- break
- elif msg in {
- 'SERVICE_UNAVAILABLE',
- 'MS_UNAVAILABLE',
- 'MS_MAX_CONCURRENT_REQ',
- 'GLOBAL_MS_MAX_CONCURRENT_REQ',
- 'TIMEOUT',
- 'SERVER_BUSY',
- }:
- raise VIESUnavailable(
- gettext('party.msg_vies_unavailable')) from e
- else:
- raise
- self.result.parties_succeed = parties_succeed
- self.result.parties_failed = parties_failed
- return 'result'
-
- def default_result(self, fields):
- return {
- 'parties_succeed': [p.id for p in self.result.parties_succeed],
- 'parties_failed': [p.id for p in self.result.parties_failed],
- }
+ config = Configuration(1)
+ now = dt.datetime.now()
+ if identifiers is None:
+ identifiers = cls.search([
+ ('type', '=', 'eu_vat'),
+ ['OR',
+ ('eu_vat_valid', '=', False),
+ ('eu_vat_validated_at', '<',
+ now - config.identifier_eu_vat_validation_period),
+ ],
+ ],
+ order=[])
+ if not identifiers:
+ return
+ failed, succeeded = [], []
+ for identifier in identifiers:
+ if identifier.type != 'eu_vat':
+ continue
+ eu_vat = get_cc_module('eu', 'vat')
+ try:
+ if not eu_vat.check_vies(identifier.code)['valid']:
+ failed.append(identifier)
+ else:
+ succeeded.append(identifier)
+ except ImportError:
+ # Missing dependencies so we consider the identifier as valid
+ succeeded.append(identifier)
+ except Exception as e:
+ for msg in e.args:
+ if msg == 'INVALID_INPUT':
+ failed.append(identifier)
+ break
+ elif msg in {
+ 'SERVICE_UNAVAILABLE',
+ 'MS_UNAVAILABLE',
+ 'MS_MAX_CONCURRENT_REQ',
+ 'GLOBAL_MS_MAX_CONCURRENT_REQ',
+ 'TIMEOUT',
+ 'SERVER_BUSY',
+ }:
+ raise VIESUnavailable(
+ gettext('party.msg_vies_unavailable')) from e
+ else:
+ raise
+ cls.write(succeeded, {
+ 'eu_vat_valid': True,
+ 'eu_vat_validated_at': now,
+ })
+ if failed:
+ encoder = PYSONEncoder()
+ cls.write(failed, {
+ 'eu_vat_valid': False,
+ 'eu_vat_validated_at': now,
+ })
+ names = ', '.join(i.rec_name for i in failed[:5])
+ if len(failed) < 5:
+ domain = [('id', 'in', [i.id for i in failed])]
+ else:
+ names += '...'
+ domain = [
+ ('type', '=', 'eu_vat'),
+ ('eu_vat_valid', '=', False),
+ ]
+ Cron.notify(
+ 'tryton-error',
+ 'party.act_identifier_form', {
+ 'pyson_domain': encoder.encode(domain),
+ },
+ 'party.msg_party_identifier_eu_vat_invalid',
+ identifiers=names,
+ )
class Replace(Wizard):
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/party.xml
--- a/modules/party/party.xml Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/party.xml Sat Jul 04 11:39:19 2026 +0200
@@ -147,21 +147,10 @@
sequence="20"
id="menu_identifier_form"/>
- <record model="ir.action.wizard" id="wizard_check_vies">
- <field name="name">Check VIES</field>
- <field name="wiz_name">party.check_vies</field>
- <field name="model">party.party</field>
- </record>
- <record model="ir.action.keyword" id="check_vies_keyword">
- <field name="keyword">form_action</field>
- <field name="model">party.party,-1</field>
- <field name="action" ref="wizard_check_vies"/>
- </record>
-
- <record model="ir.ui.view" id="check_vies_result">
- <field name="model">party.check_vies.result</field>
- <field name="type">form</field>
- <field name="name">check_vies_result</field>
+ <record model="ir.model.button" id="identifier_check_view_button">
+ <field name="model">party.identifier</field>
+ <field name="name">check_vies_button</field>
+ <field name="string">Check VIES</field>
</record>
<record model="ir.action.wizard" id="wizard_replace">
@@ -207,4 +196,11 @@
<field name="name">erase_ask_form</field>
</record>
</data>
+ <data noupdate="1">
+ <record model="ir.cron" id="cron_party_identifier_check_vies">
+ <field name="method">party.identifier|check_vies</field>
+ <field name="interval_number" eval="1"/>
+ <field name="interval_type">days</field>
+ </record>
+ </data>
</tryton>
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/tryton.cfg
--- a/modules/party/tryton.cfg Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/tryton.cfg Sat Jul 04 11:39:19 2026 +0200
@@ -15,13 +15,13 @@
[register]
model:
+ ir.Cron
country.PostalCode
category.Category
party.Party
party.PartyLang
party.PartyCategory
party.Identifier
- party.CheckVIESResult
party.ReplaceAsk
party.EraseAsk
address.Address
@@ -36,6 +36,5 @@
ir.EmailTemplate
ir.Channel
wizard:
- party.CheckVIES
party.Replace
party.Erase
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/view/check_vies_result.xml
--- a/modules/party/view/check_vies_result.xml Tue Jun 30 16:49:13 2026 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<form col="1">
- <field name="parties_succeed"/>
- <field name="parties_failed"/>
-</form>
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/view/configuration_form.xml
--- a/modules/party/view/configuration_form.xml Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/view/configuration_form.xml Sat Jul 04 11:39:19 2026 +0200
@@ -9,4 +9,6 @@
<separator name="identifier_types" colspan="4"/>
<field name="identifier_types" colspan="4" height="200"/>
+ <label name="identifier_eu_vat_validation_period"/>
+ <field name="identifier_eu_vat_validation_period"/>
</form>
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/view/identifier_form.xml
--- a/modules/party/view/identifier_form.xml Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/view/identifier_form.xml Sat Jul 04 11:39:19 2026 +0200
@@ -15,6 +15,14 @@
<label name="code"/>
<field name="code"/>
+ <group colspan="2" col="-1" id="eu_vat">
+ <label name="eu_vat_valid" string="Valid:"/>
+ <field name="eu_vat_valid"/>
+ <label name="eu_vat_validated_at" string="at"/>
+ <field name="eu_vat_validated_at"/>
+ </group>
+ <button name="check_vies_button" colspan="2"/>
+
<label name="address"/>
<field name="address"/>
</form>
diff -r 33139eb55d00 -r eed02ddd61b3 modules/party/view/identifier_list.xml
--- a/modules/party/view/identifier_list.xml Tue Jun 30 16:49:13 2026 +0200
+++ b/modules/party/view/identifier_list.xml Sat Jul 04 11:39:19 2026 +0200
@@ -6,4 +6,6 @@
<field name="address" expand="1"/>
<field name="type" expand="1"/>
<field name="code" expand="2"/>
+ <field name="eu_vat_valid" optional="1"/>
+ <field name="eu_vat_validated_at" widget="date" tree_invisible="1"/>
</tree>
diff -r 33139eb55d00 -r eed02ddd61b3
modules/party/view/identifier_list_sequence.xml
--- a/modules/party/view/identifier_list_sequence.xml Tue Jun 30 16:49:13
2026 +0200
+++ b/modules/party/view/identifier_list_sequence.xml Sat Jul 04 11:39:19
2026 +0200
@@ -6,4 +6,6 @@
<field name="address" expand="1"/>
<field name="type" expand="1"/>
<field name="code" expand="2"/>
+ <field name="eu_vat_valid" optional="1"/>
+ <field name="eu_vat_validated_at" widget="date" tree_invisible="1"/>
</tree>