From 7db50cfea120f302d7712db8ade24e5cacdd07d9 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 4 Nov 2022 17:54:40 +0100 Subject: [PATCH 1/7] l10n_fr_intrastat_* : update, improve and simplify l10n_fr_intrastat_product: - adapt to the very big changes I made in intrastat_product - DEB -> EMEBI - remove intrastat_fiscal_representative_id on res.partner - add a default intrastat transaction for B2C - filter intrastat_transaction_id on invoice form depending on move_type l10n_fr_intrastat_service: - auto-generate XML export upon confirmation to harmonize ergonomy with intrastat product - auto-delete XML upon back2draft - add confirmation pop-up when going back to draft - fix bug on total amount and number of lines when removing all lines - adapt for the fact that EU B2C fiscal positions now have intrastat=True --- l10n_fr_intrastat_product/__manifest__.py | 11 +- .../data/intrastat_product_reminder.xml | 27 +- .../data/intrastat_transaction.xml | 13 +- .../demo/intrastat_demo.xml | 17 +- l10n_fr_intrastat_product/models/__init__.py | 1 - .../models/intrastat_product_declaration.py | 297 ++++-------------- .../models/intrastat_transaction.py | 10 +- .../models/intrastat_unit.py | 2 +- .../models/res_company.py | 17 +- .../models/res_partner.py | 54 ---- l10n_fr_intrastat_product/models/stock.py | 11 +- l10n_fr_intrastat_product/post_install.py | 44 +-- .../readme/DESCRIPTION.rst | 4 +- l10n_fr_intrastat_product/readme/USAGE.rst | 2 +- .../security/intrastat_product_security.xml | 20 -- .../security/ir.model.access.csv | 4 - .../views/account_fiscal_position.xml | 20 ++ .../views/account_move.xml | 21 ++ .../views/intrastat_product_declaration.xml | 111 ++----- .../views/res_partner.xml | 22 -- .../models/intrastat_service.py | 27 +- .../readme/DESCRIPTION.rst | 2 +- .../views/intrastat_service_view.xml | 15 +- 23 files changed, 227 insertions(+), 525 deletions(-) delete mode 100644 l10n_fr_intrastat_product/models/res_partner.py delete mode 100644 l10n_fr_intrastat_product/security/intrastat_product_security.xml delete mode 100644 l10n_fr_intrastat_product/security/ir.model.access.csv create mode 100644 l10n_fr_intrastat_product/views/account_fiscal_position.xml create mode 100644 l10n_fr_intrastat_product/views/account_move.xml delete mode 100644 l10n_fr_intrastat_product/views/res_partner.xml diff --git a/l10n_fr_intrastat_product/__manifest__.py b/l10n_fr_intrastat_product/__manifest__.py index ea54a74b5..4c0a42334 100644 --- a/l10n_fr_intrastat_product/__manifest__.py +++ b/l10n_fr_intrastat_product/__manifest__.py @@ -1,13 +1,13 @@ -# Copyright 2010-2020 Akretion France (http://www.akretion.com) +# Copyright 2010-2022 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - "name": "DEB", + "name": "EMEBI", "version": "14.0.2.0.0", "category": "Localisation/Report Intrastat", "license": "AGPL-3", - "summary": "DEB (Déclaration d'Échange de Biens) for France", + "summary": "EMEBI (ex-DEB) for France", "author": "Akretion,Odoo Community Association (OCA)", "maintainers": ["alexis-via"], "website": "https://github.com/OCA/l10n-france", @@ -18,16 +18,15 @@ ], "data": [ "data/account_fiscal_position_template.xml", - "security/intrastat_product_security.xml", - "security/ir.model.access.csv", "views/intrastat_product_declaration.xml", + "views/account_fiscal_position.xml", "data/intrastat_transaction.xml", "views/intrastat_transaction.xml", "views/intrastat_unit.xml", "data/intrastat_product_reminder.xml", "views/res_config_settings.xml", - "views/res_partner.xml", "views/product_template.xml", + "views/account_move.xml", ], "post_init_hook": "set_fr_company_intrastat", "demo": ["demo/intrastat_demo.xml"], diff --git a/l10n_fr_intrastat_product/data/intrastat_product_reminder.xml b/l10n_fr_intrastat_product/data/intrastat_product_reminder.xml index 434e8be71..8c4e98986 100644 --- a/l10n_fr_intrastat_product/data/intrastat_product_reminder.xml +++ b/l10n_fr_intrastat_product/data/intrastat_product_reminder.xml @@ -7,13 +7,16 @@ - DEB Reminder + EMEBI Reminder 1 months -1 - + code model._scheduler_reminder() @@ -23,10 +26,10 @@ id="l10n_fr_intrastat_product_reminder_email_template" model="mail.template" > - DEB Reminder + EMEBI Reminder ${object.company_id.intrastat_email_list} ${object.declaration_type} DEB ${object.year_month} for ${object.company_id.name} + >${object.declaration_type} EMEBI ${object.year_month} for ${object.company_id.name} -

I would like to remind you that we are approaching the deadline for the DEB for month ${object.year_month}.

+

I would like to remind you that we are approaching the deadline for the EMEBI for month ${object.year_month}.

-

As there were no ${object.declaration_type} DEB for that month in Odoo, a draft ${object.declaration_type} DEB has been generated automatically by Odoo.

