diff --git a/l10n_fr_intrastat_product/README.rst b/l10n_fr_intrastat_product/README.rst index a59d01b1f..216585be1 100644 --- a/l10n_fr_intrastat_product/README.rst +++ b/l10n_fr_intrastat_product/README.rst @@ -1,6 +1,6 @@ -=== -DEB -=== +===== +EMEBI +===== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -28,9 +28,9 @@ DEB |badge1| |badge2| |badge3| |badge4| |badge5| -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 `_. **Table of contents** @@ -52,7 +52,7 @@ WARNING: there are A LOT of settings for DEB and all these settings need to be c Usage ===== -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. Bug Tracker =========== 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/account_fiscal_position_template.xml b/l10n_fr_intrastat_product/data/account_fiscal_position_template.xml index b93d3b39a..fd58222f7 100644 --- a/l10n_fr_intrastat_product/data/account_fiscal_position_template.xml +++ b/l10n_fr_intrastat_product/data/account_fiscal_position_template.xml @@ -11,4 +11,10 @@ > + + + 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..bfbd6713c 100644 --- a/l10n_fr_intrastat_product/data/intrastat_transaction.xml +++ b/l10n_fr_intrastat_product/data/intrastat_transaction.xml @@ -1,6 +1,6 @@ @@ -10,6 +10,7 @@ name="description" >Achat Fournisseur (Acquisitions intracomm. taxables en France)
11 + in_invoice 11 @@ -19,19 +20,31 @@ 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 1 dispatches + + Vente Client B2C (soumises à TVA) + 29 + + out_invoice + 12 + + 0 + dispatches + Avoir Client (Régularisation commerciale - minoration de valeur) 25 + out_refund -1 diff --git a/l10n_fr_intrastat_product/demo/intrastat_demo.xml b/l10n_fr_intrastat_product/demo/intrastat_demo.xml index 931b03e14..a263c0215 100644 --- a/l10n_fr_intrastat_product/demo/intrastat_demo.xml +++ b/l10n_fr_intrastat_product/demo/intrastat_demo.xml @@ -1,21 +1,12 @@ A12B - - - 1 @@ -29,10 +20,4 @@ PCE - - diff --git a/l10n_fr_intrastat_product/migrations/14.0.1.0.0/pre-migration.py b/l10n_fr_intrastat_product/migrations/14.0.1.0.0/pre-migration.py index cc47d4ffe..ddabafc3d 100644 --- a/l10n_fr_intrastat_product/migrations/14.0.1.0.0/pre-migration.py +++ b/l10n_fr_intrastat_product/migrations/14.0.1.0.0/pre-migration.py @@ -2,12 +2,18 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openupgradelib import openupgrade -def migrate(cr, version): - if not version: - return - cr.execute( - 'ALTER TABLE "l10n_fr_intrastat_product_declaration" RENAME "type" ' - 'TO "declaration_type"' - ) +@openupgrade.migrate() +def migrate(env, version): + if openupgrade.table_exists( + env.cr, "l10n_fr_intrastat_product_declaration" + ) and openupgrade.column_exists( + env.cr, "l10n_fr_intrastat_product_declaration", "type" + ): + openupgrade.logged_query( + env.cr, + "ALTER TABLE l10n_fr_intrastat_product_declaration RENAME type " + "TO declaration_type", + ) 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..b343b8fb7 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,103 +78,32 @@ 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 + if not dpt: + msg = _( + "Missing department. " + "To set it, set the country and the zip code on this partner." + ) + partner_name = self.company_id.partner_id.display_name + notedict["partner"][partner_name][msg].add(notedict["inv_origin"]) 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 - ): - line_notes = [ - _( - "Missing fiscal representative on partner '%s'." - % commercial_partner.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 - def _generate_xml(self): """Generate the INSTAT XML file export.""" + if self.company_id.country_id.code != "FR": + return super()._generate_xml() my_company_vat = self.company_id.partner_id.vat.replace(" ", "") if not self.company_id.siret: @@ -215,13 +127,15 @@ def _generate_xml(self): envelope = etree.SubElement(root, "Envelope") envelope_id = etree.SubElement(envelope, "envelopeId") if not self.company_id.fr_intrastat_accreditation: - raise UserError( - _( - "The customs accreditation identifier is not set " - "for the company '%s'." + self.message_post( + body=_( + "No XML file generated because the Customs Accreditation " + "Identifier is not set on the accounting configuration " + "page of the company '%s'." ) % self.company_id.display_name ) + return envelope_id.text = self.company_id.fr_intrastat_accreditation create_date_time = etree.SubElement(envelope, "DateTime") create_date = etree.SubElement(create_date_time, "date") @@ -249,7 +163,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 +202,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 +264,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 +272,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 +286,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 +295,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 +327,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: + if not self.src_dest_country_code: 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" - ): - 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,27 +354,28 @@ 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 " - "on January 1st 2021 and companies in Northern Ireland " - "have a new VAT number starting with 'XI'." + "Bad VAT number '%(vat)s' on line %(line_number)d. " + "Brexit took place on January 1st 2021 and companies " + "in Northern Ireland have a new VAT number starting with 'XI'." ) - % (self.vat, line_number) + % {"vat": self.vat, "line_number": 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 +401,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..60dfb04ca 100644 --- a/l10n_fr_intrastat_product/post_install.py +++ b/l10n_fr_intrastat_product/post_install.py @@ -16,22 +16,29 @@ 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": False, + "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, + "intrastat_out_invoice_transaction_id": out_inv_b2b_trans_id, + "intrastat_out_refund_transaction_id": out_ref_trans_id, + "intrastat_in_invoice_transaction_id": in_inv_trans_id, } ) fps = afpo.search([("company_id", "=", company.id)]) @@ -39,17 +46,23 @@ def set_fr_company_intrastat(cr, registry): 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, + ) + vals = {"intrastat": True} + if out_inv_trans_id: + vals[ + "intrastat_out_invoice_transaction_id" + ] = out_inv_trans_id + fp.write(vals) + 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/static/description/index.html b/l10n_fr_intrastat_product/static/description/index.html index e009bb593..b9816536e 100644 --- a/l10n_fr_intrastat_product/static/description/index.html +++ b/l10n_fr_intrastat_product/static/description/index.html @@ -1,10 +1,9 @@ - -DEB +EMEBI -
-

DEB

+
+

EMEBI

Beta License: AGPL-3 OCA/l10n-france Translate me on Weblate Try me on Runboat

-

This module adds support for the Déclaration d’Échange de Biens (DEB) for France.

-

More information about the DEB is available on this official web page.

+

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 EMEBI is available on this official web page.

Table of contents

Usage

-

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.

Bug Tracker

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/README.rst b/l10n_fr_intrastat_service/README.rst index ac0bd6df4..d1615d052 100644 --- a/l10n_fr_intrastat_service/README.rst +++ b/l10n_fr_intrastat_service/README.rst @@ -32,7 +32,7 @@ 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 `_. **Table of contents** diff --git a/l10n_fr_intrastat_service/__manifest__.py b/l10n_fr_intrastat_service/__manifest__.py index 721b988b9..3fcfebbc3 100644 --- a/l10n_fr_intrastat_service/__manifest__.py +++ b/l10n_fr_intrastat_service/__manifest__.py @@ -16,7 +16,8 @@ "data": [ "security/ir.model.access.csv", "views/intrastat_service_view.xml", - "data/intrastat_service_reminder.xml", + "data/ir_cron.xml", + "data/mail_template.xml", "security/intrastat_service_security.xml", ], "installable": True, diff --git a/l10n_fr_intrastat_service/data/intrastat_service_reminder.xml b/l10n_fr_intrastat_service/data/intrastat_service_reminder.xml deleted file mode 100644 index 2370846bd..000000000 --- a/l10n_fr_intrastat_service/data/intrastat_service_reminder.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - DES Reminder - - - 1 - months - -1 - - - - code - model._scheduler_reminder() - - - - - DES Reminder - - - - ${object.company_id.email or 'odoo@example.com'} - - ${object.company_id.intrastat_email_list} - - DES ${object.year_month} for ${object.company_id.name} - - I would like to remind you that we are approaching the deadline for the DES declaration for month ${object.year_month}.

