From eb9ed7eff6a2206f7b2974c9961a8f304231f9a1 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 20 Aug 2024 21:51:03 +0200 Subject: [PATCH 1/7] [FIX] l10n_fr_account_vat_return: Journal entry generated in negative VAT scenario Add test to check the journal entry in negative scenario and improve tests on the generated journal entry in all scenarios Full re-implementation of the product/service distribution for autoliquidation VAT accounts: we now support autoliquidation VAT accounts in all journals (error if found in sale journal) and not just purchase journal. When auto computation of product/service distribution is not possible, we trigger a wizard to ask the user. Add message in chatter if ignoring draft moves. --- l10n_fr_account_vat_return/__manifest__.py | 3 +- .../data/l10n.fr.account.vat.box.csv | 4 +- .../models/account_tax.py | 2 +- .../models/l10n_fr_account_vat_return.py | 550 +++++++++++------- .../security/ir.model.access.csv | 3 + .../tests/test_fr_account_vat_return.py | 165 +++--- .../views/l10n_fr_account_vat_return.xml | 157 ++++- .../wizards/__init__.py | 1 + .../wizards/l10n_fr_vat_autoliq_manual.py | 67 +++ .../l10n_fr_vat_autoliq_manual_view.xml | 68 +++ .../wizards/l10n_fr_vat_draft_move_option.py | 15 +- 11 files changed, 744 insertions(+), 291 deletions(-) create mode 100644 l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual.py create mode 100644 l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual_view.xml diff --git a/l10n_fr_account_vat_return/__manifest__.py b/l10n_fr_account_vat_return/__manifest__.py index 70bd24f5e..b69ef9c1f 100644 --- a/l10n_fr_account_vat_return/__manifest__.py +++ b/l10n_fr_account_vat_return/__manifest__.py @@ -4,7 +4,7 @@ { "name": "France VAT Return", - "version": "16.0.3.2.0", + "version": "16.0.4.0.0", "category": "Accounting", "license": "AGPL-3", "summary": "VAT return for France: CA3, 3310-A, 3519", @@ -20,6 +20,7 @@ "wizards/res_config_settings.xml", "wizards/l10n_fr_account_vat_return_reimbursement_view.xml", "wizards/l10n_fr_vat_draft_move_option_view.xml", + "wizards/l10n_fr_vat_autoliq_manual_view.xml", "views/l10n_fr_account_vat_box.xml", "views/l10n_fr_account_vat_return.xml", "views/account_fiscal_position.xml", diff --git a/l10n_fr_account_vat_return/data/l10n.fr.account.vat.box.csv b/l10n_fr_account_vat_return/data/l10n.fr.account.vat.box.csv index 9ca8397c8..d979bae3b 100644 --- a/l10n_fr_account_vat_return/data/l10n.fr.account.vat.box.csv +++ b/l10n_fr_account_vat_return/data/l10n.fr.account.vat.box.csv @@ -83,7 +83,7 @@ ca3_lk,3310CA3,450,,I5-base,LK,MOA,911604,True,,False,,,,,,,,,"Importations - Ta ca3_ll,3310CA3,455,,I5-taxe,LL,MOA,,True,due_vat_extracom_product_210,False,40,l10n_fr_account_vat_return.ca3_gh,,210,l10n_fr_account_vat_return.ca3_lk,debit,,,"Importations - Taux réduit 2,1 % : taxe due","Importations - Taux réduit 2,1 % : taxe due",2,572,77 ca3_lm,3310CA3,460,,I6-base,LM,MOA,911606,True,,False,,,,,,,,,"Importations - Taux réduit 1,05 % : base HT","Importations - Taux réduit 1,05 % : base HT",2,498,63 ca3_ln,3310CA3,465,,I6-taxe,LN,MOA,,True,due_vat_extracom_product_dom_105,False,40,l10n_fr_account_vat_return.ca3_gh,,105,l10n_fr_account_vat_return.ca3_lm,debit,,,"Importations - Taux réduit 1,05 % : taxe due","Importations - Taux réduit 1,05 % : taxe due",2,572,63 -ca3_gg,3310CA3,500,,15,GG,MOA,100087,True,negative_deductible_vat,False,40,l10n_fr_account_vat_return.ca3_gh,,,,,,True,TVA antérieurement déduite à reverser,"TVA antérieurement déduite à reverser, taxe due",2,572,40 +ca3_gg,3310CA3,500,,15,GG,MOA,100087,True,negative_deductible_vat,False,40,l10n_fr_account_vat_return.ca3_gh,,,,credit,,True,TVA antérieurement déduite à reverser,"TVA antérieurement déduite à reverser, taxe due",2,572,40 ca3_ga,3310CA3,505,,15-dont-pp,GA,MOA,910761,True,,False,,,,,,,,True,TVA antérieurement déduite à reverser : dont TVA sur les produits pétroliers,"TVA antérieurement déduite à reverser, taxe due : dont TVA sur les produits pétroliers",2,326,50 ca3_lq,3310CA3,510,,15-dont-pi,LQ,MOA,911609,True,,False,,,,,,,,True,TVA antérieurement déduite à reverser : dont TVA sur les produits importés hors produits pétroliers,"TVA antérieurement déduite à reverser, taxe due : dont TVA sur les produits importés hors produits pétroliers",2,387,35 ca3_ks,3310CA3,520,,5B,KS,MOA,905877,True,,True,40,l10n_fr_account_vat_return.ca3_gh,,,,,,,"Sommes à ajouter, y compris acompte congés","Sommes à ajouter, y compris acompte congés",2,572,19 @@ -93,7 +93,7 @@ ca3_sub_section_tva_deductible,3310CA3,600,sub_section,,,,,True,,False,,,,,,,,,T ca3_hg,3310CA3,700,,23,HG,MOA,,True,deductible_vat_total,False,,,,,,,,,Total de la TVA déductible (lignes 19 à 2C),Total de la TVA déductible,3,569,636 ca3_ha,3310CA3,610,,19,HA,MOA,100090,True,deductible_vat_asset,False,40,l10n_fr_account_vat_return.ca3_hg,,,,credit,,,Biens constituant des immobilisations,TVA déductible sur biens constituant des immobilisations,3,572,745 ca3_hb,3310CA3,620,,20,HB,MOA,100091,True,deductible_vat_other,False,40,l10n_fr_account_vat_return.ca3_hg,,,,credit,,,Autres biens et services,TVA déductible sur autres biens et services (déduction sur facture),3,572,726 -ca3_hc,3310CA3,630,,21,HC,MOA,100092,True,negative_due_vat,False,40,l10n_fr_account_vat_return.ca3_hg,,,,credit,,True,Autre TVA à déduire,"Autre TVA à déduire, omissions ou compléments de déductions",3,572,712 +ca3_hc,3310CA3,630,,21,HC,MOA,100092,True,negative_due_vat,False,40,l10n_fr_account_vat_return.ca3_hg,,,,debit,,True,Autre TVA à déduire,"Autre TVA à déduire, omissions ou compléments de déductions",3,572,712 ca3_hk,3310CA3,640,,21-dont-pp,HK,MOA,910762,True,negative_due_vat_oil,False,,,,,,,,True,Dont régularisation de TVA sur les produits pétroliers,"Autres TVA à déduire, dont régularisation de TVA sur les produits pétroliers",3,220,702 ca3_lp,3310CA3,650,,21-dont-import,LP,MOA,911608,True,negative_due_vat_extracom_product,False,,,,,,,,True,Dont régularisation de TVA sur les produits importés (hors produits pétroliers),"Autres TVA à déduire, dont régularisation de TVA sur les produits importés (hors produits pétroliers)",3,293,694 ca3_hh,3310CA3,660,,21-dont-col-ded,HH,MOA,100120,True,negative_due_vat_regular,False,,,,,,,,True,Dont régularisations sur de la TVA collectée sur autres produits ou PS ou déductible,"Autre TVA à déduire, dont régularisation sur de la TVA collectée sur autres produits ou PS ou déductible",3,350,685 diff --git a/l10n_fr_account_vat_return/models/account_tax.py b/l10n_fr_account_vat_return/models/account_tax.py index 58eb7fee1..af4f2ac36 100644 --- a/l10n_fr_account_vat_return/models/account_tax.py +++ b/l10n_fr_account_vat_return/models/account_tax.py @@ -10,7 +10,7 @@ class AccountTax(models.Model): _inherit = "account.tax" fr_vat_autoliquidation = fields.Boolean( - compute="_compute_fr_vat_autoliquidation", store=True, string="Auto-Liquidation" + compute="_compute_fr_vat_autoliquidation", store=True, string="Autoliquidation" ) @api.depends( diff --git a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py index 359070e24..790739d58 100644 --- a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py +++ b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py @@ -16,7 +16,7 @@ from odoo import _, api, fields, models, tools from odoo.exceptions import UserError, ValidationError -from odoo.tools import date_utils, float_is_zero, float_round +from odoo.tools import date_utils, float_compare, float_is_zero, float_round from odoo.tools.misc import format_amount, format_date from .l10n_fr_account_vat_box import PUSH_RATE_PRECISION @@ -148,6 +148,14 @@ class L10nFrAccountVatReturn(models.Model): readonly=True, states={"manual": [("readonly", False)]}, ) + autoliq_line_ids = fields.One2many( + "l10n.fr.account.vat.return.autoliq.line", + "parent_id", + string="Autoliquidation Lines", + readonly=True, + ) + ignore_draft_moves = fields.Boolean() # technical field, not displayed + autoliq_manual_done = fields.Boolean() # technical field, not displayed _sql_constraints = [ ( @@ -335,6 +343,7 @@ def _prepare_speedy(self): "fp_frvattype2label": fp_frvattype2label, "line_obj": self.env["l10n.fr.account.vat.return.line"], "log_obj": self.env["l10n.fr.account.vat.return.line.log"], + "autoliq_line_obj": self.env["l10n.fr.account.vat.return.autoliq.line"], "box_obj": self.env["l10n.fr.account.vat.box"], "aa_obj": self.env["account.account"], "am_obj": self.env["account.move"], @@ -354,8 +363,111 @@ def _prepare_speedy(self): speedy["bank_cash_journals"] = speedy["aj_obj"].search( speedy["company_domain"] + [("type", "in", ("bank", "cash"))] ) + self._autoliq_prepare_speedy(speedy) return speedy + def _autoliq_prepare_speedy(self, speedy): + speedy.update( + { + "autoliq_taxedop_type2accounts": { + "intracom": speedy["aa_obj"], # recordset 445201, 445202, 445203 + "extracom": speedy["aa_obj"], # recordset 445301, 445302, 445303 + }, + "autoliq_vat_account2rate": {}, + # {445201: 2000, 445202: 1000, 445203: 55, 445301: 2000, } + "autoliq_tax2rate": {}, + # {TVA 20% intracom (achats): 2000, TVA 10% intracom (achats): 1000, } + } + ) + autoliq_vat_taxes = speedy["at_obj"].search( + speedy["purchase_autoliq_vat_tax_domain"] + ) + for tax in autoliq_vat_taxes: + lines = tax.invoice_repartition_line_ids.filtered( + lambda x: x.repartition_type == "tax" + and x.account_id + and int(x.factor_percent) == -100 + ) + if len(lines) != 1: + raise UserError( + _( + "On the autoliquidation tax '%(tax)s', the distribution for " + "invoices should have only one line -100% of tax, and not " + "%(count)s.", + tax=tax.display_name, + count=len(lines), + ) + ) + account = lines.account_id + rate_int = int(tax.amount * 100) + speedy["autoliq_tax2rate"][tax] = rate_int + if ( + account in speedy["autoliq_vat_account2rate"] + and speedy["autoliq_vat_account2rate"][account] != rate_int + ): + raise UserError( + _( + "Account '%(account)s' is used as due VAT account on several " + "autoliquidation taxes for different rates " + "(%(rate1).2f%% and %(rate2).2f%%).", + account=account.display_name, + rate1=rate_int / 100, + rate2=speedy["autoliq_vat_account2rate"][account] / 100, + ) + ) + # Since May 2023, the new strategy to separate goods vs services + # for intracom autoliq base is by analyzing unreconciled lines, + # and not by analysing the VAT period only (which requires that the balance + # of the account is 0 at the start of the period). + # So the minimum is to make sure that the account has reconcile=True ! + if not account.reconcile: + raise UserError( + _( + "Account '%s' is an account for autoliquidation, " + "so it's reconcile option must be enabled." + ) + % account.display_name + ) + speedy["autoliq_vat_account2rate"][account] = rate_int + tax_map = speedy["afpt_obj"].search( + [ + ("tax_dest_id", "=", tax.id), + ("company_id", "=", speedy["company_id"]), + ], + limit=1, + ) + if not tax_map: + raise UserError( + _( + "Autoliquidation tax '%s' is not present in the tax mapping " + "of any fiscal position." + ) + % tax.display_name + ) + autoliq_type = tax_map.position_id.fr_vat_type + if autoliq_type not in ("intracom_b2b", "extracom"): + raise UserError( + _( + "The autoliquidation tax '%(tax)s' is set on the tax mapping " + "of fiscal position '%(fp)s' which is configured with type " + "'%(fp_fr_vat_type)s'. Autoliquidation taxes should only be configured " + "on fiscal positions with type '%(fp_fr_vat_type_intracom_b2b)s' " + "or '%(fp_fr_vat_type_extracom)s'.", + tax=tax.display_name, + fp=tax_map.position_id.display_name, + fp_fr_vat_type=speedy["fp_frvattype2label"][autoliq_type], + fp_fr_vat_type_intracom_b2b=speedy["fp_frvattype2label"][ + "intracom_b2b" + ], + fp_fr_vat_type_extracom=speedy["fp_frvattype2label"][ + "extracom" + ], + ) + ) + if autoliq_type == "intracom_b2b": + autoliq_type = "intracom" + speedy["autoliq_taxedop_type2accounts"][autoliq_type] |= account + def _get_adjust_accounts(self, speedy): # This is the method to inherit if you want to select the appropriate # accounts via a configuration parameter @@ -426,10 +538,15 @@ def manual2auto(self): def back_to_manual(self): self.ensure_one() assert self.state in ("auto", "sent") + self.autoliq_line_ids.unlink() # del auto lines self.line_ids.filtered(lambda x: not x.box_manual).unlink() self._delete_move_and_attachments() - vals = {"state": "manual"} + vals = { + "state": "manual", + "ignore_draft_moves": False, + "autoliq_manual_done": False, + } if self.reimbursement_type: vals.update(self._prepare_remove_credit_vat_reimbursement()) self.write(vals) @@ -532,7 +649,7 @@ def _setup_data_pre_check(self, speedy): date=format_date(self.env, self.end_date), ) ) - elif not self._context.get("fr_vat_return_draft_force_continue"): + elif not self.ignore_draft_moves: action = self.env["ir.actions.actions"]._for_xml_id( "l10n_fr_account_vat_return.l10n_fr_vat_draft_move_option_action" ) @@ -567,7 +684,96 @@ def _setup_data_pre_check(self, speedy): ) % self.company_id.display_name ) - return None + action = self._generate_autoliq_lines(speedy) + return action + + def _generate_autoliq_lines(self, speedy): + self.ensure_one() + action = False + if self.autoliq_manual_done: + return action + elif self.autoliq_line_ids: + self.autoliq_line_ids.unlink() + + for autoliq_type in ("intracom", "extracom"): + autoliq_vat_move_lines = speedy["aml_obj"].search( + [ + ( + "account_id", + "in", + speedy["autoliq_taxedop_type2accounts"][autoliq_type].ids, + ), + ("balance", "!=", 0), + ("full_reconcile_id", "=", False), + ] + + speedy["base_domain_end"] + ) + for line in autoliq_vat_move_lines: + if line.journal_id.type == "sale": + raise UserError( + _( + "The journal item '%(line)s' has the autoliquidation " + "VAT account '%(account)s' and is in the sale journal " + "'%(journal)s'. Autoliquidation VAT accounts should " + "never be found in sale journals.", + line=line.display_name, + account=line.account_id.display_name, + journal=line.journal_id.display_name, + ) + ) + total = 0.0 + product_subtotal = 0.0 + move = line.move_id + rate_int = speedy["autoliq_vat_account2rate"][line.account_id] + is_invoice = move.is_invoice() + if is_invoice: + other_lines = move.invoice_line_ids.filtered( + lambda x: x.display_type == "product" + ) + else: + other_lines = move.line_ids.filtered( + lambda x: x.id != line.id and x.account_id.code.startswith("6") + ) + for oline in other_lines: + for tax in oline.tax_ids: + if ( + tax in speedy["autoliq_tax2rate"] + and speedy["autoliq_tax2rate"][tax] == rate_int + ): + total += oline.balance + product_or_service = oline._fr_is_product_or_service() + if product_or_service == "product": + product_subtotal += oline.balance + break + vals = { + "parent_id": self.id, + "move_line_id": line.id, + "autoliq_type": autoliq_type, + "vat_rate_int": rate_int, + } + if speedy["currency"].is_zero(total): + vals["compute_type"] = "manual" + autoliq_line = speedy["autoliq_line_obj"].create(vals) + if not action: + action = self.env["ir.actions.actions"]._for_xml_id( + "l10n_fr_account_vat_return.l10n_fr_vat_autoliq_manual_action" + ) + action["context"] = { + "default_fr_vat_return_id": self.id, + "default_line_ids": [], + } + action["context"]["default_line_ids"].append( + (0, 0, {"autoliq_line_id": autoliq_line.id}) + ) + else: + vals.update( + { + "compute_type": "auto", + "product_ratio": round(100 * product_subtotal / total, 2), + } + ) + speedy["autoliq_line_obj"].create(vals) + return action def _generate_ca3_bottom_totals(self, speedy): # Process the END of CA3 by hand @@ -867,7 +1073,7 @@ def _generate_due_vat(self, speedy): # Compute France and Monaco monaco_logs = self._generate_due_vat_france(speedy, type_rate2logs) - # Compute Auto-liquidation extracom + intracom + # Compute Autoliquidation extracom + intracom self._generate_due_vat_autoliq(speedy, type_rate2logs) # CREATE LINES @@ -998,21 +1204,34 @@ def _generate_due_vat_france(self, speedy, type_rate2logs): return monaco_logs def _generate_due_vat_autoliq(self, speedy, type_rate2logs): - ( - autoliq_taxedop_type2accounts, - autoliq_vat_account2rate, - autoliq_tax2rate, - ) = self._generate_due_vat_prepare_autoliq_struct(speedy) - # compute bloc "opérations imposables" / Intracom # Split product/service - autoliq_rate2product_ratio = self._compute_autoliq_rate2product_ratio( - speedy, autoliq_taxedop_type2accounts, autoliq_tax2rate - ) - + autoliq_rate2product_ratio = { + "intracom": {}, # {2000: {'total': 200.0, 'product_subtotal': 112.80}} + "extracom": {}, + } + for line in self.autoliq_line_ids: + if line.vat_rate_int not in autoliq_rate2product_ratio[line.autoliq_type]: + autoliq_rate2product_ratio[line.autoliq_type][ + line.vat_rate_int + ] = defaultdict(float) + # If the implementation was perfect, we would not have to use abs() ! + # But, in the current implementation, we take the balance of the autoliq VAT + # account and we apply a product ratio. With this implementation, we don't + # handle the case where autoliq product > 0 and autoliq service < 0 + # (or the opposite) which would require a special treatment. + # abs() introduces a distortion when we have positive and negative amounts + # in the autoliq lines. But, if we don't use it, we can have a ratio > 100 + balance = abs(line.move_line_id.balance) + autoliq_rate2product_ratio[line.autoliq_type][line.vat_rate_int][ + "total" + ] += balance + autoliq_rate2product_ratio[line.autoliq_type][line.vat_rate_int][ + "product_subtotal" + ] += speedy["currency"].round(balance * line.product_ratio / 100) # autoliq_intracom_product_logs = [] # for box 17 # Compute both block B and block A for autoliq intracom + extracom - for autoliq_type, accounts in autoliq_taxedop_type2accounts.items(): + for autoliq_type, accounts in speedy["autoliq_taxedop_type2accounts"].items(): # autoliq_type is 'intracom' or 'extracom' for account in accounts: total_vat_amount = ( @@ -1020,16 +1239,28 @@ def _generate_due_vat_autoliq(self, speedy, type_rate2logs): ) if speedy["currency"].is_zero(total_vat_amount): continue - rate_int = autoliq_vat_account2rate[account] + rate_int = speedy["autoliq_vat_account2rate"][account] # If you have a small residual amount in intracom/extracom autoliq accounts # and you set it to 0 with a write-off at a date after the VAT period, you # have 0 unreconciled move lines, but total_vat_amount != 0 # In such a corner case, there is not rate_int key in # autoliq_rate2product_ratio[autoliq_type] # => we consider product_ratio = 0% and service_ratio = 100% - product_ratio = autoliq_rate2product_ratio[autoliq_type].get( - rate_int, 0 - ) + product_ratio = 0 + if rate_int in autoliq_rate2product_ratio[autoliq_type]: + rate_data = autoliq_rate2product_ratio[autoliq_type][rate_int] + product_ratio = round( + 100 * rate_data["product_subtotal"] / rate_data["total"], 2 + ) + assert float_compare(product_ratio, 100, precision_digits=2) <= 0 + assert float_compare(product_ratio, 0, precision_digits=2) >= 0 + else: + logger.warning( + "rate_int %s not in autoliq_rate2product_ratio[%s]. " + "This can happen only in a very rare scenario.", + rate_int, + autoliq_type, + ) ratio = { "product": product_ratio, "service": 100 - product_ratio, @@ -1084,201 +1315,6 @@ def _generate_due_vat_autoliq(self, speedy, type_rate2logs): } type_rate2logs[ptype][rate_int].append(vat_log) - def _generate_due_vat_prepare_autoliq_struct(self, speedy): - autoliq_taxedop_type2accounts = { - "intracom": speedy["aa_obj"], # recordset 445201, 445202, 445203 - "extracom": speedy["aa_obj"], # recordset 445301, 445302, 445303 - } - autoliq_vat_account2rate = ( - {} - ) # {445201: 2000, 445202: 1000, 445203: 55, 445301: 2000, } - autoliq_tax2rate = ( - {} - ) # {TVA 20% intracom (achats): 2000, TVA 10% intracom (achats): 1000, } - autoliq_vat_taxes = speedy["at_obj"].search( - speedy["purchase_autoliq_vat_tax_domain"] - ) - for tax in autoliq_vat_taxes: - lines = tax.invoice_repartition_line_ids.filtered( - lambda x: x.repartition_type == "tax" - and x.account_id - and int(x.factor_percent) == -100 - ) - if len(lines) != 1: - raise UserError( - _( - "On the autoliquidation tax '%(tax)s', the distribution for " - "invoices should have only one line -100% of tax, and not " - "%(count)s.", - tax=tax.display_name, - count=len(lines), - ) - ) - account = lines.account_id - rate_int = int(tax.amount * 100) - autoliq_tax2rate[tax] = rate_int - if ( - account in autoliq_vat_account2rate - and autoliq_vat_account2rate[account] != rate_int - ): - raise UserError( - _( - "Account '%(account)s' is used as due VAT account on several " - "auto-liquidation taxes for different rates " - "(%(rate1).2f%% and %(rate2).2f%%).", - account=account.display_name, - rate1=rate_int / 100, - rate2=autoliq_vat_account2rate[account] / 100, - ) - ) - # Since May 2023, the new strategy to separate goods vs services - # for intracom autoliq base is by analyzing unreconciled lines, - # and not by analysing the VAT period only (which requires that the balance - # of the account is 0 at the start of the period). - # So the minimum is to make sure that the account has reconcile=True ! - if not account.reconcile: - raise UserError( - _( - "Account '%s' is an account for autoliquidation, " - "so it's reconcile option must be enabled." - ) - % account.display_name - ) - autoliq_vat_account2rate[account] = rate_int - tax_map = speedy["afpt_obj"].search( - [ - ("tax_dest_id", "=", tax.id), - ("company_id", "=", speedy["company_id"]), - ], - limit=1, - ) - if not tax_map: - raise UserError( - _( - "Auto-liquidation tax '%s' is not present in the tax mapping " - "of any fiscal position." - ) - % tax.display_name - ) - autoliq_type = tax_map.position_id.fr_vat_type - if autoliq_type not in ("intracom_b2b", "extracom"): - raise UserError( - _( - "The autoliquidation tax '%(tax)s' is set on the tax mapping " - "of fiscal position '%(fp)s' which is configured with type " - "'%(fp_fr_vat_type)s'. Autoliquidation taxes should only be configured " - "on fiscal positions with type '%(fp_fr_vat_type_intracom_b2b)s' " - "or '%(fp_fr_vat_type_extracom)s'.", - tax=tax.display_name, - fp=tax_map.position_id.display_name, - fp_fr_vat_type=speedy["fp_frvattype2label"][autoliq_type], - fp_fr_vat_type_intracom_b2b=speedy["fp_frvattype2label"][ - "intracom_b2b" - ], - fp_fr_vat_type_extracom=speedy["fp_frvattype2label"][ - "extracom" - ], - ) - ) - if autoliq_type == "intracom_b2b": - autoliq_type = "intracom" - autoliq_taxedop_type2accounts[autoliq_type] |= account - return ( - autoliq_taxedop_type2accounts, - autoliq_vat_account2rate, - autoliq_tax2rate, - ) - - def _compute_autoliq_rate2product_ratio( - self, speedy, autoliq_taxedop_type2accounts, autoliq_tax2rate - ): - autoliq_rate2product_ratio = { - "intracom": {}, # {2000: 54.80, 1000: 24.67, ...} - "extracom": {}, - } - for autoliq_type in ["intracom", "extracom"]: - rate2total = defaultdict(float) - rate2product = defaultdict(float) - - autoliq_vat_move_lines = speedy["aml_obj"].search( - [ - ( - "account_id", - "in", - autoliq_taxedop_type2accounts[autoliq_type].ids, - ), - ("balance", "!=", 0), - ("full_reconcile_id", "=", False), - ] - + speedy["base_domain_end"] - ) - for line in autoliq_vat_move_lines: - if line.journal_id.type != "purchase": - raise UserError( - _( - "Journal entry '%(move)s' dated %(date)s is inside or " - "before the VAT period %(vat_period)s " - "and has an unreconciled journal item with an " - "%(autoliq_type)s autoliquidation due VAT " - "account '%(account)s' in journal '%(journal)s' which " - "is not a purchase journal. That journal item should be " - "reconciled.", - move=line.move_id.display_name, - date=format_date(self.env, line.date), - vat_period=self.name, - autoliq_type=autoliq_type, - account=line.account_id.display_name, - journal=line.journal_id.display_name, - ) - ) - - autoliq_vat_moves = autoliq_vat_move_lines.move_id - for move in autoliq_vat_moves: - is_invoice = move.is_invoice() - if is_invoice: - lines = move.invoice_line_ids.filtered( - lambda x: x.display_type == "product" - ) - else: - # In case we have an entry in the purchase journal which is not an invoice - # While in v14 hr_expense created entries in the purchase journal - # with move_type = 'entry', in v16 it creates entries - # with move_type = 'in_invoice' - lines = move.line_ids.filtered( - lambda x: x.account_id.code.startswith("6") - ) - for line in lines: - rate_int = 0 - for tax in line.tax_ids: - if tax in autoliq_tax2rate: - rate_int = autoliq_tax2rate[tax] - if rate_int: - rate2total[rate_int] += line.balance - product_or_service = line._fr_is_product_or_service() - if product_or_service == "product": - rate2product[rate_int] += line.balance - elif is_invoice: - raise UserError( - _( - "There is a problem on the %(autoliq_type)s " - "%(move_type)s '%(move)s': " - "check that the invoice lines have a single autoliquidation " - "tax, and not the old dual-tax system for autoliquidation " - "which was used by Odoo up to version 12.0 included.", - move_type=speedy["movetype2label"][move.move_type], - move=move.display_name, - autoliq_type=autoliq_type, - ) - ) - - for rate_int, total in rate2total.items(): - productratio = 0 - if not speedy["currency"].is_zero(total): - productratio = round(100 * rate2product[rate_int] / total, 2) - autoliq_rate2product_ratio[autoliq_type][rate_int] = productratio - - return autoliq_rate2product_ratio - def _generate_taxed_op_and_due_vat_lines(self, speedy, type_rate2logs): # Create boxes 08, 09, 9B (columns base HT et Taxe due) vat_group_rate2box = {} @@ -1937,6 +1973,21 @@ def _prepare_account_move(self, speedy): lvals["debit"] = -amount lvals_list.append(lvals) logger.debug("VAT move account %s: %s", account.code, lvals) + # Adjustment should be only caused by rounding, so not more than a few euros + if speedy["currency"].compare_amounts(abs(total), 1) > 0: + raise UserError( + _( + "Error in the generation of the journal entry: the adjustment amount " + "is %s. The ajustment is only needed because, in the VAT " + "journal entry, the amount of the VAT to pay (or VAT credit) is " + "rounded (because the amounts are rounded in the VAT return) " + "and the other amounts are not rounded." + "As a consequence, the amount of the adjustment should be under 1 €. " + "This error may be caused by a bad configuration of the " + "accounting method of some VAT boxes." + ) + % format_amount(self.env, total, speedy["currency"]) + ) total_compare = speedy["currency"].compare_amounts(total, 0) total = speedy["currency"].round(total) if total_compare > 0: @@ -2491,3 +2542,72 @@ def _check_log_line(self): ) % log.parent_id.box_id.display_name ) + + +class L10nFrAccountVatReturnAutoliqLine(models.Model): + _name = "l10n.fr.account.vat.return.autoliq.line" + _description = "VAT Return Autoliq Line for France (CA3 line)" + _order = "parent_id, id" + _check_company_auto = True + + parent_id = fields.Many2one( + "l10n.fr.account.vat.return", string="VAT Return", ondelete="cascade" + ) + company_id = fields.Many2one(related="parent_id.company_id", store=True) + # no required=True, to avoid error if move line is deleted + move_line_id = fields.Many2one( + "account.move.line", string="Journal Item", check_company=True + ) + move_id = fields.Many2one( + related="move_line_id.move_id", string="Journal Entry", store=True + ) + journal_id = fields.Many2one(related="move_id.journal_id", store=True) + date = fields.Date(related="move_id.date", store=True) + partner_id = fields.Many2one(related="move_line_id.partner_id", store=True) + account_id = fields.Many2one(related="move_line_id.account_id", store=True) + ref = fields.Char(related="move_id.ref", store=True) + label = fields.Char(related="move_line_id.name", store=True) + company_currency_id = fields.Many2one( + related="move_line_id.company_currency_id", store=True + ) + debit = fields.Monetary( + related="move_line_id.debit", currency_field="company_currency_id", store=True + ) + credit = fields.Monetary( + related="move_line_id.credit", currency_field="company_currency_id", store=True + ) + product_ratio = fields.Float(digits=(16, 2)) + autoliq_type = fields.Selection( + [ + ("intracom", "Intracom"), + ("extracom", "Extracom"), + ], + required=True, + string="Type", + ) + compute_type = fields.Selection( + [ + ("auto", "Auto"), + ("manual", "Manual"), + ], + required=True, + ) + vat_rate_int = fields.Integer( + string="VAT Rate", required=True, help="VAT rate x 100" + ) + + @api.constrains("product_ratio") + def _check_autoliq_line(self): + for line in self: + if ( + float_compare(line.product_ratio, 0, precision_digits=2) < 0 + or float_compare(line.product_ratio, 100, precision_digits=2) > 0 + ): + raise ValidationError( + _( + "On journal item '%(move_line)s', the product ratio must be " + "between 0%% and 100%% (current value: %(ratio)s %%).", + move_line=line.move_line_id.display_name, + ratio=line.product_ratio, + ) + ) diff --git a/l10n_fr_account_vat_return/security/ir.model.access.csv b/l10n_fr_account_vat_return/security/ir.model.access.csv index 06957ff2f..79733560e 100644 --- a/l10n_fr_account_vat_return/security/ir.model.access.csv +++ b/l10n_fr_account_vat_return/security/ir.model.access.csv @@ -4,6 +4,9 @@ access_l10n_fr_account_vat_box_full,Full access on l10n.fr.account.vat.box,model access_l10n_fr_account_vat_return_full,Full access on l10n.fr.account.vat.return,model_l10n_fr_account_vat_return,account.group_account_user,1,1,1,1 access_l10n_fr_account_vat_return_line_full,Full access on l10n.fr.account.vat.return.line,model_l10n_fr_account_vat_return_line,account.group_account_user,1,1,1,1 access_l10n_fr_account_vat_return_line_log_full,Full access on l10n.fr.account.vat.return.line.log,model_l10n_fr_account_vat_return_line_log,account.group_account_user,1,1,1,1 +access_l10n_fr_account_vat_return_autoliq_line,Full access on l10n.fr.account.vat.return.autoliq.line,model_l10n_fr_account_vat_return_autoliq_line,account.group_account_user,1,1,1,1 access_l10n_fr_account_vat_return_reimbursement_full,Full access on l10n.fr.account.vat.return.reimbursement wizard,model_l10n_fr_account_vat_return_reimbursement,account.group_account_user,1,1,1,1 access_l10n_fr_vat_exigibility_update,Full access on l10n_fr_vat_exigibility_update wizard,model_l10n_fr_vat_exigibility_update,account.group_account_manager,1,1,1,1 access_l10n_fr_vat_draft_move_option,Full access on l10n.fr.vat.draft.move.option wizard,model_l10n_fr_vat_draft_move_option,account.group_account_user,1,1,1,1 +access_l10n_fr_vat_autoliq_manual,Full access on l10n.fr.vat.autoliq.manual wizard,model_l10n_fr_vat_autoliq_manual,account.group_account_user,1,1,1,1 +access_l10n_fr_vat_autoliq_manual_line,Full access on l10n.fr.vat.autoliq.manual.line wizard,model_l10n_fr_vat_autoliq_manual_line,account.group_account_user,1,1,1,1 diff --git a/l10n_fr_account_vat_return/tests/test_fr_account_vat_return.py b/l10n_fr_account_vat_return/tests/test_fr_account_vat_return.py index 90324d5fb..c70ff2239 100644 --- a/l10n_fr_account_vat_return/tests/test_fr_account_vat_return.py +++ b/l10n_fr_account_vat_return/tests/test_fr_account_vat_return.py @@ -8,6 +8,7 @@ from dateutil.relativedelta import relativedelta from odoo import fields +from odoo.exceptions import UserError from odoo.tests import tagged from odoo.tests.common import TransactionCase @@ -30,29 +31,39 @@ def setUpClass(cls): company_name="FR Company VAT on_payment", fr_vat_exigibility="on_payment" ) - def _check_vat_return_result(self, vat_return, result): + def _check_vat_return_result(self, vat_return, box_result, move_result): box2value = {} for line in vat_return.line_ids.filtered( lambda x: not x.box_display_type and x.box_edi_type == "MOA" ): box2value[(line.box_form_code, line.box_edi_code)] = line.value - for box_xmlid, expected_value in result.items(): + for box_xmlid, expected_value in box_result.items(): box = self.env.ref("l10n_fr_account_vat_return.%s" % box_xmlid) real_valuebox = box2value.pop((box.form_code, box.edi_code)) self.assertEqual(real_valuebox, expected_value) self.assertFalse(box2value) + move = vat_return.move_id + company = vat_return.company_id + self.assertTrue(move) + self.assertTrue(move.ref) + if vat_return.state in ("auto", "sent"): + self.assertEqual(move.state, "draft") + elif vat_return.state == "posted": + self.assertEqual(move.state, "posted") + self.assertEqual(move.date, vat_return.end_date) + self.assertEqual(move.journal_id, company.fr_vat_journal_id) - def _move_to_dict(self, move): - assert move - currency = move.company_id.currency_id + currency = company.currency_id move_dict = defaultdict(float) for line in move.line_ids: move_dict[line.account_id.code] += line.balance + for account_code, amount in move_result.items(): + self.assertFalse(currency.compare_amounts(move_dict[account_code], amount)) + # Check that 758 and 658 have an amount under 1 € self.assertEqual( currency.compare_amounts(move_dict.get("758000", 0) * -1, 1), -1 ) self.assertEqual(currency.compare_amounts(move_dict.get("658000", 0), 1), -1) - return move_dict def test_vat_return_on_invoice(self): company = self.on_invoice_company @@ -111,7 +122,7 @@ def test_vat_return_on_invoice(self): ) vat_return.manual2auto() self.assertEqual(vat_return.state, "auto") - expected_res = { + box_result = { "ca3_ca": 51510, # A "ca3_kh": 2210, # A3 HA intracom services "ca3_dk": 2810, # A4 HA extracom products @@ -155,27 +166,29 @@ def test_vat_return_on_invoice(self): "ca3_ja": 2566, # credit TVA (ligne 23 - 16) "ca3_jc": 2566, # crédit à reporter } - self._check_vat_return_result(vat_return, expected_res) - move = vat_return.move_id - self.assertTrue(move) - self.assertEqual(move.state, "draft") - self.assertEqual(move.date, vat_return.end_date) - self.assertEqual(move.journal_id, company.fr_vat_journal_id) - move_dict = self._move_to_dict(move) - self.assertFalse( - currency.compare_amounts(move_dict["447000"], expected_res["ca3_kb"] * -1) - ) - self.assertFalse( - currency.compare_amounts( - move_dict["445670"], expected_res["ca3_jc"] - expected_res["ca3_hd"] - ) - ) - self.assertFalse( - currency.compare_amounts(move_dict["635800"], expected_res["a_kj"]) - ) - self.assertFalse( - currency.compare_amounts(move_dict["635900"], expected_res["a_ud"]) - ) + move_result = { + "445711": 46, + "445712": 28, + "445713": 1265, + "445714": 588, + "445620": -1065, + "445660": -94.6, + "445201": 40, + "445202": 22, + "445203": 110, + "445204": 46.2, + "445662": -218.2, + "445301": 60, + "445302": 31, + "445303": 165, + "445304": 65.1, + "445663": -321.1, + "447000": box_result["ca3_kb"] * -1, + "445670": box_result["ca3_jc"] - box_result["ca3_hd"], + "635800": box_result["a_kj"], + "635900": box_result["a_ud"], + } + self._check_vat_return_result(vat_return, box_result, move_result) # Test reimbursement self.assertTrue(vat_return.reimbursement_show_button) @@ -190,34 +203,30 @@ def test_vat_return_on_invoice(self): } ) reimb_wiz.validate() - reimb_expected_res = dict(expected_res) - reimb_expected_res.update( + reimb_box_result = dict(box_result) + reimb_box_result.update( { "ca3_jb": reimbursement_amount, - "ca3_jc": expected_res["ca3_jc"] - reimbursement_amount, + "ca3_jc": box_result["ca3_jc"] - reimbursement_amount, } ) - self._check_vat_return_result(vat_return, reimb_expected_res) - self.assertEqual(vat_return.reimbursement_type, reimbursement_type) - self.assertEqual( - vat_return.reimbursement_first_creation_date, self.first_creation_date - ) - move = vat_return.move_id - move_dict = self._move_to_dict(move) - self.assertFalse( - currency.compare_amounts(move_dict["445830"], reimbursement_amount) + reimb_move_result = dict(move_result) + reimb_move_result.update( + { + "445830": reimbursement_amount, + "445670": reimb_box_result["ca3_jc"] - initial_credit_vat, + } ) # 445670: Do not mix the balance of the move and the balance of the # account. The balance of the account must be equal to - # reimb_expected_res["ca3_jc"] - self.assertFalse( - currency.compare_amounts( - initial_credit_vat + move_dict["445670"], reimb_expected_res["ca3_jc"] - ) + # reimb_box_result["ca3_jc"] + self._check_vat_return_result(vat_return, reimb_box_result, reimb_move_result) + self.assertEqual(vat_return.reimbursement_type, reimbursement_type) + self.assertEqual( + vat_return.reimbursement_first_creation_date, self.first_creation_date ) vat_return.remove_credit_vat_reimbursement() - move = vat_return.move_id - self._check_vat_return_result(vat_return, expected_res) + self._check_vat_return_result(vat_return, box_result, move_result) self.assertFalse(vat_return.reimbursement_type) self.assertFalse(vat_return.reimbursement_first_creation_date) vat_return.print_ca3() @@ -225,7 +234,7 @@ def test_vat_return_on_invoice(self): self.assertEqual(vat_return.state, "sent") vat_return.sent2posted() self.assertEqual(vat_return.state, "posted") - self.assertEqual(move.state, "posted") + self._check_vat_return_result(vat_return, box_result, move_result) aao = self.env["account.account"] speedy = vat_return._prepare_speedy() bal_zero_accounts = ["445711", "445712", "445713", "445714", "445715"] @@ -237,7 +246,7 @@ def test_vat_return_on_invoice(self): balance = acc._fr_vat_get_balance("base_domain_end", speedy) self.assertTrue(currency.is_zero(balance)) must_be_reconciled = bal_zero_accounts + ["445620"] - for line in move.line_ids: + for line in vat_return.move_id.line_ids: if line.account_id.code in must_be_reconciled: self.assertTrue(line.full_reconcile_id) @@ -260,7 +269,7 @@ def test_vat_return_on_payment(self): self.assertEqual(vat_return.state, "manual") vat_return.manual2auto() self.assertEqual(vat_return.state, "auto") - expected_res = { + box_result = { "ca3_ca": 40148, # A "ca3_kh": 2210, # A3 HA intracom services "ca3_dk": 2810, # A4 HA extracom products @@ -300,25 +309,33 @@ def test_vat_return_on_payment(self): "ca3_nd": 350, # TVA nette due (ligne TD - X5) "ca3_ke": 350, # Total à payer } - self._check_vat_return_result(vat_return, expected_res) - move = vat_return.move_id - self.assertTrue(move) - self.assertEqual(move.state, "draft") - self.assertEqual(move.date, vat_return.end_date) - self.assertEqual(move.journal_id, company.fr_vat_journal_id) - move_dict = self._move_to_dict(move) - self.assertFalse( - currency.compare_amounts(move_dict["445510"], expected_res["ca3_ke"] * -1) - ) - self.assertFalse( - currency.compare_amounts(move_dict["445670"], initial_credit_vat * -1) - ) + move_result = { + "445711": 37.5, + "445712": 21, + "445713": 1031.25, + "445714": 441, + "445620": -1065, + "445660": -94.6, + "445201": 40, + "445202": 22, + "445203": 110, + "445204": 46.2, + "445662": -218.2, + "445301": 60, + "445302": 31, + "445303": 165, + "445304": 65.1, + "445663": -321.1, + "445510": box_result["ca3_ke"] * -1, + "445670": initial_credit_vat * -1, + } + self._check_vat_return_result(vat_return, box_result, move_result) vat_return.print_ca3() vat_return.auto2sent() self.assertEqual(vat_return.state, "sent") vat_return.sent2posted() self.assertEqual(vat_return.state, "posted") - self.assertEqual(move.state, "posted") + self._check_vat_return_result(vat_return, box_result, move_result) aao = self.env["account.account"] speedy = vat_return._prepare_speedy() acc2bal = { @@ -336,7 +353,7 @@ def test_vat_return_on_payment(self): real_bal = acc._fr_vat_get_balance("base_domain_end", speedy) self.assertFalse(currency.compare_amounts(real_bal, expected_bal)) must_be_reconciled = ["445620"] - for line in move.line_ids: + for line in vat_return.move_id.line_ids: if line.account_id.code in must_be_reconciled: self.assertTrue(line.full_reconcile_id) @@ -414,7 +431,7 @@ def test_vat_return_on_invoice_negative(self): } ) vat_return.manual2auto() - expected_res = { + box_result = { "ca3_de": 660, # F8 "ca3_ce": 300, # B5 regul "ca3_gg": 76, # 15 TVA antérieurement déduite à reverser @@ -427,7 +444,21 @@ def test_vat_return_on_invoice_negative(self): "ca3_ja": initial_credit_vat - 76 + 50, # Crédit TVA "ca3_jc": initial_credit_vat - 76 + 50, # Crédit TVA } - self._check_vat_return_result(vat_return, expected_res) + move_result = { + "445711": -40, + "445712": -10, + "445660": 21, + "445620": 55, + "445670": box_result["ca3_jc"] - box_result["ca3_hd"], + } + self._check_vat_return_result(vat_return, box_result, move_result) + # Test that if box 15 has bad accounting method, the error is catched + vat_return.back_to_manual() + self.env.ref("l10n_fr_account_vat_return.ca3_gg").write( + {"accounting_method": "debit"} + ) + with self.assertRaises(UserError): + vat_return.manual2auto() def test_vat_return_on_invoice_with_adjustment(self): company = self.on_invoice_company diff --git a/l10n_fr_account_vat_return/views/l10n_fr_account_vat_return.xml b/l10n_fr_account_vat_return/views/l10n_fr_account_vat_return.xml index 7560a40f4..c1fb18d7d 100644 --- a/l10n_fr_account_vat_return/views/l10n_fr_account_vat_return.xml +++ b/l10n_fr_account_vat_return/views/l10n_fr_account_vat_return.xml @@ -163,6 +163,17 @@ name="reimbursement_comment_dgfip" nolabel="1" colspan="2" + /> + + + @@ -183,7 +194,7 @@ l10n.fr.account.vat.return - + + + l10n.fr.account.vat.return.autoliq.line + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + l10n.fr.account.vat.return.autoliq.line + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_fr_account_vat_return/wizards/__init__.py b/l10n_fr_account_vat_return/wizards/__init__.py index 9897b2df8..3a055bb5a 100644 --- a/l10n_fr_account_vat_return/wizards/__init__.py +++ b/l10n_fr_account_vat_return/wizards/__init__.py @@ -2,3 +2,4 @@ from . import l10n_fr_account_vat_return_reimbursement from . import l10n_fr_vat_exigibility_update from . import l10n_fr_vat_draft_move_option +from . import l10n_fr_vat_autoliq_manual diff --git a/l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual.py b/l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual.py new file mode 100644 index 000000000..6381babda --- /dev/null +++ b/l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual.py @@ -0,0 +1,67 @@ +# Copyright 2024 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class L10nFrVatAutoliqManual(models.TransientModel): + _name = "l10n.fr.vat.autoliq.manual" + _description = "FR VAT Return: ask product or service for autoliquidation lines" + + fr_vat_return_id = fields.Many2one( + "l10n.fr.account.vat.return", string="FR VAT Return", readonly=True + ) + line_ids = fields.One2many("l10n.fr.vat.autoliq.manual.line", "parent_id") + + def run(self): + for line in self.line_ids: + if line.option == "product": + vals = {"product_ratio": 100} + elif line.option == "service": + vals = {"product_ratio": 0} + elif line.option == "mix": + vals = {"product_ratio": line.product_ratio} + else: + raise UserError( + _("You must select product or service for journal item '%s'.") + % line.move_line_id.display_name + ) + line.autoliq_line_id.write(vals) + self.fr_vat_return_id.write({"autoliq_manual_done": True}) + self.fr_vat_return_id.manual2auto() + + +class L10nFrVatAutoliqManualLine(models.TransientModel): + _name = "l10n.fr.vat.autoliq.manual.line" + _description = "FR VAT Return: ask product or service on specific journal items" + + parent_id = fields.Many2one("l10n.fr.vat.autoliq.manual", ondelete="cascade") + autoliq_line_id = fields.Many2one( + "l10n.fr.account.vat.return.autoliq.line", required=True + ) + move_line_id = fields.Many2one(related="autoliq_line_id.move_line_id") + journal_id = fields.Many2one(related="move_line_id.journal_id") + date = fields.Date(related="move_line_id.date") + partner_id = fields.Many2one(related="move_line_id.partner_id") + account_id = fields.Many2one(related="move_line_id.account_id") + ref = fields.Char(related="move_line_id.move_id.ref") + label = fields.Char(related="move_line_id.name") + company_currency_id = fields.Many2one(related="move_line_id.company_currency_id") + debit = fields.Monetary( + related="move_line_id.debit", currency_field="company_currency_id" + ) + credit = fields.Monetary( + related="move_line_id.credit", currency_field="company_currency_id" + ) + option = fields.Selection( + [ + ("product", "Product"), + ("service", "Service"), + ("mix", "Mix"), + ], + string="Product or Service", + ) + product_ratio = fields.Float(digits=(16, 2), string="Product Radio (%)") + autoliq_type = fields.Selection(related="autoliq_line_id.autoliq_type") diff --git a/l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual_view.xml b/l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual_view.xml new file mode 100644 index 000000000..4f581dcf1 --- /dev/null +++ b/l10n_fr_account_vat_return/wizards/l10n_fr_vat_autoliq_manual_view.xml @@ -0,0 +1,68 @@ + + + + + + l10n.fr.vat.autoliq.manual + +
+
Odoo could not automatically compute the distribution between product and services on the journal items below that have a VAT autoliquidation account. Please indicate manually for each journal item below if the autoliquidation is related to a product or a service (you can also select 'mix' and indicate a percentage for the product part). +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Autoliquidation: Select product or service + l10n.fr.vat.autoliq.manual + form + new + + +
diff --git a/l10n_fr_account_vat_return/wizards/l10n_fr_vat_draft_move_option.py b/l10n_fr_account_vat_return/wizards/l10n_fr_vat_draft_move_option.py index 5ae2ff43f..d774bd0c3 100644 --- a/l10n_fr_account_vat_return/wizards/l10n_fr_vat_draft_move_option.py +++ b/l10n_fr_account_vat_return/wizards/l10n_fr_vat_draft_move_option.py @@ -2,7 +2,8 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import _, fields, models +from odoo.tools.misc import format_date class L10nFrVatDraftMoveOption(models.TransientModel): @@ -47,6 +48,12 @@ def option_show(self): def option_continue(self): self.ensure_one() - self.with_context( - fr_vat_return_draft_force_continue=True - ).fr_vat_return_id.manual2auto() + self.fr_vat_return_id.write({"ignore_draft_moves": True}) + self.fr_vat_return_id.message_post( + body=_( + "Ignoring %(count)s draft journal entries dated before %(end_date)s.", + count=self.draft_move_count, + end_date=format_date(self.env, self.end_date), + ) + ) + return self.fr_vat_return_id.manual2auto() From d9e4c8653fc1a48a1d3ae52b29e21dcc686a8967 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 27 Aug 2024 01:42:45 +0200 Subject: [PATCH 2/7] [IMP] l10n_fr_account_vat_return: use with statement to open file --- .../models/l10n_fr_account_vat_return.py | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py index 790739d58..7c3896341 100644 --- a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py +++ b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py @@ -2263,26 +2263,25 @@ def generate_ca3_attachment(self): watermark_pdf_reader_p2 = PdfReader(packet2) watermark_pdf_reader_p3 = PdfReader(packet3) # read your existing PDF - ca3_original_fd = tools.file_open( + with tools.file_open( "l10n_fr_account_vat_return/report/CA3_cerfa.pdf", "rb" - ) - ca3_original_reader = PdfReader(ca3_original_fd) - ca3_writer = PdfWriter() - # add the "watermark" (which is the new pdf) on the existing page - page1 = ca3_original_reader.pages[0] - page2 = ca3_original_reader.pages[1] - page3 = ca3_original_reader.pages[2] - page1.merge_page(watermark_pdf_reader_p1.pages[0]) - page2.merge_page(watermark_pdf_reader_p2.pages[0]) - page3.merge_page(watermark_pdf_reader_p3.pages[0]) - ca3_writer.add_page(page1) - ca3_writer.add_page(page2) - ca3_writer.add_page(page3) - # finally, write "output" to a real file - out_ca3_io = io.BytesIO() - ca3_writer.write(out_ca3_io) - out_ca3_bytes = out_ca3_io.getvalue() - ca3_original_fd.close() + ) as ca3_original_fd: + ca3_original_reader = PdfReader(ca3_original_fd) + ca3_writer = PdfWriter() + # add the "watermark" (which is the new pdf) on the existing page + page1 = ca3_original_reader.pages[0] + page2 = ca3_original_reader.pages[1] + page3 = ca3_original_reader.pages[2] + page1.merge_page(watermark_pdf_reader_p1.pages[0]) + page2.merge_page(watermark_pdf_reader_p2.pages[0]) + page3.merge_page(watermark_pdf_reader_p3.pages[0]) + ca3_writer.add_page(page1) + ca3_writer.add_page(page2) + ca3_writer.add_page(page3) + # finally, write "output" to a real file + out_ca3_io = io.BytesIO() + ca3_writer.write(out_ca3_io) + out_ca3_bytes = out_ca3_io.getvalue() filename = "CA3_%s.pdf" % self.display_name attach = self.env["ir.attachment"].create( From 8b4daaa3ff31843267f0354ef65e97fbdd78a39d Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 9 Sep 2024 16:29:10 +0200 Subject: [PATCH 3/7] [FIX] l10n_fr_account_vat_return: fix migration script if table doesn't exist yet --- .../migrations/0.0.0/pre-migration.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/l10n_fr_account_vat_return/migrations/0.0.0/pre-migration.py b/l10n_fr_account_vat_return/migrations/0.0.0/pre-migration.py index a7898e895..7c0a3d17a 100644 --- a/l10n_fr_account_vat_return/migrations/0.0.0/pre-migration.py +++ b/l10n_fr_account_vat_return/migrations/0.0.0/pre-migration.py @@ -5,14 +5,19 @@ # According to odoo/modules/migration.py, a special folder named '0.0.0' # can contain scripts that will be run on any version change +from openupgradelib import openupgrade -def migrate(cr, version): + +@openupgrade.migrate() +def migrate(env, version): # When data/l10n.fr.account.vat.box.csv is updated, # a box can take the previous value of another box located # in a row after it in the CSV, so it hits the SQL constraint before # reaching/updating the other box in the CSV # Set I set to null the fields that are in a unique SQL constraint - cr.execute( - "UPDATE l10n_fr_account_vat_box SET sequence=null, nref_code=null, " - "print_x=null, print_y=null, print_page=null, code=null" - ) + if openupgrade.table_exists(env.cr, "l10n_fr_account_vat_box"): + openupgrade.logged_query( + env.cr, + "UPDATE l10n_fr_account_vat_box SET sequence=null, nref_code=null, " + "print_x=null, print_y=null, print_page=null, code=null", + ) From 354f9bc4ccfb7775f143ed03cd554d840f22a9b7 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 22 Nov 2024 09:55:44 +0100 Subject: [PATCH 4/7] [IMP] l10n_fr_account_vat_return: add compress_content_streams() to reduce pdf file size --- .../models/l10n_fr_account_vat_return.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py index 7c3896341..c8d680753 100644 --- a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py +++ b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py @@ -2278,6 +2278,9 @@ def generate_ca3_attachment(self): ca3_writer.add_page(page1) ca3_writer.add_page(page2) ca3_writer.add_page(page3) + ca3_writer.pages[0].compress_content_streams() + ca3_writer.pages[1].compress_content_streams() + ca3_writer.pages[2].compress_content_streams() # finally, write "output" to a real file out_ca3_io = io.BytesIO() ca3_writer.write(out_ca3_io) From 8594d133a7545c4b1694c814ecdda8baa74f42d0 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 22 Nov 2024 10:10:23 +0100 Subject: [PATCH 5/7] [FIX] l10n_fr_account_vat_return: move ref for yearly periodicity --- .../models/l10n_fr_account_vat_return.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py index c8d680753..014b8dd6a 100644 --- a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py +++ b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py @@ -284,6 +284,17 @@ def company_id_change(self): ) self.start_date = fy_date_from + @api.depends("name", "vat_periodicity") + def name_get(self): + res = [] + for rec in self: + if rec.vat_periodicity == "12": + name = f"CA12 {rec.name}" + else: + name = f"CA3 {rec.name}" + res.append((rec.id, name)) + return res + def _prepare_speedy(self): # Generate a speed-dict called speedy that is used in several methods # or for some domains that we may need to inherit @@ -2012,7 +2023,7 @@ def _prepare_account_move(self, speedy): vals = { "date": self.end_date, "journal_id": self.company_id.fr_vat_journal_id.id, - "ref": "CA3 %s" % self.display_name, + "ref": self.display_name, "company_id": speedy["company_id"], "line_ids": [(0, 0, x) for x in lvals_list], } From 8d1306c55a1b61de0b82937ce25a5601b682d339 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 22 Nov 2024 13:15:56 +0100 Subject: [PATCH 6/7] [FIX] l10n_fr_account_vat_return: fix max amount for 658/758 --- .../models/l10n_fr_account_vat_return.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py index 014b8dd6a..c62c02afa 100644 --- a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py +++ b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py @@ -1984,8 +1984,11 @@ def _prepare_account_move(self, speedy): lvals["debit"] = -amount lvals_list.append(lvals) logger.debug("VAT move account %s: %s", account.code, lvals) - # Adjustment should be only caused by rounding, so not more than a few euros - if speedy["currency"].compare_amounts(abs(total), 1) > 0: + # On 1 due VAT or deductible VAT cell, the rounding effect can cause a gap of 0.50 € + # between due/deduc VAT account and VAT to pay (or VAT credit) + # We have 2 deduc VAT cells (immo and 5 regular) and 10 due VAT cells + # (5 VAT rates x 2 for regular and import) + if speedy["currency"].compare_amounts(abs(total), 0.5 * 12) > 0: raise UserError( _( "Error in the generation of the journal entry: the adjustment amount " From 4d97a2b9f7a3697e34ee26a83d526a514548fac3 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 22 Nov 2024 17:27:55 +0100 Subject: [PATCH 7/7] [IMP] l10n_fr_account_vat_return: reconciliation of 445670 now possible --- .../models/l10n_fr_account_vat_return.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py index c62c02afa..3442eed03 100644 --- a/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py +++ b/l10n_fr_account_vat_return/models/l10n_fr_account_vat_return.py @@ -959,7 +959,7 @@ def _generate_credit_deferment(self, speedy): raise UserError( _( "The balance of account '%(account)s' is %(balance)s. " - "It should always be positive.", + "It should always be positive or null.", account=account.display_name, balance=format_amount(self.env, balance, speedy["currency"]), ) @@ -2042,6 +2042,19 @@ def _reconcile_account_move(self, move, speedy): ["origin_move_id"], ) excluded_line_ids = [x["origin_move_id"][0] for x in excluded_lines] + # to allow reconciliation of 445670, we need to exclude the debit line + # from the reconciliation to have a balance at 0 + credit_vat_account = self._get_box_account( + speedy["meaning_id2box"]["credit_deferment"] + ) + credit_vat_debit_mline = speedy["aml_obj"].search( + [ + ("move_id", "=", move.id), + ("account_id", "=", credit_vat_account.id), + ("debit", ">", 0.9), + ], + limit=1, + ) for line in move.line_ids.filtered(lambda x: x.account_id.reconcile): account = line.account_id domain = speedy["base_domain_end"] + [ @@ -2049,6 +2062,8 @@ def _reconcile_account_move(self, move, speedy): ("full_reconcile_id", "=", False), ("move_id", "not in", excluded_line_ids), ] + if account == credit_vat_account and credit_vat_debit_mline: + domain.append(("id", "!=", credit_vat_debit_mline.id)) rg_res = speedy["aml_obj"].read_group(domain, ["balance"], []) # or 0 is need to avoid a crash: rg_res[0]["balance"] = None # when the moves are already reconciled @@ -2056,6 +2071,9 @@ def _reconcile_account_move(self, move, speedy): moves_to_reconcile = speedy["aml_obj"].search(domain) moves_to_reconcile.remove_move_reconcile() moves_to_reconcile.reconcile() + logger.info( + "Successful reconciliation in account %s", account.display_name + ) def _create_draft_account_move(self, speedy): self.ensure_one()