+

As there were no ${object.declaration_type} EMEBI for that month in Odoo, a draft ${object.declaration_type} EMEBI has been generated automatically by Odoo.

% if ctx.get('exception'): -

When trying to generate the lines of the ${object.declaration_type} DEB, the following error was encountered:

+

When trying to generate the lines of the ${object.declaration_type} EMEBI, the following error was encountered:

${ctx.get('error_msg')}

-

You should solve this error, then go to the menu "Invoicing > Reporting > Intrastat > DEB", open the ${object.declaration_type} declaration for month ${object.year_month} and click on the button "Generate lines from invoices".

+

You should solve this error, then go to the menu "Invoicing > Reporting > Intrastat > EMEBI", open the ${object.declaration_type} declaration for month ${object.year_month} and click on the button "Generate lines from invoices".

% else: % if object.num_lines and object.num_lines > 0: -

This draft ${object.declaration_type} DEB contains ${object.num_decl_lines} ${object.num_decl_lines == 1 and 'line' or 'lines'}.

+

This draft ${object.declaration_type} EMEBI contains ${object.num_decl_lines} ${object.num_decl_lines == 1 and 'line' or 'lines'}.

% else: -

This draft ${object.declaration_type} DEB generated automatically by Odoo doesn't contain any line.

+

This draft ${object.declaration_type} EMEBI generated automatically by Odoo doesn't contain any line.

% endif -

Go and check this declaration in Odoo in the menu "Invoicing > Reporting > Intrastat > DEB".

+

Go and check this declaration in Odoo in the menu "Invoicing > Reporting > Intrastat > EMEBI".

