diff --git a/l10n_fr_intrastat_service/__manifest__.py b/l10n_fr_intrastat_service/__manifest__.py index 1c1ad1cfd..505dfb349 100644 --- a/l10n_fr_intrastat_service/__manifest__.py +++ b/l10n_fr_intrastat_service/__manifest__.py @@ -4,7 +4,7 @@ { "name": "DES", - "version": "17.0.1.0.0", + "version": "18.0.1.0.0", "category": "Localisation/Report Intrastat", "license": "AGPL-3", "summary": "Module for Intrastat service reporting (DES) for France", @@ -16,11 +16,10 @@ "data": [ "security/ir.model.access.csv", "report/report.xml", - "views/intrastat_service_view.xml", + "views/intrastat_service.xml", "data/ir_cron.xml", "data/mail_template.xml", "security/intrastat_service_security.xml", ], "installable": True, - "application": True, } diff --git a/l10n_fr_intrastat_service/data/ir_cron.xml b/l10n_fr_intrastat_service/data/ir_cron.xml index 8194272b7..7a490f91e 100644 --- a/l10n_fr_intrastat_service/data/ir_cron.xml +++ b/l10n_fr_intrastat_service/data/ir_cron.xml @@ -12,9 +12,6 @@ 1 months - -1 - - code model._scheduler_reminder() diff --git a/l10n_fr_intrastat_service/models/intrastat_service.py b/l10n_fr_intrastat_service/models/intrastat_service.py index 7d9828ed1..de65d9147 100644 --- a/l10n_fr_intrastat_service/models/intrastat_service.py +++ b/l10n_fr_intrastat_service/models/intrastat_service.py @@ -27,6 +27,7 @@ class L10nFrIntrastatServiceDeclaration(models.Model): string="Company", required=True, default=lambda self: self.env.company, + ondelete="cascade", ) start_date = fields.Date( required=True, @@ -153,6 +154,7 @@ def _prepare_domain(self): return domain def _is_service(self, invoice_line): + # What are we supposed to do for the new 'combo' type ??? if invoice_line.product_id.type == "service": return True else: @@ -160,6 +162,7 @@ def _is_service(self, invoice_line): def generate_service_lines(self): self.ensure_one() + self.company_id._check_company() line_obj = self.env["l10n.fr.intrastat.service.declaration.line"] amo = self.env["account.move"] # delete all DES lines generated from invoices @@ -167,6 +170,7 @@ def generate_service_lines(self): [("move_id", "!=", False), ("parent_id", "=", self.id)] ) lines_to_remove.unlink() + lines_to_create = [] company_currency = self.company_id.currency_id invoices = amo.search(self._prepare_domain(), order="invoice_date") for invoice in invoices: @@ -184,8 +188,11 @@ def generate_service_lines(self): amount_invoice_cur_to_write = 0.0 amount_company_cur_to_write = 0.0 amount_invoice_cur_regular_service = 0.0 + amount_company_cur_regular_service = 0.0 amount_invoice_cur_accessory_cost = 0.0 + amount_company_cur_accessory_cost = 0.0 regular_product_in_invoice = False + subtotal_sign = invoice.move_type == "out_refund" and -1 or 1 for line in invoice.invoice_line_ids.filtered( lambda x: x.display_type == "product" and x.product_id @@ -204,39 +211,37 @@ def generate_service_lines(self): # - some HW products with value = 0 # - some accessory costs # => we want to have the accessory costs in DEB, not in DES - if line.currency_id.is_zero(line.price_subtotal): + if company_currency.is_zero(line.balance): continue if line.product_id.is_accessory_cost: - amount_invoice_cur_accessory_cost += line.price_subtotal + amount_invoice_cur_accessory_cost += ( + line.price_subtotal * subtotal_sign + ) + amount_company_cur_accessory_cost += line.balance * -1 else: - amount_invoice_cur_regular_service += line.price_subtotal + amount_invoice_cur_regular_service += ( + line.price_subtotal * subtotal_sign + ) + amount_company_cur_regular_service += line.balance * -1 # END of the loop on invoice lines if regular_product_in_invoice: amount_invoice_cur_to_write = amount_invoice_cur_regular_service + amount_company_cur_to_write = amount_company_cur_regular_service else: amount_invoice_cur_to_write = ( amount_invoice_cur_regular_service + amount_invoice_cur_accessory_cost ) - - amount_company_cur_to_write = int( - round( - invoice.currency_id._convert( - amount_invoice_cur_to_write, - company_currency, - self.company_id, - invoice.invoice_date, - ) + amount_company_cur_to_write = ( + amount_company_cur_regular_service + + amount_company_cur_accessory_cost ) - ) - if amount_company_cur_to_write: - if invoice.move_type == "out_refund": - amount_invoice_cur_to_write *= -1 - amount_company_cur_to_write *= -1 + amount_company_cur_to_write = int(round(amount_company_cur_to_write)) + if amount_company_cur_to_write: # Why do I check that the Partner has a VAT number # only here and not earlier ? Because, if I sell # to a physical person in the EU with VAT, then @@ -255,7 +260,7 @@ def generate_service_lines(self): else: partner_vat_to_write = invoice.commercial_partner_id.vat - line_obj.create( + lines_to_create.append( { "parent_id": self.id, "move_id": invoice.id, @@ -266,12 +271,14 @@ def generate_service_lines(self): "amount_company_currency": amount_company_cur_to_write, } ) + line_obj.create(lines_to_create) self.message_post(body=_("Re-generating lines from invoices")) def done(self): for decl in self: assert decl.state == "draft" - decl.generate_xml() + decl._check_company() + decl._generate_xml() self.write({"state": "done"}) def back2draft(self): @@ -281,12 +288,43 @@ def back2draft(self): decl.attachment_id.unlink() self.write({"state": "draft"}) - def _generate_des_xml_root(self): + def _check_company(self): self.ensure_one() - if not self.company_id.partner_id.vat: + company = self.company_id + company_vat = company.partner_id.vat + if not company_vat: + raise UserError( + _("Missing VAT number on company '%s'.") % company.display_name + ) + if not company_vat.startswith("FR"): + raise UserError( + _( + "DES is only for French companies, so the VAT number should " + "start with 'FR'. VAT number of company '%(company)s' is %(vat)s.", + company=company.display_name, + vat=company_vat, + ) + ) + if not is_valid(company_vat): raise UserError( - _("Missing VAT number on company '%s'.") % self.company_id.display_name + _( + "The VAT number of company '%(company)s' is %(vat)s. " + "This VAT number is not valid.", + company=company.display_name, + vat=company_vat, + ) + ) + if company.currency_id.name != "EUR": + raise UserError( + _( + "The currency of company %(company)s is %(currency)s and not EUR.", + company=company.display_name, + currency=company.currency_id.name, + ) ) + + def _generate_des_xml_root(self): + self.ensure_one() my_company_vat = self.company_id.partner_id.vat # Tech spec of XML export are available here : @@ -322,7 +360,7 @@ def _generate_des_xml_root(self): ligne_des.partner_des = vat return root - def generate_xml(self): + def _generate_xml(self): self.ensure_one() assert not self.attachment_id if not self.declaration_line_ids: @@ -338,21 +376,20 @@ def generate_xml(self): xml_bytes, "l10n_fr_intrastat_service/data/des.xsd" ) # Attach the XML file - attach_id = self._attach_xml_file(xml_bytes) - self.write({"attachment_id": attach_id}) + attach_vals = self._prepare_attachment(xml_bytes) + attach = self.env["ir.attachment"].create(attach_vals) + self.write({"attachment_id": attach.id}) - def _attach_xml_file(self, xml_bytes): + def _prepare_attachment(self, xml_bytes): self.ensure_one() filename = f"{self.year_month}_des.xml" - attach = self.env["ir.attachment"].create( - { - "name": filename, - "res_id": self.id, - "res_model": self._name, - "raw": xml_bytes, - } - ) - return attach.id + attach_vals = { + "name": filename, + "res_id": self.id, + "res_model": self._name, + "raw": xml_bytes, + } + return attach_vals @api.model def _scheduler_reminder(self): @@ -452,10 +489,17 @@ class L10nFrIntrastatServiceDeclarationLine(models.Model): invoice_date = fields.Date( related="move_id.invoice_date", string="Invoice Date", store=True ) - partner_vat = fields.Char(string="Customer VAT", required=True) + partner_vat = fields.Char( + string="Customer VAT", + required=True, + compute="_compute_partner_vat", + store=True, + readonly=False, + precompute=True, + ) partner_id = fields.Many2one( "res.partner", - string="Partner Name", + string="Customer", ondelete="restrict", domain=[("parent_id", "=", False)], ) @@ -468,18 +512,20 @@ class L10nFrIntrastatServiceDeclarationLine(models.Model): "date and rounded at 0 digits", ) amount_invoice_currency = fields.Monetary( - string="Amount in Invoice Currency", + string="Invoiced Amount", readonly=True, currency_field="invoice_currency_id", + help="Invoiced amount in the invoice currency", ) invoice_currency_id = fields.Many2one( "res.currency", "Invoice Currency", readonly=True ) - @api.onchange("partner_id") - def partner_on_change(self): - if self.partner_id and self.partner_id.vat: - self.partner_vat = self.partner_id.vat + @api.depends("partner_id") + def _compute_partner_vat(self): + for line in self: + if line.partner_id and line.partner_id.vat: + line.partner_vat = line.partner_id.vat @api.constrains("partner_vat") def _check_partner_vat(self): diff --git a/l10n_fr_intrastat_service/security/ir.model.access.csv b/l10n_fr_intrastat_service/security/ir.model.access.csv index b9a00c503..da3e13457 100644 --- a/l10n_fr_intrastat_service/security/ir.model.access.csv +++ b/l10n_fr_intrastat_service/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_l10n_fr_intrastat_service_declaration,Full access to l10n.fr.intrastat.service.declaration to accountant,model_l10n_fr_intrastat_service_declaration,account.group_account_user,1,1,1,1 access_l10n_fr_intrastat_service_declaration_line,Full access to l10n.fr.intrastat.service.declaration.line to accoutant,model_l10n_fr_intrastat_service_declaration_line,account.group_account_user,1,1,1,1 +access_l10n_fr_intrastat_service_declaration_read,Read-only access to l10n.fr.intrastat.service.declaration to account viewer,model_l10n_fr_intrastat_service_declaration,account.group_account_readonly,1,0,0,0 +access_l10n_fr_intrastat_service_declaration_line_read,Read-only access to l10n.fr.intrastat.service.declaration.line to accout viewer,model_l10n_fr_intrastat_service_declaration_line,account.group_account_readonly,1,0,0,0 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 5588a90e0..d058f8ad4 100644 --- a/l10n_fr_intrastat_service/tests/test_fr_intrastat_service.py +++ b/l10n_fr_intrastat_service/tests/test_fr_intrastat_service.py @@ -8,7 +8,6 @@ from dateutil.relativedelta import relativedelta from lxml import etree -from odoo import Command from odoo.exceptions import UserError from odoo.tests import tagged from odoo.tools import float_compare @@ -19,22 +18,15 @@ @tagged("post_install", "-at_install") class TestFrIntrastatService(AccountTestInvoicingCommon): @classmethod - def setUpClass(cls, chart_template_ref=None): - super().setUpClass(chart_template_ref=chart_template_ref) + @AccountTestInvoicingCommon.setup_country("fr") + def setUpClass(cls): + super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - cls.fr_test_company = cls.setup_company_data( - "Akretion France", - chart_template=chart_template_ref, - country_id=cls.env.ref("base.fr").id, + cls.fr_test_company = cls.setup_other_company( + name="Akretion France TEST DES", vat="FR86792377731", ) cls.company = cls.fr_test_company["company"] - cls.user.write( - { - "company_ids": [Command.link(cls.company.id)], - "company_id": cls.company.id, - } - ) cls.fp_eu_b2b = cls.env["account.fiscal.position"].create( { "name": "EU B2B", @@ -49,14 +41,12 @@ def setUpClass(cls, chart_template_ref=None): { "name": "Engineering services", "type": "service", - "company_id": cls.company.id, } ) cls.hw_product = cls.env["product.product"].create( { "name": "Hardware product", "type": "consu", - "company_id": cls.company.id, } ) @@ -66,7 +56,6 @@ def setUpClass(cls, chart_template_ref=None): "is_company": True, "vat": "BE0477472701", "country_id": cls.env.ref("base.be").id, - "company_id": False, } ) cls.account_revenue = cls.fr_test_company["default_account_revenue"] diff --git a/l10n_fr_intrastat_service/views/intrastat_service_view.xml b/l10n_fr_intrastat_service/views/intrastat_service.xml similarity index 93% rename from l10n_fr_intrastat_service/views/intrastat_service_view.xml rename to l10n_fr_intrastat_service/views/intrastat_service.xml index a046aa29f..3eb39e49a 100644 --- a/l10n_fr_intrastat_service/views/intrastat_service_view.xml +++ b/l10n_fr_intrastat_service/views/intrastat_service.xml @@ -75,20 +75,16 @@ /> -
- - - -
+
- fr.intrastat.service.declaration.tree + fr.intrastat.service.declaration.list l10n.fr.intrastat.service.declaration - + @@ -100,7 +96,7 @@ decoration-info="state == 'draft'" widget="badge" /> - + @@ -176,7 +172,8 @@ fr.intrastat.service.declaration.line.tree l10n.fr.intrastat.service.declaration.line - + + @@ -200,7 +199,7 @@ > DES l10n.fr.intrastat.service.declaration - tree,form,pivot,graph + list,form,pivot,graph