diff --git a/intrastat_base/__manifest__.py b/intrastat_base/__manifest__.py index 031abb3..e414249 100644 --- a/intrastat_base/__manifest__.py +++ b/intrastat_base/__manifest__.py @@ -9,12 +9,11 @@ "category": "Intrastat", "license": "AGPL-3", "summary": "Base module for Intrastat reporting", - "author": "Akretion,Noviat,Odoo Community Association (OCA)", + "author": "ACSONE SA/NV, Akretion,Noviat,Odoo Community Association (OCA)", "website": "https://github.com/OCA/intrastat-extrastat", "depends": ["base_vat", "account"], "excludes": ["account_intrastat"], "data": [ - "security/ir.model.access.csv", "views/product_template.xml", "views/res_partner.xml", "views/res_config_settings.xml", diff --git a/intrastat_base/models/__init__.py b/intrastat_base/models/__init__.py index eff3d68..cea19b9 100644 --- a/intrastat_base/models/__init__.py +++ b/intrastat_base/models/__init__.py @@ -1,5 +1,5 @@ from . import product_template from . import res_company -from . import intrastat_common from . import account_fiscal_position +from . import account_fiscal_position_template from . import account_move diff --git a/intrastat_base/models/account_chart_template.py b/intrastat_base/models/account_chart_template.py new file mode 100644 index 0000000..939e9e2 --- /dev/null +++ b/intrastat_base/models/account_chart_template.py @@ -0,0 +1,17 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class AccountChartTemplate(models.Model): + _inherit = "account.chart.template" + + def _get_fp_vals(self, company, position): + """ + Get fiscal position chart template instrastat value + to create fiscal position + """ + vals = super()._get_fp_vals(company, position) + vals["intrastat"] = position.intrastat + return vals diff --git a/intrastat_base/models/account_fiscal_position.py b/intrastat_base/models/account_fiscal_position.py index fe3ebfb..0c56e59 100644 --- a/intrastat_base/models/account_fiscal_position.py +++ b/intrastat_base/models/account_fiscal_position.py @@ -13,18 +13,3 @@ class AccountFiscalPosition(models.Model): help="Set to True if the invoices with this fiscal position should " "be taken into account for the generation of the intrastat reports.", ) - - -class AccountFiscalPositionTemplate(models.Model): - _inherit = "account.fiscal.position.template" - - intrastat = fields.Boolean(string="Intrastat") - - -class AccountChartTemplate(models.Model): - _inherit = "account.chart.template" - - def _get_fp_vals(self, company, position): - vals = super()._get_fp_vals(company, position) - vals["intrastat"] = position.intrastat - return vals diff --git a/intrastat_base/models/account_fiscal_position_template.py b/intrastat_base/models/account_fiscal_position_template.py new file mode 100644 index 0000000..9085f53 --- /dev/null +++ b/intrastat_base/models/account_fiscal_position_template.py @@ -0,0 +1,14 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class AccountFiscalPositionTemplate(models.Model): + _inherit = "account.fiscal.position.template" + + intrastat = fields.Boolean( + string="Intrastat", + help="Check this if you want to generate intrastat declarations with" + "the created fiscal position", + ) diff --git a/intrastat_base/models/intrastat_common.py b/intrastat_base/models/intrastat_common.py deleted file mode 100644 index 94dc5a0..0000000 --- a/intrastat_base/models/intrastat_common.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright 2010-2020 Akretion () -# Copyright 2009-2020 Noviat (http://www.noviat.com) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import logging -from io import BytesIO -from sys import exc_info -from traceback import format_exception - -from lxml import etree - -from odoo import _, api, fields, models, tools -from odoo.exceptions import UserError - -logger = logging.getLogger(__name__) - - -class IntrastatCommon(models.AbstractModel): - _name = "intrastat.common" - _description = "Common functions for intrastat reports for products " - "and services" - - # The method _compute_numbers has been removed - # because it was using a loop on lines, which is slow -> we should - # use read_group() instead, but then the code depends on - # the line object, so it can't be factorized here - - def _check_generate_xml(self): - for this in self: - if not this.company_id.partner_id.vat: - raise UserError( - _("The VAT number is not set for the partner '%s'.") - % this.company_id.partner_id.name - ) - - @api.model - def _check_xml_schema(self, xml_bytes, xsd_file): - """Validate the XML file against the XSD""" - xsd_etree_obj = etree.parse(tools.file_open(xsd_file, mode="rb")) - official_schema = etree.XMLSchema(xsd_etree_obj) - try: - t = etree.parse(BytesIO(xml_bytes)) - official_schema.assertValid(t) - except (etree.XMLSchemaParseError, etree.DocumentInvalid) as e: - logger.warning("The XML file is invalid against the XML Schema Definition") - logger.warning(xml_bytes) - logger.warning(e) - usererror = "{}\n\n{}".format(e.__class__.__name__, str(e)) - raise UserError(usererror) - except Exception: - error = _("Unknown Error") - tb = "".join(format_exception(*exc_info())) - error += "\n%s" % tb - logger.warning(error) - raise UserError(error) - - def _attach_xml_file(self, xml_bytes, declaration_name): - """Attach the XML file to the report_intrastat_product/service - object""" - self.ensure_one() - filename = "{}_{}.xml".format(self.year_month, declaration_name) - attach = self.env["ir.attachment"].create( - { - "name": filename, - "res_id": self.id, - "res_model": self._name, - "raw": xml_bytes, - } - ) - return attach.id - - def _unlink_attachments(self): - atts = self.env["ir.attachment"].search( - [("res_model", "=", self._name), ("res_id", "=", self.id)] - ) - atts.unlink() - - # Method _open_attach_view() removed - # Let's handle attachments like in l10n_fr_intrastat_service v14 - # with the field attachment_id on the declaration and the download - # link directly on the form view of the declaration. - - def _generate_xml(self): - """ - Inherit this method in the localization module - to generate the INTRASTAT Declaration XML file - - Returns: - string with XML data - - Call the _check_xml_schema() method - before returning the XML string. - """ - return False - - def send_reminder_email(self, mail_template_xmlid): - mail_template = self.env.ref(mail_template_xmlid) - for this in self: - if this.company_id.intrastat_remind_user_ids: - mail_template.send_mail(this.id) - logger.info( - "Intrastat Reminder email has been sent (XMLID: %s)." - % mail_template_xmlid - ) - else: - logger.warning( - "The list of users receiving the Intrastat Reminder is " - "empty on company %s" % this.company_id.name - ) - return True - - def unlink(self): - for intrastat in self: - if intrastat.state == "done": - raise UserError( - _("Cannot delete the declaration %s " "because it is in Done state") - % self.year_month - ) - return super().unlink() - - -class IntrastatResultView(models.TransientModel): - """ - Transient Model to display Intrastat Report results - """ - - _name = "intrastat.result.view" - _description = "Pop-up to show errors on intrastat report generation" - - note = fields.Text( - string="Notes", readonly=True, default=lambda self: self._context.get("note") - ) diff --git a/intrastat_base/models/res_company.py b/intrastat_base/models/res_company.py index eefce32..7a2dbc9 100644 --- a/intrastat_base/models/res_company.py +++ b/intrastat_base/models/res_company.py @@ -2,8 +2,17 @@ # @author: # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +import logging +from io import BytesIO +from sys import exc_info +from traceback import format_exception + +from lxml import etree + +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError, ValidationError + +logger = logging.getLogger(__name__) class ResCompany(models.Model): @@ -40,3 +49,24 @@ class ResCompany(models.Model): raise ValidationError( _("Missing e-mail address on user '%s'.") % (user.name) ) + + @api.model + def _intrastat_check_xml_schema(self, xml_bytes, xsd_file): + """Validate the XML file against the XSD""" + xsd_etree_obj = etree.parse(tools.file_open(xsd_file, mode="rb")) + official_schema = etree.XMLSchema(xsd_etree_obj) + try: + t = etree.parse(BytesIO(xml_bytes)) + official_schema.assertValid(t) + except (etree.XMLSchemaParseError, etree.DocumentInvalid) as e: + logger.warning("The XML file is invalid against the XML Schema Definition") + logger.warning(xml_bytes) + logger.warning(e) + usererror = "{}\n\n{}".format(e.__class__.__name__, str(e)) + raise UserError(usererror) + except Exception: + error = _("Unknown Error") + tb = "".join(format_exception(*exc_info())) + error += "\n%s" % tb + logger.warning(error) + raise UserError(error) diff --git a/intrastat_base/readme/CONTRIBUTORS.rst b/intrastat_base/readme/CONTRIBUTORS.rst index 6a9e1c1..af9ef52 100644 --- a/intrastat_base/readme/CONTRIBUTORS.rst +++ b/intrastat_base/readme/CONTRIBUTORS.rst @@ -2,3 +2,4 @@ * Luc De Meyer, Noviat * Kumar Aberer, brain-tec AG * Andrea Stirpe +* Denis Roussel diff --git a/intrastat_base/security/ir.model.access.csv b/intrastat_base/security/ir.model.access.csv deleted file mode 100644 index fbe710c..0000000 --- a/intrastat_base/security/ir.model.access.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_intrastat_result_view,Access on intrastat.result.view,model_intrastat_result_view,account.group_account_user,1,1,1,0 diff --git a/intrastat_base/tests/__init__.py b/intrastat_base/tests/__init__.py index 7836283..a2afb4b 100644 --- a/intrastat_base/tests/__init__.py +++ b/intrastat_base/tests/__init__.py @@ -1 +1,2 @@ +from . import common from . import test_all diff --git a/intrastat_base/tests/common.py b/intrastat_base/tests/common.py new file mode 100644 index 0000000..ac1fbf3 --- /dev/null +++ b/intrastat_base/tests/common.py @@ -0,0 +1,16 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +class IntrastatCommon(object): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.chart_template_obj = cls.env["account.chart.template"] + cls.mail_obj = cls.env["mail.mail"] + + cls.demo_user = cls.env.ref("base.user_demo") + cls.demo_company = cls.env.ref("base.main_company") + + cls.shipping_cost = cls.env.ref("intrastat_base.shipping_costs_exclude") diff --git a/intrastat_base/tests/models.py b/intrastat_base/tests/models.py new file mode 100644 index 0000000..302914c --- /dev/null +++ b/intrastat_base/tests/models.py @@ -0,0 +1,10 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class IntrastatDeclarationTest(models.Model): + _inherit = ["mail.thread", "mail.activity.mixin", "intrastat.common"] + _name = "intrastat.declaration.test" + _description = "Intrastat Declaration Test" diff --git a/intrastat_base/tests/test_all.py b/intrastat_base/tests/test_all.py index f05fc49..40b414f 100644 --- a/intrastat_base/tests/test_all.py +++ b/intrastat_base/tests/test_all.py @@ -1,13 +1,30 @@ -from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + +from .common import IntrastatCommon -class TestIntrastatBase(TransactionCase): +class TestIntrastatBase(IntrastatCommon): """Tests for this module""" + @classmethod + def setUpClass(cls): + super().setUpClass() + def test_company(self): # add 'Demo user' to intrastat_remind_user_ids - demo_user = self.env.ref("base.user_demo") - demo_company = self.env.ref("base.main_company") - demo_company.write({"intrastat_remind_user_ids": [(6, False, [demo_user.id])]}) + self.demo_company.write( + {"intrastat_remind_user_ids": [(6, False, [self.demo_user.id])]} + ) # then check if intrastat_email_list contains the email of the user - self.assertEqual(demo_company.intrastat_email_list, demo_user.email) + self.assertEqual(self.demo_company.intrastat_email_list, self.demo_user.email) + + def test_no_email(self): + self.demo_user.email = False + with self.assertRaises(ValidationError): + self.demo_company.write( + {"intrastat_remind_user_ids": [(6, False, [self.demo_user.id])]} + ) + + def test_accessory(self): + with self.assertRaises(ValidationError): + self.shipping_cost.type = "consu" diff --git a/intrastat_base/views/intrastat.xml b/intrastat_base/views/intrastat.xml index ca14bae..4784270 100644 --- a/intrastat_base/views/intrastat.xml +++ b/intrastat_base/views/intrastat.xml @@ -18,18 +18,4 @@ parent="account.menu_finance_configuration" sequence="50" /> - - intrastat.result_view_form - intrastat.result.view - -
- - - -
-
-
-
-
diff --git a/intrastat_product/__init__.py b/intrastat_product/__init__.py index bf588bc..cf6083c 100644 --- a/intrastat_product/__init__.py +++ b/intrastat_product/__init__.py @@ -1,2 +1,3 @@ from . import models from . import report +from . import wizards diff --git a/intrastat_product/__manifest__.py b/intrastat_product/__manifest__.py index ca05a0c..6c6aee9 100644 --- a/intrastat_product/__manifest__.py +++ b/intrastat_product/__manifest__.py @@ -11,7 +11,7 @@ "category": "Intrastat", "license": "AGPL-3", "summary": "Base module for Intrastat Product", - "author": "brain-tec AG, Akretion, Noviat, Odoo Community Association (OCA)", + "author": "ACSONE SA/NV, brain-tec AG, Akretion, Noviat, Odoo Community Association (OCA)", "website": "https://github.com/OCA/intrastat-extrastat", "depends": [ "intrastat_base", @@ -35,6 +35,7 @@ "views/account_move.xml", "views/sale_order.xml", "views/stock_warehouse.xml", + "wizards/intrastat_result_view.xml", "data/intrastat_transport_mode.xml", "data/intrastat_unit.xml", ], diff --git a/intrastat_product/models/intrastat_product_declaration.py b/intrastat_product/models/intrastat_product_declaration.py index a13a5c8..4b404b4 100644 --- a/intrastat_product/models/intrastat_product_declaration.py +++ b/intrastat_product/models/intrastat_product_declaration.py @@ -20,7 +20,7 @@ class IntrastatProductDeclaration(models.Model): _name = "intrastat.product.declaration" _description = "Intrastat Product Report Base Object" _rec_name = "year_month" - _inherit = ["mail.thread", "mail.activity.mixin", "intrastat.common"] + _inherit = ["mail.thread", "mail.activity.mixin"] _order = "year_month desc, declaration_type, revision" _sql_constraints = [ ( @@ -46,14 +46,26 @@ class IntrastatProductDeclaration(models.Model): string="Company", required=True, states={"done": [("readonly", True)]}, - default=lambda self: self._default_company_id(), + default=lambda self: self.env.company, ) company_country_code = fields.Char( compute="_compute_company_country_code", string="Company Country Code", readonly=True, store=True, - help="Used in views and methods of localization modules.", + ) + state = fields.Selection( + selection=[("draft", "Draft"), ("done", "Done")], + string="State", + readonly=True, + tracking=True, + copy=False, + default="draft", + help="State of the declaration. When the state is set to 'Done', " + "the parameters become read-only.", + ) + note = fields.Text( + string="Notes", help="You can add some comments here if you want." ) year = fields.Char( string="Year", required=True, states={"done": [("readonly", True)]} @@ -133,19 +145,6 @@ class IntrastatProductDeclaration(models.Model): currency_id = fields.Many2one( "res.currency", related="company_id.currency_id", string="Currency" ) - state = fields.Selection( - selection=[("draft", "Draft"), ("done", "Done")], - string="State", - readonly=True, - tracking=True, - copy=False, - default="draft", - help="State of the declaration. When the state is set to 'Done', " - "the parameters become read-only.", - ) - note = fields.Text( - string="Notes", help="You can add some comments here if you want." - ) reporting_level = fields.Selection( selection="_get_reporting_level", string="Reporting Level", @@ -160,10 +159,6 @@ class IntrastatProductDeclaration(models.Model): related="xml_attachment_id.name", string="XML Filename" ) - @api.model - def _default_company_id(self): - return self.env.company - @api.model def _get_declaration_type(self): res = [] @@ -208,12 +203,11 @@ class IntrastatProductDeclaration(models.Model): for this in self: this.valid = True - @api.model @api.constrains("year") def _check_year(self): for this in self: if len(this.year) != 4 or this.year[0] != "2": - raise ValidationError(_("Invalid Year !")) + raise ValidationError(_("Invalid Year!")) @api.onchange("declaration_type") def _onchange_declaration_type(self): @@ -242,6 +236,36 @@ class IntrastatProductDeclaration(models.Model): msg, action.id, _("Go to Accounting Configuration Settings screen") ) + def _attach_xml_file(self, xml_bytes, declaration_name): + """Attach the XML file to the report_intrastat_product/service + object""" + self.ensure_one() + filename = "{}_{}.xml".format(self.year_month, declaration_name) + attach = self.env["ir.attachment"].create( + { + "name": filename, + "res_id": self.id, + "res_model": self._name, + "raw": xml_bytes, + } + ) + return attach.id + + def _unlink_attachments(self): + atts = self.env["ir.attachment"].search( + [("res_model", "=", self._name), ("res_id", "=", self.id)] + ) + atts.unlink() + + def unlink(self): + for this in self: + if this.state == "done": + raise UserError( + _("Cannot delete the declaration %s because it is in Done state.") + % this.display_name + ) + return super().unlink() + def _get_partner_country(self, inv_line, notedict, eu_countries): inv = inv_line.move_id country = inv.src_dest_country_id or inv.partner_id.country_id @@ -794,7 +818,7 @@ class IntrastatProductDeclaration(models.Model): self.write(vals) if vals["note"]: - result_view = self.env.ref("intrastat_base.intrastat_result_view_form") + result_view = self.env.ref("intrastat_product.intrastat_result_view_form") return { "name": _("Generate lines from invoices: results"), "view_type": "form", @@ -891,6 +915,14 @@ class IntrastatProductDeclaration(models.Model): cl.write({"declaration_line_id": declaration_line.id}) return True + def _check_generate_xml(self): + self.ensure_one() + if not self.company_id.partner_id.vat: + raise UserError( + _("The VAT number is not set for the partner '%s'.") + % self.company_id.partner_id.display_name + ) + def generate_xml(self): """ generate the INTRASTAT Declaration XML file """ self.ensure_one() diff --git a/intrastat_product/readme/CONTRIBUTORS.rst b/intrastat_product/readme/CONTRIBUTORS.rst index 08d6018..bd08e8d 100644 --- a/intrastat_product/readme/CONTRIBUTORS.rst +++ b/intrastat_product/readme/CONTRIBUTORS.rst @@ -1,5 +1,6 @@ * Alexis de Lattre, Akretion * Luc De Meyer, Noviat +* Denis Roussel * Tecnativa : * João Marques diff --git a/intrastat_product/security/ir.model.access.csv b/intrastat_product/security/ir.model.access.csv index ac93450..65141ca 100644 --- a/intrastat_product/security/ir.model.access.csv +++ b/intrastat_product/security/ir.model.access.csv @@ -12,3 +12,4 @@ access_account_move_intrastat_line,Full access on Invoice Intrastat Lines,model_ access_intrastat_product_declaration,Full access on Intrastat Product Declarations to Accountant,model_intrastat_product_declaration,account.group_account_user,1,1,1,1 access_intrastat_product_computation_line,Full access on Intrastat Product Computation Lines to Accountant,model_intrastat_product_computation_line,account.group_account_user,1,1,1,1 access_intrastat_product_declaration_line,Full access on Intrastat Product Declaration Lines to Accountant,model_intrastat_product_declaration_line,account.group_account_user,1,1,1,1 +access_intrastat_result_view,Access on intrastat.result.view,model_intrastat_result_view,account.group_account_user,1,1,1,0 diff --git a/intrastat_product/tests/__init__.py b/intrastat_product/tests/__init__.py new file mode 100644 index 0000000..804b5f7 --- /dev/null +++ b/intrastat_product/tests/__init__.py @@ -0,0 +1,7 @@ +from . import common +from . import common_purchase +from . import common_sale +from . import test_intrastat_product +from . import test_company +from . import test_purchase_order +from . import test_sale_order diff --git a/intrastat_product/tests/common.py b/intrastat_product/tests/common.py new file mode 100644 index 0000000..6efb68b --- /dev/null +++ b/intrastat_product/tests/common.py @@ -0,0 +1,114 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.intrastat_base.tests.common import IntrastatCommon + + +class IntrastatProductCommon(IntrastatCommon): + @classmethod + def _init_products(cls): + # Create category - don't init intrastat values, do it in tests + vals = { + "name": "Robots", + "parent_id": cls.category_saleable.id, + } + cls.categ_robots = cls.category_obj.create(vals) + + vals = { + "name": "C3PO", + "categ_id": cls.categ_robots.id, + "origin_country_id": cls.env.ref("base.us").id, + "weight": 300, + # Computer - C3PO is one of them + "hs_code_id": cls.hs_code_computer.id, + } + cls.product_c3po = cls.product_template_obj.create(vals) + + @classmethod + def _init_company(cls): + # Default transport for company is Road + cls.demo_company.intrastat_transport_id = cls.transport_road + + @classmethod + def _init_fiscal_position(cls): + vals = { + "name": "Intrastat Fiscal Position", + "intrastat": True, + } + cls.position = cls.position_obj.create(vals) + + @classmethod + def _init_regions(cls): + # Create Belgium Region + cls._create_region() + + vals = { + "code": "DE", + "name": "Germany", + "country_id": cls.env.ref("base.de").id, + "description": "Germany", + } + cls._create_region(vals) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.region_obj = cls.env["intrastat.region"] + cls.transaction_obj = cls.env["intrastat.transaction"] + cls.transport_mode_obj = cls.env["intrastat.transport_mode"] + cls.partner_obj = cls.env["res.partner"] + cls.category_saleable = cls.env.ref("product.product_category_1") + cls.category_obj = cls.env["product.category"] + cls.product_template_obj = cls.env["product.template"] + cls.declaration_obj = cls.env["intrastat.product.declaration"] + cls.position_obj = cls.env["account.fiscal.position"] + cls.hs_code_computer = cls.env.ref("product_harmonized_system.84715000") + + cls.transport_rail = cls.env.ref("intrastat_product.intrastat_transport_2") + cls.transport_road = cls.env.ref("intrastat_product.intrastat_transport_3") + + cls._init_regions() + cls._init_company() + cls._init_fiscal_position() + cls._init_products() + + @classmethod + def _create_region(cls, vals=None): + values = { + "code": "BE_w", + "country_id": cls.env.ref("base.be").id, + "company_id": cls.env.company.id, + "description": "Belgium Walloon Region", + "name": "Walloon Region", + } + if vals is not None: + values.update(vals) + cls.region = cls.region_obj.create(values) + + @classmethod + def _create_transaction(cls, vals=None): + values = { + "code": "11", + "company_id": cls.env.company.id, + "description": "Sale / Purchase", + } + if vals is not None: + values.update(vals) + cls.transaction = cls.transaction_obj.create(values) + + @classmethod + def _create_transport_mode(cls, vals=None): + values = {} + if vals is not None: + values.update(vals) + cls.transport_mode = cls.transport_mode_obj.create(values) + + @classmethod + def _create_declaration(cls, vals=None): + values = { + "company_id": cls.env.company.id, + "declaration_type": "dispatches", + } + if vals is not None: + values.update(vals) + cls.declaration = cls.declaration_obj.create(values) diff --git a/intrastat_product/tests/common_purchase.py b/intrastat_product/tests/common_purchase.py new file mode 100644 index 0000000..7574457 --- /dev/null +++ b/intrastat_product/tests/common_purchase.py @@ -0,0 +1,81 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.tests import Form + +from .common import IntrastatProductCommon + + +class IntrastatPurchaseCommon(IntrastatProductCommon): + """ + We define common flow: + - Supplier in Germany + """ + + def _get_expected_vals(self, line): + return { + "declaration_type": "arrivals", + "suppl_unit_qty": line.qty_received, + "hs_code_id": line.product_id.hs_code_id, + "product_origin_country_id": line.product_id.origin_country_id, + "amount_company_currency": line.price_subtotal, + "src_dest_country_id": line.partner_id.country_id, + } + + def _check_line_values(self, final=False, declaration=None, purchase=None): + """ + This method allows to test computation lines and declaration + lines values from original sale order line + """ + if declaration is None: + declaration = self.declaration + if purchase is None: + purchase = self.purchase + for line in purchase.order_line: + expected_vals = self._get_expected_vals(line) + comp_line = declaration.computation_line_ids.filtered( + lambda cline: cline.product_id == line.product_id + ) + self.assertTrue( + all(comp_line[key] == val for key, val in expected_vals.items()) + ) + if final: + decl_line = declaration.declaration_line_ids.filtered( + lambda dline: comp_line in dline.computation_line_ids + ) + self.assertTrue( + all(decl_line[key] == val for key, val in expected_vals.items()) + ) + + @classmethod + def _init_supplier(cls, vals=None): + values = { + "name": "DE Supplier", + "country_id": cls.env.ref("base.de").id, + "property_account_position_id": cls.position.id, + } + if vals is not None: + values.update(vals) + cls.supplier = cls.partner_obj.create(values) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.purchase_obj = cls.env["purchase.order"] + cls.move_obj = cls.env["account.move"] + cls._init_supplier() + + @classmethod + def _create_purchase_order(cls, vals=None): + vals = { + "partner_id": cls.supplier.id, + } + purchase_new = cls.purchase_obj.new(vals) + purchase_new.onchange_partner_id() + purchase_vals = purchase_new._convert_to_write(purchase_new._cache) + cls.purchase = cls.purchase_obj.create(purchase_vals) + with Form(cls.purchase) as purchase_form: + with purchase_form.order_line.new() as line: + line.product_id = cls.product_c3po.product_variant_ids[0] + line.product_qty = 3.0 + # Price should not be void - if no purchase pricelist + line.price_unit = 150.0 diff --git a/intrastat_product/tests/common_sale.py b/intrastat_product/tests/common_sale.py new file mode 100644 index 0000000..4a3a457 --- /dev/null +++ b/intrastat_product/tests/common_sale.py @@ -0,0 +1,76 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.tests import Form + +from .common import IntrastatProductCommon + + +class IntrastatSaleCommon(IntrastatProductCommon): + """ + We define common flow: + - Customer in Netherlands + """ + + def _get_expected_vals(self, line): + return { + "declaration_type": "dispatches", + "suppl_unit_qty": line.qty_delivered, + "hs_code_id": line.product_id.hs_code_id, + "product_origin_country_id": line.product_id.origin_country_id, + } + + def _check_line_values(self, final=False, declaration=None, sale=None): + """ + This method allows to test computation lines and declaration + lines values from original sale order line + """ + if declaration is None: + declaration = self.declaration + if sale is None: + sale = self.sale + for line in sale.order_line: + expected_vals = self._get_expected_vals(line) + comp_line = declaration.computation_line_ids.filtered( + lambda cline: cline.product_id == line.product_id + ) + self.assertTrue( + all(comp_line[key] == val for key, val in expected_vals.items()) + ) + if final: + decl_line = declaration.declaration_line_ids.filtered( + lambda dline: comp_line in dline.computation_line_ids + ) + self.assertTrue( + all(decl_line[key] == val for key, val in expected_vals.items()) + ) + + @classmethod + def _init_customer(cls, vals=None): + values = { + "name": "NL Customer", + "country_id": cls.env.ref("base.nl").id, + "property_account_position_id": cls.position.id, + } + if vals is not None: + values.update(vals) + cls.customer = cls.partner_obj.create(values) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.sale_obj = cls.env["sale.order"] + cls._init_customer() + + @classmethod + def _create_sale_order(cls, vals=None): + vals = { + "partner_id": cls.customer.id, + } + sale_new = cls.sale_obj.new(vals) + sale_new.onchange_partner_id() + sale_vals = sale_new._convert_to_write(sale_new._cache) + cls.sale = cls.sale_obj.create(sale_vals) + with Form(cls.sale) as sale_form: + with sale_form.order_line.new() as line: + line.product_id = cls.product_c3po.product_variant_ids[0] + line.product_uom_qty = 3.0 diff --git a/intrastat_product/tests/test_company.py b/intrastat_product/tests/test_company.py new file mode 100644 index 0000000..7fe99ac --- /dev/null +++ b/intrastat_product/tests/test_company.py @@ -0,0 +1,40 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.tests.common import SavepointCase + +from .common import IntrastatProductCommon + + +class TestIntrastatCompany(IntrastatProductCommon): + """Tests for this module""" + + def test_company_values(self): + # Exempt for arrivals and dispatches => exempt + self.demo_company.update( + { + "intrastat_arrivals": "exempt", + "intrastat_dispatches": "exempt", + } + ) + self.assertEqual("exempt", self.demo_company.intrastat) + + # Extended for arrivals or dispatches => extended + self.demo_company.update( + { + "intrastat_arrivals": "extended", + } + ) + self.assertEqual("extended", self.demo_company.intrastat) + + # standard for arrivals or dispatches => standard + self.demo_company.update( + { + "intrastat_arrivals": "exempt", + "intrastat_dispatches": "standard", + } + ) + self.assertEqual("standard", self.demo_company.intrastat) + + +class TestIntrastatProductCase(TestIntrastatCompany, SavepointCase): + """ Test Intrastat Product """ diff --git a/intrastat_product/tests/test_intrastat_product.py b/intrastat_product/tests/test_intrastat_product.py new file mode 100644 index 0000000..af0b265 --- /dev/null +++ b/intrastat_product/tests/test_intrastat_product.py @@ -0,0 +1,67 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from psycopg2 import IntegrityError + +from odoo.exceptions import UserError, ValidationError +from odoo.tests.common import SavepointCase +from odoo.tools import mute_logger + +from .common import IntrastatProductCommon + + +class TestIntrastatProduct(IntrastatProductCommon): + """Tests for this module""" + + # Test duplicates + @mute_logger("odoo.sql_db") + def test_region(self): + with self.assertRaises(IntegrityError): + self._create_region() + + @mute_logger("odoo.sql_db") + def test_transaction(self): + self._create_transaction() + with self.assertRaises(IntegrityError): + self._create_transaction() + + @mute_logger("odoo.sql_db") + def test_transport_mode(self): + vals = {"code": 1, "name": "Sea"} + with self.assertRaises(IntegrityError): + self._create_transport_mode(vals) + + def test_copy(self): + """ + When copying declaration, the new one has an incremented revision + value. + """ + vals = {"declaration_type": "dispatches"} + self._create_declaration(vals) + decl_copy = self.declaration.copy() + self.assertEqual(self.declaration.revision + 1, decl_copy.revision) + + def test_declaration_no_country(self): + self.demo_company.country_id = False + with self.assertRaises(ValidationError): + self._create_declaration() + self.declaration.flush() + + def test_declaration_no_vat(self): + self.demo_company.partner_id.vat = False + with self.assertRaises(UserError): + self._create_declaration() + self.declaration.flush() + self.declaration._check_generate_xml() + + def test_declaration_state(self): + self._create_declaration() + self.declaration.unlink() + + self._create_declaration() + self.declaration.state = "done" + with self.assertRaises(UserError): + self.declaration.unlink() + + +class TestIntrastatProductCase(TestIntrastatProduct, SavepointCase): + """ Test Intrastat Product """ diff --git a/intrastat_product/tests/test_purchase_order.py b/intrastat_product/tests/test_purchase_order.py new file mode 100644 index 0000000..f6538c1 --- /dev/null +++ b/intrastat_product/tests/test_purchase_order.py @@ -0,0 +1,53 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from freezegun import freeze_time + +from odoo import fields +from odoo.tests.common import SavepointCase + +from .common_purchase import IntrastatPurchaseCommon + + +class TestIntrastatProductPurchase(IntrastatPurchaseCommon): + """Tests for this module""" + + def test_purchase_to_invoice_default(self): + date_order = "2021-09-01" + declaration_date = "2021-10-01" + with freeze_time(date_order): + self._create_purchase_order() + self.purchase.button_confirm() + self.purchase.picking_ids.action_assign() + for line in self.purchase.picking_ids.move_line_ids: + line.qty_done = line.product_uom_qty + self.purchase.picking_ids._action_done() + self.assertEqual("done", self.purchase.picking_ids.state) + + with freeze_time(date_order): + action = self.purchase.action_create_invoice() + invoice_id = action["res_id"] + invoice = self.move_obj.browse(invoice_id) + + invoice.invoice_date = fields.Date.from_string(date_order) + invoice.action_post() + + # Check if transport mode has been transmitted to invoice + # It should be None as not defined on sale order + self.assertFalse( + invoice.intrastat_transport_id, + ) + + vals = { + "declaration_type": "arrivals", + } + with freeze_time(declaration_date): + self._create_declaration(vals) + self.declaration.action_gather() + + self._check_line_values() + self.declaration.generate_declaration() + self._check_line_values(final=True) + + +class TestIntrastatProductPurchaseCase(TestIntrastatProductPurchase, SavepointCase): + """ Test Intrastat Purchase """ diff --git a/intrastat_product/tests/test_sale_order.py b/intrastat_product/tests/test_sale_order.py new file mode 100644 index 0000000..7d5b411 --- /dev/null +++ b/intrastat_product/tests/test_sale_order.py @@ -0,0 +1,88 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from freezegun import freeze_time + +from odoo.tests.common import SavepointCase + +from .common_sale import IntrastatSaleCommon + + +class TestIntrastatProductSale(IntrastatSaleCommon): + """Tests for this module""" + + def test_sale_to_invoice_default(self): + self._create_sale_order() + self.sale.action_confirm() + self.sale.picking_ids.action_assign() + for line in self.sale.picking_ids.move_line_ids: + line.qty_done = line.product_uom_qty + self.sale.picking_ids._action_done() + self.assertEqual("done", self.sale.picking_ids.state) + + invoice = self.sale._create_invoices() + invoice.action_post() + + # Check if transport mode has been transmitted to invoice + # It should be None as not defined on sale order + self.assertFalse( + invoice.intrastat_transport_id, + ) + + # Test specific transport set on sale to invoice + def test_sale_to_invoice(self): + self._create_sale_order() + # Set intrastat transport mode to rail + self.sale.intrastat_transport_id = self.transport_rail + self.sale.action_confirm() + self.sale.picking_ids.action_assign() + for line in self.sale.picking_ids.move_line_ids: + line.qty_done = line.product_uom_qty + self.sale.picking_ids._action_done() + self.assertEqual("done", self.sale.picking_ids.state) + + invoice = self.sale._create_invoices() + invoice.action_post() + + # Check if transport mode has been transmitted to invoice + self.assertEqual( + self.transport_rail, + invoice.intrastat_transport_id, + ) + + def test_sale_declaration(self): + date_order = "2021-09-01" + declaration_date = "2021-10-01" + with freeze_time(date_order): + self._create_sale_order() + # Set intrastat transport mode to rail + self.sale.intrastat_transport_id = self.transport_rail + self.sale.action_confirm() + self.sale.picking_ids.action_assign() + for line in self.sale.picking_ids.move_line_ids: + line.qty_done = line.product_uom_qty + self.sale.picking_ids._action_done() + self.assertEqual("done", self.sale.picking_ids.state) + + with freeze_time(date_order): + invoice = self.sale._create_invoices() + invoice.action_post() + + # Check if transport mode has been transmitted to invoice + self.assertEqual( + self.transport_rail, + invoice.intrastat_transport_id, + ) + vals = { + "declaration_type": "dispatches", + } + with freeze_time(declaration_date): + self._create_declaration(vals) + self.declaration.action_gather() + + self._check_line_values() + self.declaration.generate_declaration() + self._check_line_values(final=True) + + +class TestIntrastatProductSaleCase(TestIntrastatProductSale, SavepointCase): + """ Test Intrastat Sale """ diff --git a/intrastat_product/wizards/__init__.py b/intrastat_product/wizards/__init__.py new file mode 100644 index 0000000..3277b28 --- /dev/null +++ b/intrastat_product/wizards/__init__.py @@ -0,0 +1 @@ +from . import intrastat_result_view diff --git a/intrastat_product/wizards/intrastat_result_view.py b/intrastat_product/wizards/intrastat_result_view.py new file mode 100644 index 0000000..efd743f --- /dev/null +++ b/intrastat_product/wizards/intrastat_result_view.py @@ -0,0 +1,14 @@ +# Copyright 2010-2021 Akretion () +# Copyright 2009-2021 Noviat (http://www.noviat.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IntrastatResultView(models.TransientModel): + _name = "intrastat.result.view" + _description = "Pop-up to show errors on intrastat report generation" + + note = fields.Text( + string="Notes", readonly=True, default=lambda self: self._context.get("note") + ) diff --git a/intrastat_product/wizards/intrastat_result_view.xml b/intrastat_product/wizards/intrastat_result_view.xml new file mode 100644 index 0000000..16b9303 --- /dev/null +++ b/intrastat_product/wizards/intrastat_result_view.xml @@ -0,0 +1,22 @@ + + + + + intrastat.result_view_form + intrastat.result.view + +
+ + + + +
+
+
+
diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..864b829 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +odoo-test-helper +freezegun