% endif diff --git a/l10n_fr_intrastat_product/data/intrastat_transaction.xml b/l10n_fr_intrastat_product/data/intrastat_transaction.xml index 36dec1203..a7e81c15c 100644 --- a/l10n_fr_intrastat_product/data/intrastat_transaction.xml +++ b/l10n_fr_intrastat_product/data/intrastat_transaction.xml @@ -1,6 +1,6 @@ @@ -19,7 +19,7 @@ Vente Client (Livraisons intracomm. exo. en France et taxables dans l'Etat d'arrivée) + >Vente Client B2B (Livraisons intracomm. exo. en France et taxables dans l'Etat d'arrivée)
21 out_invoice 11 @@ -27,6 +27,15 @@ 1 dispatches + + Vente Client B2C (soumises à TVA) + 29 + out_invoice + 12 + + 0 + dispatches + A12B - - - 1 @@ -29,10 +20,4 @@ PCE - - diff --git a/l10n_fr_intrastat_product/models/__init__.py b/l10n_fr_intrastat_product/models/__init__.py index 316820b60..9ba2e4e6b 100644 --- a/l10n_fr_intrastat_product/models/__init__.py +++ b/l10n_fr_intrastat_product/models/__init__.py @@ -1,5 +1,4 @@ from . import intrastat_transaction -from . import res_partner from . import intrastat_unit from . import stock from . import res_company diff --git a/l10n_fr_intrastat_product/models/intrastat_product_declaration.py b/l10n_fr_intrastat_product/models/intrastat_product_declaration.py index 8712e81c5..0ea5f5deb 100644 --- a/l10n_fr_intrastat_product/models/intrastat_product_declaration.py +++ b/l10n_fr_intrastat_product/models/intrastat_product_declaration.py @@ -1,4 +1,4 @@ -# Copyright 2009-2020 Akretion France (http://www.akretion.com) +# Copyright 2009-2022 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). @@ -9,7 +9,7 @@ from lxml import etree from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError logger = logging.getLogger(__name__) @@ -37,53 +37,36 @@ def _compute_fr_numbers(self): decl.num_decl_lines = num_lines decl.total_amount = total_amount - -class L10nFrIntrastatProductDeclaration(models.Model): - _name = "l10n.fr.intrastat.product.declaration" - _description = "Intrastat Product for France (DEB)" - _inherit = [ - "intrastat.product.declaration", - "mail.thread", - "mail.activity.mixin", - "report.intrastat_product.product_declaration_xls", - ] - - computation_line_ids = fields.One2many( - "l10n.fr.intrastat.product.computation.line", - "parent_id", - string="Intrastat Product Computation Lines", - states={"done": [("readonly", True)]}, - ) - declaration_line_ids = fields.One2many( - "l10n.fr.intrastat.product.declaration.line", - "parent_id", - string="Intrastat Product Declaration Lines", - states={"done": [("readonly", True)]}, - ) + @api.constrains("reporting_level", "declaration_type") + def _check_fr_declaration(self): + for decl in self: + if ( + decl.declaration_type == "arrivals" + and decl.reporting_level == "standard" + and decl.company_id.country_id.code == "FR" + ): + raise ValidationError( + _( + "In France, an arrival EMEBI cannot have a 'standard' reporting level." + ) + ) def _prepare_invoice_domain(self): domain = super()._prepare_invoice_domain() - for index, entry in enumerate(domain): - if entry[0] == "move_type": - domain.pop(index) if self.declaration_type == "arrivals": + for index, entry in enumerate(domain): + if entry[0] == "move_type": + domain.pop(index) domain.append(("move_type", "=", "in_invoice")) - elif self.declaration_type == "dispatches": - domain.append(("move_type", "in", ("out_invoice", "out_refund"))) return domain - def _get_product_origin_country(self, inv_line, notedict): - """Inherit to add warning when origin_country_id is missing""" - if ( - self.reporting_level == "extended" - and not inv_line.product_id.origin_country_id - ): - line_notes = [ - _("Missing country of origin on product '%s'.") - % inv_line.product_id.display_name - ] - self._format_line_note(inv_line, notedict, line_notes) - return super()._get_product_origin_country(inv_line, notedict) + def _get_region_code(self, inv_line, notedict): + if self.company_id.country_id.code != "FR": + return super()._get_region_code(inv_line, notedict) + else: + dpt = self._get_fr_department(inv_line, notedict) + region_code = dpt and dpt.code or False + return region_code def _get_fr_department(self, inv_line, notedict): dpt = False @@ -95,100 +78,29 @@ def _get_fr_department(self, inv_line, notedict): if po_line: wh = po_line.order_id.picking_type_id.warehouse_id if wh: - dpt = wh.get_fr_department() + dpt = wh._get_fr_department() elif po_line.move_ids: location = po_line.move_ids[0].location_dest_id - dpt = location.get_fr_department() + dpt = location._get_fr_department() elif move_type in ("out_invoice", "out_refund"): so_line = self.env["sale.order.line"].search( [("invoice_lines", "in", inv_line.id)], limit=1 ) if so_line: so = so_line.order_id - dpt = so.warehouse_id.get_fr_department() + dpt = so.warehouse_id._get_fr_department() if not dpt: dpt = self.company_id.partner_id.department_id - return dpt - - def _update_computation_line_vals(self, inv_line, line_vals, notedict): - super()._update_computation_line_vals(inv_line, line_vals, notedict) - if not line_vals.get("vat"): - inv = inv_line.move_id - commercial_partner = inv.commercial_partner_id - eu_countries = self.env.ref("base.europe").country_ids - if ( - commercial_partner.country_id not in eu_countries - and not commercial_partner.intrastat_fiscal_representative_id - ): + if not dpt: line_notes = [ _( - "Missing fiscal representative on partner '%s'." - % commercial_partner.display_name + "Missing department on partner '%s'. " + "To set it, set the country and the zip code on this partner." ) + % self.company_id.partner_id.display_name ] self._format_line_note(inv_line, notedict, line_notes) - else: - fiscal_rep = commercial_partner.intrastat_fiscal_representative_id - if not fiscal_rep.vat: - line_notes = [ - _( - "Missing VAT number on partner '%s' which is the " - "fiscal representative of partner '%s'." - % (fiscal_rep.display_name, commercial_partner.display_name) - ) - ] - self._format_line_note(inv_line, notedict, line_notes) - else: - line_vals["vat"] = fiscal_rep.vat - dpt = self._get_fr_department(inv_line, notedict) - line_vals["fr_department_id"] = dpt and dpt.id or False - - @api.model - def _group_line_hashcode_fields(self, computation_line): - res = super()._group_line_hashcode_fields(computation_line) - res["fr_department_id"] = computation_line.fr_department_id.id or False - return res - - @api.model - def _prepare_grouped_fields(self, computation_line, fields_to_sum): - vals = super()._prepare_grouped_fields(computation_line, fields_to_sum) - vals["fr_department_id"] = computation_line.fr_department_id.id - return vals - - def _get_region(self, inv_line, notedict): - # TODO : modify only for country == FR - return False - - @api.model - def _xls_template(self): - res = super()._xls_template() - res.update( - { - "fr_department": { - "header": { - "type": "string", - "value": self._("Department"), - }, - "line": { - "value": self._render("line.fr_department_id.display_name"), - }, - "width": 18, - } - } - ) - return res - - @api.model - def _xls_computation_line_fields(self): - field_list = super()._xls_computation_line_fields() - field_list += ["fr_department"] - return field_list - - @api.model - def _xls_declaration_line_fields(self): - field_list = super()._xls_declaration_line_fields() - field_list += ["fr_department"] - return field_list + return dpt def _generate_xml(self): """Generate the INSTAT XML file export.""" @@ -249,7 +161,7 @@ def _generate_xml(self): declaration_type_code = etree.SubElement(declaration, "declarationTypeCode") level2letter = { "standard": "4", - "extended": "5", # DEB 2022: stat + fisc, 2 in 1 combo + "extended": "5", # EMEBI 2022: stat + fisc, 2 in 1 combo } assert self.reporting_level in level2letter declaration_type_code.text = level2letter[self.reporting_level] @@ -288,7 +200,7 @@ def _generate_xml(self): @api.model def _scheduler_reminder(self): - logger.info("Start DEB reminder") + logger.info("Start EMEBI reminder") previous_month = datetime.strftime( datetime.today() + relativedelta(day=1, months=-1), "%Y-%m" ) @@ -350,7 +262,7 @@ def _scheduler_reminder(self): } ) logger.info( - "An %s DEB for month %s has been created by Odoo for " + "An %s EMEBI for month %s has been created by Odoo for " "company %s", declaration_type, previous_month, @@ -358,7 +270,7 @@ def _scheduler_reminder(self): ) intrastat.message_post( body=_( - "This DEB has been auto-generated by the DEB reminder " + "This EMEBI has been auto-generated by the EMEBI reminder " "scheduled action." ) ) @@ -372,7 +284,7 @@ def _scheduler_reminder(self): if company.intrastat_remind_user_ids: mail_template.send_mail(intrastat.id) logger.info( - "DEB Reminder email has been sent to %s", + "EMEBI Reminder email has been sent to %s", company.intrastat_email_list, ) else: @@ -381,83 +293,15 @@ def _scheduler_reminder(self): "is empty on company %s", company.display_name, ) - logger.info("End of the DEB reminder") + logger.info("End of the EMEBI reminder") return -class L10nFrIntrastatProductComputationLine(models.Model): - _name = "l10n.fr.intrastat.product.computation.line" - _description = "DEB computation lines" - _inherit = "intrastat.product.computation.line" - - parent_id = fields.Many2one( - "l10n.fr.intrastat.product.declaration", - string="Intrastat Product Declaration", - ondelete="cascade", - readonly=True, - ) - declaration_line_id = fields.Many2one( - "l10n.fr.intrastat.product.declaration.line", - string="Declaration Line", - readonly=True, - ) - fr_department_id = fields.Many2one( - "res.country.department", string="Department", ondelete="restrict" - ) - # the 2 fields below are useful for reports - amount_company_currency_sign = fields.Float( - compute="_compute_amount_company_currency_sign", store=True - ) - amount_accessory_cost_company_currency_sign = fields.Float( - compute="_compute_amount_company_currency_sign", store=True - ) - - @api.depends( - "amount_company_currency", - "amount_accessory_cost_company_currency", - "transaction_id.fr_fiscal_value_multiplier", - ) - def _compute_amount_company_currency_sign(self): - for line in self: - sign = line.transaction_id.fr_fiscal_value_multiplier or 1 - line.amount_company_currency_sign = sign * line.amount_company_currency - line.amount_accessory_cost_company_currency_sign = ( - sign * line.amount_accessory_cost_company_currency - ) - - -class L10nFrIntrastatProductDeclarationLine(models.Model): - _name = "l10n.fr.intrastat.product.declaration.line" - _description = "DEB declaration lines" +class IntrastatProductDeclarationLine(models.Model): _inherit = "intrastat.product.declaration.line" - parent_id = fields.Many2one( - "l10n.fr.intrastat.product.declaration", - string="Intrastat Product Declaration", - ondelete="cascade", - readonly=True, - ) - computation_line_ids = fields.One2many( - "l10n.fr.intrastat.product.computation.line", - "declaration_line_id", - string="Computation Lines", - readonly=True, - ) - fr_department_id = fields.Many2one( - "res.country.department", string="Departement", ondelete="restrict" - ) - # the field below is useful for reports - amount_company_currency_sign = fields.Float( - compute="_compute_amount_company_currency_sign", store=True - ) - - @api.depends("amount_company_currency", "transaction_id.fr_fiscal_value_multiplier") - def _compute_amount_company_currency_sign(self): - for line in self: - sign = line.transaction_id.fr_fiscal_value_multiplier or 1 - line.amount_company_currency_sign = sign * line.amount_company_currency - # flake8: noqa: C901 + # TODO update error message to avoid quoting declaration line number def _generate_xml_line(self, parent_node, eu_countries, line_number): self.ensure_one() decl = self.parent_id @@ -481,43 +325,21 @@ def _generate_xml_line(self, parent_node, eu_countries, line_number): su_code.text = iunit_id.fr_xml_label or iunit_id.name src_dest_country = etree.SubElement(item, "MSConsDestCode") - if not self.src_dest_country_id: - raise UserError( - _("Missing Country of Origin/Destination on line %d.") % line_number - ) - src_dest_country_code = self.src_dest_country_id.code - if ( - self.src_dest_country_id not in eu_countries - and src_dest_country_code != "GB" - ): + if not self.src_dest_country_code: raise UserError( - _( - "On line %d, the source/destination country is '%s', " - "which is not part of the European Union." - ) - % (line_number, self.src_dest_country_id.name) + _("Missing Country Code of Origin/Destination on line %d.") + % line_number ) - if src_dest_country_code == "GB" and decl.year >= "2021": - # all warnings are done during generation - src_dest_country_code = "XI" - src_dest_country.text = src_dest_country_code + src_dest_country.text = self.src_dest_country_code - # DEB 2022 : origin country is now for arrival AND dispatches + # EMEBI 2022 : origin country is now for arrival AND dispatches country_origin = etree.SubElement(item, "countryOfOriginCode") - if not self.product_origin_country_id: + if not self.product_origin_country_code: raise UserError( - _("Missing product country of origin on line %d.") % line_number + _("Missing product country of origin code on line %d.") + % line_number ) - country_origin_code = self.product_origin_country_id.code - # BOD dated 5/1/2021 says: - # Si, pour une marchandise produite au Royaume-Uni, - # le déclarant ignore si le lieu de production de la - # marchandise est situé en Irlande du Nord ou dans le - # reste du Royaume-Uni, il utilise également le code XU. - # => we always use XU - if country_origin == "GB" and decl.year >= "2021": - country_origin_code = "XU" - country_origin.text = country_origin_code + country_origin.text = self.product_origin_country_code weight = etree.SubElement(item, "netMass") if not self.weight: @@ -530,17 +352,18 @@ def _generate_xml_line(self, parent_node, eu_countries, line_number): raise UserError(_("Missing quantity on line %d.") % line_number) quantity_in_SU.text = str(self.suppl_unit_qty) - # START of elements that are part of all DEBs + # START of elements that are part of all EMEBIs invoiced_amount = etree.SubElement(item, "invoicedAmount") if not self.amount_company_currency: raise UserError(_("Missing fiscal value on line %d.") % line_number) invoiced_amount.text = str(self.amount_company_currency) - # DEB 2022 : Partner VAT now required for all dispatches + # EMEBI 2022 : Partner VAT now required for all dispatches with + # some exceptions for regime 29 in case of B2C if decl.declaration_type == "dispatches": partner_vat = etree.SubElement(item, "partnerId") - if not self.vat: + if not self.vat and transaction.code != "29": raise UserError(_("Missing VAT number on line %d.") % line_number) - if self.vat.startswith("GB") and decl.year >= "2021": + if self.vat and self.vat.startswith("GB") and decl.year >= "2021": raise UserError( _( "Bad VAT number '%s' on line %d. Brexit took place " @@ -549,8 +372,8 @@ def _generate_xml_line(self, parent_node, eu_countries, line_number): ) % (self.vat, line_number) ) - partner_vat.text = self.vat.replace(" ", "") - # Code régime is on all DEBs + partner_vat.text = self.vat and self.vat.replace(" ", "") or "" + # Code régime is on all EMEBIs statistical_procedure_code = etree.SubElement(item, "statisticalProcedureCode") statistical_procedure_code.text = transaction.code @@ -576,6 +399,6 @@ def _generate_xml_line(self, parent_node, eu_countries, line_number): ) mode_of_transport_code.text = str(self.transport_id.code) region_code = etree.SubElement(item, "regionCode") - if not self.fr_department_id: - raise UserError(_("Department is not set on line %d.") % line_number) - region_code.text = self.fr_department_id.code + if not self.region_code: + raise UserError(_("Region Code is not set on line %d.") % line_number) + region_code.text = self.region_code diff --git a/l10n_fr_intrastat_product/models/intrastat_transaction.py b/l10n_fr_intrastat_product/models/intrastat_transaction.py index b3af74c76..32cf5f5a8 100644 --- a/l10n_fr_intrastat_product/models/intrastat_transaction.py +++ b/l10n_fr_intrastat_product/models/intrastat_transaction.py @@ -1,7 +1,9 @@ -# Copyright 2010-2020 Akretion France (http://www.akretion.com/) +# Copyright 2010-2022 Akretion France (http://www.akretion.com/) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from textwrap import shorten + from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -48,7 +50,7 @@ class IntrastatTransaction(models.Model): ("99", "99"), ], string="Transaction code", - help="For the 'DEB' declaration to France's customs " + help="For the 'EMEBI' declaration to France's customs " "administration, you should enter the number 'nature de la " "transaction' here.", ) @@ -58,12 +60,12 @@ class IntrastatTransaction(models.Model): ) fr_fiscal_value_multiplier = fields.Integer( string="Fiscal value multiplier", + default=1, help="'0' for procedure codes 19 and 29, " "'-1' for procedure code 25, '1' for all the others. " "This multiplier is used to compute the total fiscal value of " "the declaration.", ) - # TODO : see with Luc if we can move it to intrastat_product fr_intrastat_product_type = fields.Selection( [ ("arrivals", "Arrivals"), @@ -121,6 +123,6 @@ def name_get(self): name += "/%s" % trans.fr_transaction_code if trans.description: name += " " + trans.description - name = len(name) > 55 and name[:55] + "..." or name + name = shorten(name, 55, placeholder="...") res.append((trans.id, name)) return res diff --git a/l10n_fr_intrastat_product/models/intrastat_unit.py b/l10n_fr_intrastat_product/models/intrastat_unit.py index 5d20a5d99..44b261c1e 100644 --- a/l10n_fr_intrastat_product/models/intrastat_unit.py +++ b/l10n_fr_intrastat_product/models/intrastat_unit.py @@ -1,4 +1,4 @@ -# Copyright 2010-2020 Akretion France (http://www.akretion.com) +# Copyright 2010-2022 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). diff --git a/l10n_fr_intrastat_product/models/res_company.py b/l10n_fr_intrastat_product/models/res_company.py index 925f6eca5..713c2e158 100644 --- a/l10n_fr_intrastat_product/models/res_company.py +++ b/l10n_fr_intrastat_product/models/res_company.py @@ -1,4 +1,4 @@ -# Copyright 2010-2020 Akretion France (http://www.akretion.com) +# Copyright 2010-2022 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -16,14 +16,17 @@ class ResCompany(models.Model): fr_intrastat_accreditation = fields.Char( string="Customs accreditation identifier", size=4, - help="Company identifier for Intrastat file export. " "Size : 4 characters.", + help="Company identifier for Intrastat file export. Size: 4 characters.", ) @api.constrains("intrastat_arrivals", "country_id") def check_fr_intrastat(self): for company in self: - if company.country_id and company.country_id.code == "FR": - if company.intrastat_arrivals == "standard": - raise ValidationError( - _("In France, Arrival DEB can only be Exempt " "or Extended.") - ) + if ( + company.country_id + and company.country_id.code == "FR" + and company.intrastat_arrivals == "standard" + ): + raise ValidationError( + _("In France, Arrival EMEBI can only be Exempt or Extended.") + ) diff --git a/l10n_fr_intrastat_product/models/res_partner.py b/l10n_fr_intrastat_product/models/res_partner.py deleted file mode 100644 index 2f3ef6771..000000000 --- a/l10n_fr_intrastat_product/models/res_partner.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2011-2020 Akretion France (http://www.akretion.com) -# @author Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError - - -class ResPartner(models.Model): - _inherit = "res.partner" - - intrastat_fiscal_representative_id = fields.Many2one( - "res.partner", - string="EU fiscal representative", - domain=[("parent_id", "=", False)], - help="If this partner is located outside the EU but you " - "deliver the goods inside the UE, the partner needs to " - "have a fiscal representative with a VAT number inside the EU. " - "In this scenario, the VAT number of the fiscal representative " - "will be used for the Intrastat Product report (DEB).", - ) - - @api.constrains("intrastat_fiscal_representative_id") - def _check_fiscal_representative(self): - """The Fiscal rep. must be based in the same country as our - company or in an intrastat country""" - eu_countries = self.env.ref("base.europe").country_ids - for partner in self: - rep = partner.intrastat_fiscal_representative_id - if rep: - if not rep.country_id: - raise ValidationError( - _( - "The fiscal representative '%s' of partner '%s' " - "must have a country." - ) - % (rep.display_name, partner.display_name) - ) - if rep.country_id not in eu_countries: - raise ValidationError( - _( - "The fiscal representative '%s' of partner '%s' " - "must be based in an EU country." - ) - % (rep.display_name, partner.display_name) - ) - if not rep.vat: - raise ValidationError( - _( - "The fiscal representative '%s' of partner '%s' " - "must have a VAT number." - ) - % (rep.display_name, partner.display_name) - ) diff --git a/l10n_fr_intrastat_product/models/stock.py b/l10n_fr_intrastat_product/models/stock.py index 7013cfe7c..ada275dfd 100644 --- a/l10n_fr_intrastat_product/models/stock.py +++ b/l10n_fr_intrastat_product/models/stock.py @@ -1,4 +1,4 @@ -# Copyright 2010-2020 Akretion France (http://www.akretion.com) +# Copyright 2010-2022 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -9,7 +9,7 @@ class StockWarehouse(models.Model): _inherit = "stock.warehouse" - def get_fr_department(self): + def _get_fr_department(self): self.ensure_one() if not self.partner_id: raise UserError(_("Missing partner on warehouse '%s'.") % self.display_name) @@ -19,14 +19,11 @@ def get_fr_department(self): class StockLocation(models.Model): _inherit = "stock.location" - def get_fr_department(self): - """I don't think it's a good idea to use the get_intrastat_region() - of intrastat_product because it doesn't return the same object. - That's why there is a small code duplication in this method""" + def _get_fr_department(self): self.ensure_one() warehouse = self.env["stock.warehouse"].search( [("lot_stock_id", "parent_of", self.ids)], limit=1 ) if warehouse: - return warehouse.get_fr_department() + return warehouse._get_fr_department() return None diff --git a/l10n_fr_intrastat_product/post_install.py b/l10n_fr_intrastat_product/post_install.py index 08c3b17c0..249aa3c48 100644 --- a/l10n_fr_intrastat_product/post_install.py +++ b/l10n_fr_intrastat_product/post_install.py @@ -16,40 +16,48 @@ def set_fr_company_intrastat(cr, registry): afpo = env["account.fiscal.position"] fr_id = env.ref("base.fr").id companies = env["res.company"].search([("partner_id.country_id", "=", fr_id)]) - out_inv_trans_id = env.ref( + out_inv_b2b_trans_id = env.ref( "l10n_fr_intrastat_product.intrastat_transaction_21_11" ).id + out_inv_b2c_trans_id = env.ref( + "l10n_fr_intrastat_product.intrastat_transaction_29_12" + ).id out_ref_trans_id = env.ref( "l10n_fr_intrastat_product.intrastat_transaction_25" ).id in_inv_trans_id = env.ref( "l10n_fr_intrastat_product.intrastat_transaction_11_11" ).id + fpdict = { + "intraeub2b": out_inv_b2b_trans_id, + "intraeub2c": out_inv_b2c_trans_id, + } for company in companies: - company.write( - { - "intrastat_transaction_out_invoice": out_inv_trans_id, - "intrastat_transaction_out_refund": out_ref_trans_id, - "intrastat_transaction_in_invoice": in_inv_trans_id, - "intrastat_accessory_costs": True, - } - ) + company.write({"intrastat_accessory_costs": True}) fps = afpo.search([("company_id", "=", company.id)]) for fp in fps: xmlid_rec = imdo.search( [ ("model", "=", "account.fiscal.position"), - ("module", "=", "l10n_fr"), + ("module", "=like", "l10n_fr%"), ("res_id", "=", fp.id), - ("name", "=like", "%_fiscal_position_template_intraeub2b"), ], limit=1, ) if xmlid_rec: - logger.debug( - "set_fr_company_intrastat writing intrastat=True " - "on fiscal position ID %d", - fp.id, - ) - fp.write({"intrastat": True}) - return + for fp_type, out_inv_trans_id in fpdict.items(): + if xmlid_rec.name.endswith(fp_type): + logger.debug( + "set_fr_company_intrastat writing intrastat=True " + "on fiscal position ID %d", + fp.id, + ) + fp.write( + { + "intrastat": True, + "intrastat_out_invoice_transaction_id": out_inv_trans_id, + "intrastat_out_refund_transaction_id": out_ref_trans_id, + "intrastat_in_invoice_transaction_id": in_inv_trans_id, + } + ) + break diff --git a/l10n_fr_intrastat_product/readme/DESCRIPTION.rst b/l10n_fr_intrastat_product/readme/DESCRIPTION.rst index 587f21600..2bff2e1d3 100644 --- a/l10n_fr_intrastat_product/readme/DESCRIPTION.rst +++ b/l10n_fr_intrastat_product/readme/DESCRIPTION.rst @@ -1,3 +1,3 @@ -This module adds support for the *Déclaration d'Échange de Biens* (DEB) for France. +This module adds support for the *Enquête mensuelle statistique sur les échanges de biens intra-UE* (EMEBI), for France. Before 2022, this declaration was called Déclaration d'Échange de Biens (DEB). -More information about the DEB is available on this `official web page `_. +More information about the EMEBI is available on this `official web page `_. diff --git a/l10n_fr_intrastat_product/readme/USAGE.rst b/l10n_fr_intrastat_product/readme/USAGE.rst index 1b0a0084d..79ed357c5 100644 --- a/l10n_fr_intrastat_product/readme/USAGE.rst +++ b/l10n_fr_intrastat_product/readme/USAGE.rst @@ -1 +1 @@ -To use this module, you need to go to the menu Invoicing > Reports > Intrastat > DEB and create a new DEB. Depending on your obligation levels, you may have to create 2 DEBs: one for export (Expéditions) and one for import (Introductions). Then, click on the button *Generate lines from invoices* to automatically generate the lines of DEB. After checking the lines that have been automatically generated, click on the button *Attach XML file* to create the XML file corresponding to the DEB. Eventually, connect to your account on `pro.douane `_ and upload the DEB XML file. +To use this module, you need to go to the menu Invoicing > Reports > Intrastat > EMEBI and create a new EMEBI. Depending on your obligation levels, you may have to create 2 EMEBIs: one for departures (Expéditions) and one for arrivals (Introductions). Then, click on the button *Generate lines from invoices* to automatically generate the computation lines of EMEBI. After checking the lines that have been automatically generated, click on the button *Confirm* to generate the declaration lines, create the XML file and set the declaration readonly. Eventually, connect to your account on `douane.gouv.fr `_ and upload the EMEBI XML file. diff --git a/l10n_fr_intrastat_product/security/intrastat_product_security.xml b/l10n_fr_intrastat_product/security/intrastat_product_security.xml deleted file mode 100644 index eaf4c31c3..000000000 --- a/l10n_fr_intrastat_product/security/intrastat_product_security.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - DEB multi-company - - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] - - diff --git a/l10n_fr_intrastat_product/security/ir.model.access.csv b/l10n_fr_intrastat_product/security/ir.model.access.csv deleted file mode 100644 index ae692613a..000000000 --- a/l10n_fr_intrastat_product/security/ir.model.access.csv +++ /dev/null @@ -1,4 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_fr_intrastat_product,Full access to l10n.fr.intrastat.product.declaration to accountant,model_l10n_fr_intrastat_product_declaration,account.group_account_user,1,1,1,1 -access_fr_intrastat_product_computation_line,Full access to l10n.fr.intrastat.product.computation.line to accountant,model_l10n_fr_intrastat_product_computation_line,account.group_account_user,1,1,1,1 -access_fr_intrastat_product_declaration_line,Full access to l10n.fr.intrastat.product.declaration.line to accountant,model_l10n_fr_intrastat_product_declaration_line,account.group_account_user,1,1,1,1 diff --git a/l10n_fr_intrastat_product/views/account_fiscal_position.xml b/l10n_fr_intrastat_product/views/account_fiscal_position.xml new file mode 100644 index 000000000..ad880c98a --- /dev/null +++ b/l10n_fr_intrastat_product/views/account_fiscal_position.xml @@ -0,0 +1,20 @@ + + + + + intrastat_product.account.fiscal.position.form + account.fiscal.position + + + + {'invisible': [('company_country_code', '=', 'FR')]} + + + + diff --git a/l10n_fr_intrastat_product/views/account_move.xml b/l10n_fr_intrastat_product/views/account_move.xml new file mode 100644 index 000000000..ac9eb2c15 --- /dev/null +++ b/l10n_fr_intrastat_product/views/account_move.xml @@ -0,0 +1,21 @@ + + + + + + account.move + + + + [('fr_object_type', '=', move_type)] + + + + + diff --git a/l10n_fr_intrastat_product/views/intrastat_product_declaration.xml b/l10n_fr_intrastat_product/views/intrastat_product_declaration.xml index c63599508..7eea6df4b 100644 --- a/l10n_fr_intrastat_product/views/intrastat_product_declaration.xml +++ b/l10n_fr_intrastat_product/views/intrastat_product_declaration.xml @@ -5,122 +5,71 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> - - l10n.fr.intrastat.product.declaration.form - l10n.fr.intrastat.product.declaration - primary - - -
- DEB -
-
-
- - l10n.fr.intrastat.product.declaration.tree - l10n.fr.intrastat.product.declaration - primary - - - - DEB - - - - - l10n.fr.intrastat.product.computation.line.form - l10n.fr.intrastat.product.computation.line - primary + + intrastat.product.computation.line - - 0 - - - + + {'invisible': [('declaration_type', '=', 'arrivals'), ('company_country_code', '=', 'FR')]} - - l10n.fr.intrastat.product.computation.line.tree - l10n.fr.intrastat.product.computation.line - primary + + intrastat.product.computation.line - - 0 - - - + + {'column_invisible': [('parent.declaration_type', '=', 'arrivals'), ('parent.company_country_code', '=', 'FR')]} - - - l10n.fr.intrastat.product.declaration.line.form - l10n.fr.intrastat.product.declaration.line - primary + + + intrastat.product.declaration.line - - 0 - - - + + {'invisible': [('declaration_type', '=', 'arrivals'), ('company_country_code', '=', 'FR')]} - - l10n.fr.intrastat.product.declaration.line.tree - l10n.fr.intrastat.product.declaration.line - primary + + + + intrastat.product.declaration.line - - 0 - - - + + {'column_invisible': [('parent.declaration_type', '=', 'arrivals'), ('parent.company_country_code', '=', 'FR')]} - - l10n.fr.intrastat.product.declaration.search - l10n.fr.intrastat.product.declaration - primary - - - - Search DEB - - - + - DEB - l10n.fr.intrastat.product.declaration + EMEBI + intrastat.product.declaration tree,form,graph,pivot - - - - - fr.intrastat.product.res.partner - res.partner - - - - - - - - diff --git a/l10n_fr_intrastat_service/models/intrastat_service.py b/l10n_fr_intrastat_service/models/intrastat_service.py index cb99c0521..dbd2efcaa 100644 --- a/l10n_fr_intrastat_service/models/intrastat_service.py +++ b/l10n_fr_intrastat_service/models/intrastat_service.py @@ -109,7 +109,8 @@ def _compute_numbers(self): for x in rg_res } for rec in self: - rec.write(data.get(rec.id, {})) + rec.total_amount = data.get(rec.id, {}).get("total_amount", 0) + rec.num_decl_lines = data.get(rec.id, {}).get("num_decl_lines", 0) @api.depends("start_date") def _compute_dates(self): @@ -154,6 +155,7 @@ def _prepare_domain(self): ("invoice_date", ">=", self.start_date), ("state", "=", "posted"), ("intrastat_fiscal_position", "=", True), + ("fiscal_position_id.vat_required", "=", True), ("company_id", "=", self.company_id.id), ] return domain @@ -276,14 +278,16 @@ def generate_service_lines(self): return def done(self): + for decl in self: + assert decl.state == "draft" + decl.generate_xml() self.write({"state": "done"}) def back2draft(self): for decl in self: + assert decl.state == "done" if decl.attachment_id: - raise UserError( - _("Before going back to draft, you must delete the XML export.") - ) + decl.attachment_id.unlink() self.write({"state": "draft"}) def _generate_des_xml_root(self): @@ -337,14 +341,9 @@ def _generate_des_xml_root(self): def generate_xml(self): self.ensure_one() - if self.attachment_id: - raise UserError( - _( - "An XML Export already exists for %s. " - "To re-generate it, you must first delete it." - ) - % self.display_name - ) + assert not self.attachment_id + if not self.declaration_line_ids: + return root = self._generate_des_xml_root() xml_bytes = etree.tostring( root, pretty_print=True, encoding="UTF-8", xml_declaration=True @@ -371,10 +370,6 @@ def _attach_xml_file(self, xml_bytes): ) return attach.id - def delete_xml(self): - self.ensure_one() - self.attachment_id and self.attachment_id.unlink() - @api.model def _scheduler_reminder(self): logger.info("Start DES reminder") diff --git a/l10n_fr_intrastat_service/readme/DESCRIPTION.rst b/l10n_fr_intrastat_service/readme/DESCRIPTION.rst index 733c2f026..23c9d6a3c 100644 --- a/l10n_fr_intrastat_service/readme/DESCRIPTION.rst +++ b/l10n_fr_intrastat_service/readme/DESCRIPTION.rst @@ -2,4 +2,4 @@ This module adds support for the **Déclaration Européenne des Services** (DES) The DES declaration has been introduced on January 1st 2010 in France. All French companies must send this declaration each month to France's Customs administration if they sell services without VAT to other EU companies. -More information about the DES is available on this `official web page `_. +More information about the DES is available on this `official web page `_. diff --git a/l10n_fr_intrastat_service/views/intrastat_service_view.xml b/l10n_fr_intrastat_service/views/intrastat_service_view.xml index 1f0be3cdb..24a9c9699 100644 --- a/l10n_fr_intrastat_service/views/intrastat_service_view.xml +++ b/l10n_fr_intrastat_service/views/intrastat_service_view.xml @@ -21,29 +21,18 @@ />