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>

Reply via email to