- -

As there were no DES for that month in Odoo, a draft declaration has been generated automatically.

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

When trying to generate the DES lines, the following error was encountered:

- -

${ctx.get('error_msg')}

- -

You should solve this error, then go to the menu Invoicing > Reporting > Intrastat > DES, open the declaration of month ${object.year_month} and click on the button Re-generate lines.

- -% else: -% if object.num_decl_lines > 0: -

This draft DES contains ${object.num_decl_lines} ${object.num_decl_lines == 1 and 'line' or 'lines'} and the total amount is ${object.total_amount} ${object.currency_id.symbol}.

-% else: -

This draft DES generated automatically by Odoo doesn't contain any line.

-% endif - -

Go and check this declaration in the menu Invoicing > Reporting > Intrastat > DES.

- -% endif - -

---
-Automatic e-mail sent by Odoo. -

-]]>
-
-
-
diff --git a/l10n_fr_intrastat_service/data/ir_cron.xml b/l10n_fr_intrastat_service/data/ir_cron.xml new file mode 100644 index 000000000..8194272b7 --- /dev/null +++ b/l10n_fr_intrastat_service/data/ir_cron.xml @@ -0,0 +1,22 @@ + + + + + + DES Reminder + + + 1 + months + -1 + + + + code + model._scheduler_reminder() + + diff --git a/l10n_fr_intrastat_service/data/mail_template.xml b/l10n_fr_intrastat_service/data/mail_template.xml new file mode 100644 index 000000000..f964f21e5 --- /dev/null +++ b/l10n_fr_intrastat_service/data/mail_template.xml @@ -0,0 +1,63 @@ + + + + + + DES Reminder + + + ${object.company_id.email} + ${object.company_id.intrastat_email_list} + DES ${object.year_month} for ${object.company_id.name} + +
+

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

