📄 Déclaration TVA - Contrôles automatiques

Déclaration TVA - Contrôles automatiques

Source : account_reports/models/account_return.py (Enterprise 19.0). Les contrôles TVA ne passent pas par account.return.check.template (réservé à l'Audit) : ils sont générés dans _run_checks -> _check_suite_common_vat_report (+ VIES UE).

Orchestration

def _run_checks(self, check_codes_to_ignore):
self.ensure_one()
checks = []
report_country = self.type_id.report_id.country_id
europe_country_group = self.env.ref('base.europe')
if report_country.code in europe_country_group.mapped('country_ids.code'):
checks += self._check_suite_eu_vat_report(check_codes_to_ignore)

if self.is_tax_return:
checks += self._check_suite_common_vat_report(check_codes_to_ignore)
elif self.is_ec_sales_list_return:
checks += self._check_suite_common_ec_sales_list(check_codes_to_ignore)
if self.type_external_id == 'account_reports.annual_corporate_tax_return_type':
checks += self._check_suite_annual_closing(check_codes_to_ignore)

return checks


1. check_company_data - Données société

Pas de domaine ORM. Compte les champs société vides parmi vat, country_id, phone, email. Anomalie si count > 0.

if 'check_company_data' not in check_codes_to_ignore:
review_action = {
'type': 'ir.actions.act_window',
'name': _('Set your company data'),
'res_model': 'res.company',
'res_id': self.company_id.id,
'views': [(self.env.ref('account.res_company_form_view_onboarding').id, "form")],
'target': 'new',
}
company = self.company_id
required_fields = [company.vat, company.country_id, company.phone, company.email]
invalid_fields_count = sum(1 for field in required_fields if not field)

checks.append({
'name': _lt("Company data"),
'message': _lt(
"Missing company details (like VAT number or country) can cause errors in your report, "
"such as using the wrong VAT rate, wrongly exempting transactions."
),
'code': 'check_company_data',
'records_count': invalid_fields_count,
'action': review_action,
'result': 'anomaly' if invalid_fields_count else 'reviewed',
})


2. check_match_all_bank_entries - Rapprochement bancaire

Appelle _check_match_all_bank_entries. Modèle account.bank.statement.line.

if 'check_match_all_bank_entries' not in check_codes_to_ignore:
checks.append(self._check_match_all_bank_entries(
code='check_match_all_bank_entries',
name=_lt("Bank Matching"),
message=_lt("Bank matching isn't required for VAT returns but helps spot missing bills."),
)
)

def _check_match_all_bank_entries(self, code, name, message):
domain = [
('is_reconciled', '=', False),
('state', '!=', 'cancel'),
('company_id', 'in', self.company_ids.ids),
('date', '<=', fields.Date.to_string(self.date_to)),
('date', '>=', fields.Date.to_string(self.date_from)),
]
unreconciled_bank_entries_count = self.env['account.bank.statement.line'].sudo().search_count(
domain, limit=LIMIT_CHECK_ENTRIES
)
# result = anomaly if count else reviewed


3. check_draft_entries - Brouillons

exclude_entries=True pour la TVA : exclut move_type == 'entry'.

if 'check_draft_entries' not in check_codes_to_ignore:
check_codes_to_ignore.add('check_draft_entries')
checks.append(self._check_draft_entries(
code='check_draft_entries',
name=_lt("Draft entries"),
message=_lt("Review and post draft invoices and bills in the period, or change their accounting date."),
exclude_entries=True,
)
)

def _check_draft_entries(self, code, name, message, exclude_entries=False):
domain = [
('state', '=', 'draft'),
('company_id', 'in', self.company_ids.ids),
('date', '<=', fields.Date.to_string(self.date_to)),
('date', '>=', fields.Date.to_string(self.date_from)),
]
if exclude_entries:
domain += [('move_type', '!=', 'entry')]
draft_entries_count = self.env['account.move'].sudo().search_count(domain, limit=LIMIT_CHECK_ENTRIES)
# result = anomaly if count else reviewed


4. check_bills_attachment - Pièces jointes factures fournisseur

if 'check_bills_attachment' not in check_codes_to_ignore:
domain = [
('attachment_ids', '=', False),
('move_type', '=', 'in_invoice'),
('company_id', 'in', self.company_ids.ids),
('date', '<=', fields.Date.to_string(self.date_to)),
('date', '>=', fields.Date.to_string(self.date_from)),
('state', '=', 'posted'),
]
bills_without_attachments_count = self.env['account.move'].sudo().search_count(
domain, limit=LIMIT_CHECK_ENTRIES
)

review_action = {
'type': 'ir.actions.act_window',
'name': _("Bill Attachments"),
'view_mode': 'list',
'res_model': 'account.move',
'domain': domain,
'views': [[False, 'list'], [False, 'form']],
}

checks.append({
'name': _lt("Bill attachments"),
'code': 'check_bills_attachment',
'message': _lt("Each bill should have its own document attached as a proof in case of audit."),
'records_count': bills_without_attachments_count,
'records_model': self.env['ir.model']._get('account.move').id,
'action': review_action if bills_without_attachments_count else None,
'result': 'anomaly' if bills_without_attachments_count else 'reviewed',
})


5. check_tax_countries - Cohérence position fiscale / pays partenaire

Requête SQL (pas domaine ORM simple). Anomalie si ARRAY_AGG(move.id) non vide.

if 'check_tax_countries' not in check_codes_to_ignore:
self.env['account.move'].flush_model()
self.env['account.fiscal.position'].flush_model()
self.env['res.partner'].flush_model()
self.env['res.country.group'].flush_model()

self.env.cr.execute(SQL(
"""
SELECT ARRAY_AGG(move.id)
FROM account_move move
JOIN account_fiscal_position fpos
ON fpos.id = move.fiscal_position_id
JOIN res_partner partner
ON partner.id = move.commercial_partner_id
WHERE
move.state = 'posted'
AND move.company_id IN %(company_ids)s
AND move.move_type IN %(invoice_types)s
AND move.date >= %(date_from)s
AND move.date <= %(date_to)s
AND (fpos.country_id IS NOT NULL OR fpos.country_group_id IS NOT NULL)
AND (fpos.country_id IS NULL OR partner.country_id IS NULL
OR fpos.country_id != partner.country_id)
AND (
fpos.country_group_id IS NULL
OR partner.country_id IS NULL
OR NOT EXISTS (
SELECT 1
FROM res_country_res_country_group_rel group_rel
WHERE group_rel.res_country_id = partner.country_id
AND group_rel.res_country_group_id = fpos.country_group_id
)
)
""",
company_ids=tuple(self.company_ids.ids),
invoice_types=tuple(self.env['account.move'].get_invoice_types()),
date_from=fields.Date.to_string(self.date_from),
date_to=fields.Date.to_string(self.date_to),
))
country_error_move_ids = self.env.cr.fetchone()[0]
# result = anomaly if country_error_move_ids else reviewed


6. check_partner_vies - Validité VIES (UE, conditionnel)

Uniquement si base_vat installé et company.vat_check_vies = True. Appelé via _check_suite_eu_vat_report.

def _check_suite_eu_vat_report(self, check_codes_to_ignore):
checks = []
self._generic_vies_vat_check(check_codes_to_ignore, checks)
check_codes_to_ignore.add('check_partner_vies')
return checks

def _generic_vies_vat_check(self, check_codes_to_ignore, checks):
is_base_vat_installed = 'base_vat' in self.env['ir.module.module']._installed()
use_vies = is_base_vat_installed and self.company_id.vat_check_vies
if 'check_partner_vies' not in check_codes_to_ignore and use_vies:
european_country_group = self.env.ref('base.europe')
invalid_vies_partners = self.env['account.move'].sudo()._read_group(
domain=[
('partner_id.country_id', 'in', european_country_group.country_ids.ids),
('partner_id.country_id', '!=', self.company_id.account_fiscal_country_id.id),
('partner_id.vies_valid', '=', False),
('company_id', 'in', self.company_ids.ids),
('date', '<=', fields.Date.to_string(self.date_to)),
('date', '>=', fields.Date.to_string(self.date_from)),
('fiscal_position_id.vat_required', '=', True),
],
aggregates=['partner_id:recordset'],
)[0][0]
# result = anomaly if partners else reviewed


Rappels opératoires

  • refresh_checks() crée / met à jour les account.return.check (template_id = False pour la TVA).
  • Si refresh_result = False sur un check, le result manuel n'est plus écrasé au refresh.
  • Les templates account.return.check.template liés à return_type Audit ne s'appliquent pas au flux VAT.


Référence blog public : Déclaration de TVA Odoo 19 - décryptage des contrôles automatiques (Actualités RoPi) :
https://www.ropi-it.fr/blog/actualites-6/controles-automatiques-declaration-tva-odoo-19-30