+ +As there were no DES for that month in Odoo, a draft declaration has been generated automatically.

+ +% if ctx.get('exception'): +When trying to generate the DES lines, the following error was encountered:

+ +${ctx.get('error_msg')}

+ +You should solve this error, then go to the menu Invoicing > Reporting > Intrastat > DES, open the declaration of month ${object.year_month} and click on the button Re-generate lines.

+ +% else: +% if object.num_decl_lines != 0: +This draft DES contains ${object.num_decl_lines} ${object.num_decl_lines == 1 and 'line' or 'lines'} and the total amount is ${object.total_amount} ${object.currency_id.symbol}.

+% else: +This draft DES generated automatically by Odoo doesn't contain any line.

+% endif + +Go and check this declaration in the menu Invoicing > Reporting > Intrastat > DES.

+ +% endif +

+ +

+--
+Automatic e-mail sent by Odoo. +

+
+
+
+
diff --git a/l10n_fr_intrastat_service/models/intrastat_service.py b/l10n_fr_intrastat_service/models/intrastat_service.py index cb99c0521..a533f275f 100644 --- a/l10n_fr_intrastat_service/models/intrastat_service.py +++ b/l10n_fr_intrastat_service/models/intrastat_service.py @@ -56,7 +56,6 @@ class L10nFrIntrastatServiceDeclaration(models.Model): total_amount = fields.Monetary( compute="_compute_numbers", currency_field="currency_id", - string="Total Amount", store=True, tracking=True, ) @@ -68,7 +67,6 @@ class L10nFrIntrastatServiceDeclaration(models.Model): ("draft", "Draft"), ("done", "Done"), ], - string="State", readonly=True, tracking=True, default="draft", @@ -109,7 +107,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 +153,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 @@ -273,17 +273,18 @@ def generate_service_lines(self): } ) self.message_post(body=_("Re-generating lines from invoices")) - 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,20 +338,15 @@ 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 ) - # We now validate the XML file against the official XSD + # Validate the XML file against the official XSD self.company_id._intrastat_check_xml_schema( xml_bytes, "l10n_fr_intrastat_service/data/des.xsd" ) @@ -371,10 +367,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") @@ -469,7 +461,7 @@ class L10nFrIntrastatServiceDeclarationLine(models.Model): index=True, ) company_id = fields.Many2one( - "res.company", related="parent_id.company_id", string="Company", store=True + "res.company", related="parent_id.company_id", store=True ) company_currency_id = fields.Many2one( "res.currency", 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/static/description/index.html b/l10n_fr_intrastat_service/static/description/index.html index ff78b9a21..b620a9fb8 100644 --- a/l10n_fr_intrastat_service/static/description/index.html +++ b/l10n_fr_intrastat_service/static/description/index.html @@ -1,4 +1,3 @@ - @@ -372,7 +371,7 @@

DES

Beta License: AGPL-3 OCA/l10n-france Translate me on Weblate Try me on Runboat

This module adds support for the Déclaration Européenne des Services (DES) for France.

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.

Table of contents

    diff --git a/l10n_fr_intrastat_service/tests/test_fr_intrastat_service.py b/l10n_fr_intrastat_service/tests/test_fr_intrastat_service.py index f45730366..01058d09a 100644 --- a/l10n_fr_intrastat_service/tests/test_fr_intrastat_service.py +++ b/l10n_fr_intrastat_service/tests/test_fr_intrastat_service.py @@ -8,98 +8,89 @@ from dateutil.relativedelta import relativedelta from lxml import etree -from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tests.common import SavepointCase from odoo.tools import float_compare -class TestFrIntrastatService(TransactionCase): - def setUp(self): - super(TestFrIntrastatService, self).setUp() +@tagged("post_install", "-at_install") +class TestFrIntrastatService(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) # Set company country to France # Using base.main_company is more difficult now because # its currency is USD # So I decided to create another company from scratch - self.company = self.env["res.company"].create( + cls.company = cls.env["res.company"].create( { "name": "Akretion France", "street": "27 rue Henri Rolland", "zip": "69100", "city": "Villeurbanne", - "country_id": self.env.ref("base.fr").id, + "country_id": cls.env.ref("base.fr").id, "vat": "FR86792377731", } ) - self.env.company.chart_template_id.try_loading(company=self.company) - self.env.user.write({"company_ids": [(4, self.company.id)]}) - self.env.user.write({"company_id": self.company.id}) - self.fp_eu_b2b = self.env["account.fiscal.position"].create( + cls.env.company.chart_template_id.try_loading(company=cls.company) + cls.env.user.write({"company_ids": [(4, cls.company.id)]}) + cls.env.user.write({"company_id": cls.company.id}) + cls.fp_eu_b2b = cls.env["account.fiscal.position"].create( { "name": "EU B2B", "intrastat": True, + "vat_required": True, } ) - self.move_model = self.env["account.move"] - self.move_line_model = self.env["account.move.line"] - self.account_account_model = self.env["account.account"] - self.service_product = self.env["product.product"].create( + cls.move_model = cls.env["account.move"] + cls.account_account_model = cls.env["account.account"] + cls.service_product = cls.env["product.product"].create( { "name": "Engineering services", "type": "service", - "company_id": self.company.id, + "company_id": cls.company.id, } ) - self.hw_product = self.env["product.product"].create( + cls.hw_product = cls.env["product.product"].create( { "name": "Hardware product", "type": "consu", - "company_id": self.company.id, + "company_id": cls.company.id, } ) - self.belgian_partner = self.env["res.partner"].create( + cls.belgian_partner = cls.env["res.partner"].create( { "name": "Odoo SA", "is_company": True, "vat": "BE0477472701", - "country_id": self.env.ref("base.be").id, + "country_id": cls.env.ref("base.be").id, "company_id": False, } ) - self.account_receivable = self.account_account_model.search( - [("code", "=", "411100"), ("company_id", "=", self.company.id)], limit=1 + cls.account_revenue = cls.account_account_model.search( + [("code", "=", "706000"), ("company_id", "=", cls.company.id)], limit=1 ) - if not self.account_receivable: - self.account_receivable = self.account_account_model.create( - { - "code": "411100", - "name": "Debtors - (test)", - "reconcile": True, - "user_type_id": self.ref("account.data_account_type_receivable"), - "company_id": self.company.id, - } - ) - assert self.account_receivable - self.account_revenue = self.account_account_model.search( - [("code", "=", "706000"), ("company_id", "=", self.company.id)], limit=1 - ) - if not self.account_revenue: - self.account_revenue = self.account_account_model.create( + if not cls.account_revenue: + cls.account_revenue = cls.account_account_model.create( { "code": "706000", "name": "Service Sales - (test)", - "user_type_id": self.ref("account.data_account_type_revenue"), - "company_id": self.company.id, + "user_type_id": cls.ref("account.data_account_type_revenue"), + "company_id": cls.company.id, } ) # create first invoice date = datetime.today() + relativedelta(day=5, months=-1) - inv1 = self.move_model.create( + inv1 = cls.move_model.create( { - "company_id": self.company.id, - "partner_id": self.belgian_partner.id, - "fiscal_position_id": self.fp_eu_b2b.id, - "currency_id": self.env.ref("base.EUR").id, + "company_id": cls.company.id, + "partner_id": cls.belgian_partner.id, + "fiscal_position_id": cls.fp_eu_b2b.id, + "currency_id": cls.env.ref("base.EUR").id, "move_type": "out_invoice", "invoice_date": date, "invoice_line_ids": [ @@ -107,11 +98,11 @@ def setUp(self): 0, 0, { - "product_id": self.service_product.id, + "product_id": cls.service_product.id, "quantity": 5, "price_unit": 90, "name": "Audit service", - "account_id": self.account_revenue.id, + "account_id": cls.account_revenue.id, }, ), ( @@ -119,11 +110,11 @@ def setUp(self): 0, { # product - "product_id": self.hw_product.id, + "product_id": cls.hw_product.id, "quantity": 1, "price_unit": 1950, "name": "Laptop", - "account_id": self.account_revenue.id, + "account_id": cls.account_revenue.id, }, ), ], @@ -131,12 +122,12 @@ def setUp(self): ) inv1.action_post() # create 2nd invoice - inv2 = self.move_model.create( + inv2 = cls.move_model.create( { - "company_id": self.company.id, - "partner_id": self.belgian_partner.id, - "fiscal_position_id": self.fp_eu_b2b.id, - "currency_id": self.env.ref("base.EUR").id, + "company_id": cls.company.id, + "partner_id": cls.belgian_partner.id, + "fiscal_position_id": cls.fp_eu_b2b.id, + "currency_id": cls.env.ref("base.EUR").id, "move_type": "out_invoice", "invoice_date": date, "invoice_line_ids": [ @@ -144,11 +135,11 @@ def setUp(self): 0, 0, { - "product_id": self.env.ref("product.product_product_1").id, + "product_id": cls.env.ref("product.product_product_1").id, "quantity": 2, "price_unit": 90.2, "name": "GAP Analysis for your Odoo v10 project", - "account_id": self.account_revenue.id, + "account_id": cls.account_revenue.id, }, ), ( @@ -156,11 +147,11 @@ def setUp(self): 0, { # consu product - "product_id": self.env.ref("product.product_product_7").id, + "product_id": cls.env.ref("product.product_product_7").id, "quantity": 1, "price_unit": 45, "name": "Apple headphones", - "account_id": self.account_revenue.id, + "account_id": cls.account_revenue.id, }, ), ], @@ -168,12 +159,12 @@ def setUp(self): ) inv2.action_post() # create refund - inv3 = self.move_model.create( + inv3 = cls.move_model.create( { - "company_id": self.company.id, - "partner_id": self.belgian_partner.id, - "fiscal_position_id": self.fp_eu_b2b.id, - "currency_id": self.env.ref("base.EUR").id, + "company_id": cls.company.id, + "partner_id": cls.belgian_partner.id, + "fiscal_position_id": cls.fp_eu_b2b.id, + "currency_id": cls.env.ref("base.EUR").id, "move_type": "out_refund", "invoice_date": date, "invoice_line_ids": [ @@ -181,11 +172,11 @@ def setUp(self): 0, 0, { - "product_id": self.service_product.id, + "product_id": cls.service_product.id, "quantity": 1, "price_unit": 90, "name": "Refund consulting hour", - "account_id": self.account_revenue.id, + "account_id": cls.account_revenue.id, }, ) ], @@ -198,11 +189,10 @@ def test_generate_des(self): {"company_id": self.company.id} ) des.generate_service_lines() - self.assertEqual(float_compare(des.total_amount, 540.0, precision_digits=0), 0) + self.assertFalse(float_compare(des.total_amount, 540.0, precision_digits=0)) self.assertEqual(des.num_decl_lines, 3) des.done() self.assertEqual(des.state, "done") - des.generate_xml() xml_des_file = des.attachment_id self.assertTrue(xml_des_file) self.assertEqual(xml_des_file.name[-4:], ".xml") @@ -212,3 +202,10 @@ def test_generate_des(self): self.assertEqual(company_vat_xpath[0].text, self.company.vat) lines_xpath = xml_root.xpath("/fichier_des/declaration_des/ligne_des") self.assertEqual(len(lines_xpath), des.num_decl_lines) + with self.assertRaises(UserError): + des.unlink() + des.back2draft() + self.assertEqual(des.state, "draft") + + def test_cron(self): + self.env["l10n.fr.intrastat.service.declaration"]._scheduler_reminder() diff --git a/l10n_fr_intrastat_service/views/intrastat_service_view.xml b/l10n_fr_intrastat_service/views/intrastat_service_view.xml index 1f0be3cdb..2a410c69c 100644 --- a/l10n_fr_intrastat_service/views/intrastat_service_view.xml +++ b/l10n_fr_intrastat_service/views/intrastat_service_view.xml @@ -10,7 +10,7 @@ fr.intrastat.service.declaration.form l10n.fr.intrastat.service.declaration -
    +
    - +