Skip to content

Commit

Permalink
edi_exchange_template: declare tmpl to use explicitly
Browse files Browse the repository at this point in the history
Relying on the code by convention was fragile for many reasons.
Most important:

1. users have to remember about this convention
2. is not clear which template is going to be used by a specific type

With this change we make it more clear and deprecate the old behavior.
  • Loading branch information
simahawk committed Feb 5, 2025
1 parent 621e860 commit f4ec1bc
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 27 deletions.
3 changes: 2 additions & 1 deletion edi_exchange_template_oca/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"name": "EDI Exchange Template",
"summary": """Allows definition of exchanges via templates.""",
"version": "14.0.1.5.2",
"version": "14.0.1.6.0",
"development_status": "Beta",
"license": "LGPL-3",
"author": "ACSONE,Camptocamp,Odoo Community Association (OCA)",
Expand All @@ -15,5 +15,6 @@
"data": [
"security/ir_model_access.xml",
"views/edi_exchange_template_output_views.xml",
"views/edi_exchange_type_views.xml",
],
}
60 changes: 60 additions & 0 deletions edi_exchange_template_oca/migrations/14.0.1.6.0/post-migrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2025 Camptocamp SA (http://www.camptocamp.com)
# @author Simone Orsi <simahawk@gmail.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

import logging

from odoo import SUPERUSER_ID, api

_logger = logging.getLogger(__name__)


def migrate(cr, version):
if not version:
return

env = api.Environment(cr, SUPERUSER_ID, {})

# Look for templates w/ a type set and set them as allowed on the type
# plus link the type to the template
templates = env["edi.exchange.template.output"].search([("type_id", "!=", False)])
for tmpl in templates:
allowed_type = tmpl.type_id
tmpl.type_id.output_template_id = tmpl
tmpl.allowed_type_ids += allowed_type
tmpl.type_id = None
_logger.info(
"Set output template %s on exchange type %s",
tmpl.name,
allowed_type.name,
)

# Look for types w/o a template
# and find the template by code to set as output template
types = env["edi.exchange.type"].search([("output_template_id", "=", False)])
for t in types:
settings = t.get_settings()
generate_usage = settings.get("components", {}).get("generate", {}).get("usage")
if generate_usage:
templates = env["edi.exchange.template.output"].search(
[
("code", "=", generate_usage),
("backend_type_id", "=", t.backend_type_id.id),
]
)
if len(templates) == 1:
tmpl = templates[0]
tmpl.allowed_type_ids += t
t.output_template_id = tmpl
_logger.info(
"Set output template %s on exchange type %s",
t.output_template_id.name,
t.name,
)
continue

_logger.warning(
"Cannot set a template for exchange type %s. "
"Either no template found or multiple found.",
t.name,
)
1 change: 1 addition & 0 deletions edi_exchange_template_oca/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import edi_backend
from . import edi_exchange_template_mixin
from . import edi_exchange_template_output
from . import edi_exchange_type
53 changes: 38 additions & 15 deletions edi_exchange_template_oca/models/edi_backend.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Copyright 2020 ACSONE SA
# Copyright 2025 Camptocamp SA
# @author Simone Orsi <simahawk@gmail.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import models
import logging

from odoo import fields, models

_logger = logging.getLogger(__name__)


class EDIBackend(models.Model):
Expand All @@ -26,30 +31,48 @@ def output_template_model(self):
return self.env["edi.exchange.template.output"]

def _get_output_template(self, exchange_record, code=None):
"""Retrieve output templates by convention.
"""Retrieve output template.
Template's code must match the same component usage as per normal components.
:param exchange_record: record to generate.
:param code: explicit template code to lookup.
"""
tmpl = exchange_record.type_id.output_template_id
if tmpl:
return tmpl
_logger.warning(
"DEPRECATED: please set the template to use explicitly on the type %s.",
exchange_record.type_id.code,
)
# Deprecated behavior: emplate's code must match
# the same component usage as per normal components.t
# Wherever possible old types relying on code
# have been migrated to use the explicit template.
search = self.output_template_model.search
# TODO: maybe we can add a m2o to output templates
# but then we would need another for input templates if they are introduced.
tmpl = None
# NOTE: this is kind of broken because
# it should use the usage of the generate component one.
# As this is depraecated we can leave it as is.
code = code or exchange_record.type_id.code
if code:
domain = [("code", "=", code)]
tmpl = search(domain, limit=1)
if tmpl:
return tmpl
for domain in self._get_output_template_domains(exchange_record):
tmpl = search(domain, limit=1)
if tmpl:
break
tmpl = self._get_output_template_fallback(exchange_record)
return tmpl

def _get_output_template_domains(self, exchange_record):
def _get_output_template_fallback(self, exchange_record):
"""Retrieve domains to lookup for templates by priority."""
backend_type_leaf = [("backend_type_id", "=", self.backend_type_id.id)]
exchange_type_leaf = [("type_id", "=", exchange_record.type_id.id)]
full_match_domain = backend_type_leaf + exchange_type_leaf
partial_match_domain = backend_type_leaf
return full_match_domain, partial_match_domain
# Match by backend and allowed types
base_domain = [
("backend_type_id", "=", self.backend_type_id.id),
"|",
("allowed_type_ids", "=", exchange_record.type_id.id),
("allowed_type_ids", "=", False),
]
candidates = self.output_template_model.search(base_domain)
for rec in candidates:
if rec.type_id == exchange_record.type_id:
return rec

Check warning on line 76 in edi_exchange_template_oca/models/edi_backend.py

View check run for this annotation

Codecov / codecov/patch

edi_exchange_template_oca/models/edi_backend.py#L76

Added line #L76 was not covered by tests
# Take the 1st one having allowed_type_ids set
return fields.first(candidates.sorted(lambda x: 0 if x.allowed_type_ids else 1))
31 changes: 30 additions & 1 deletion edi_exchange_template_oca/models/edi_exchange_template_mixin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2020 ACSONE SA
# Copyright 2025 Camptocamp SA
# @author Simone Orsi <simahawk@gmail.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import datetime
Expand All @@ -7,7 +8,7 @@

import pytz

from odoo import fields, models
from odoo import _, api, exceptions, fields, models
from odoo.tools import DotDict, safe_eval

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -41,12 +42,23 @@ class EDIExchangeTemplateMixin(models.AbstractModel):
ondelete="restrict",
required=True,
)
# TODO: deprecate this field.
# Templates should be explicitly linked by a type
# and use `allowed_type_ids` to define allowed types.
type_id = fields.Many2one(
string="EDI Exchange type",
comodel_name="edi.exchange.type",
ondelete="cascade",
auto_join=True,
)
allowed_type_ids = fields.Many2many(
comodel_name="edi.exchange.type",
relation="edi_exchange_template_type_rel",
column1="template_id",
column2="type_id",
string="Allowed Exchange Types",
help="Types allowed to use this template.",
)
backend_id = fields.Many2one(
comodel_name="edi.backend",
ondelete="cascade",
Expand Down Expand Up @@ -147,3 +159,20 @@ def _get_validator(self, exchange_record):

def validate(self, exchange_record):
pass

@api.constrains("type_id", "allowed_type_ids")
def _check_type_id(self):
for rec in self:
if (
rec.type_id
and rec.allowed_type_ids
and rec.type_id not in rec.allowed_type_ids
):
raise exceptions.ValidationError(
_(
"The selected type must appear among the allowed types. "
"NOTE: the type field is deprecated and will be removed soon. "
"Use 'Allowed types' instead and set the template to use "
"explicitly on the type that will use this template."
)
)
45 changes: 45 additions & 0 deletions edi_exchange_template_oca/models/edi_exchange_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2025 Camptocamp SA
# @author: Simone Orsi <simone.orsi@camptocamp.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).


from odoo import _, api, exceptions, fields, models


class EDIExchangeType(models.Model):
_inherit = "edi.exchange.type"

output_template_id = fields.Many2one(
comodel_name="edi.exchange.template.output",
string="Exchange Template",
ondelete="restrict",
required=False,
help="Template used to generate or process this type.",
)
output_template_allowed_ids = fields.Many2many(
comodel_name="edi.exchange.template.output",
# Inverse relation is defined in `edi.exchange.template.mixin`
relation="edi_exchange_template_type_rel",
column1="type_id",
column2="template_id",
string="Allowed Templates",
help="Templates allowed to be used with this type.",
)

@api.constrains("output_template_id")
def _check_output_template_id(self):
for rec in self:
tmpl = rec.output_template_id
if tmpl.type_id:
if tmpl.type_id != rec:
raise exceptions.ValidationError(

Check warning on line 35 in edi_exchange_template_oca/models/edi_exchange_type.py

View check run for this annotation

Codecov / codecov/patch

edi_exchange_template_oca/models/edi_exchange_type.py#L35

Added line #L35 was not covered by tests
_("Template type must match exchange type.")
)
if (
tmpl
and tmpl.allowed_type_ids
and tmpl not in rec.output_template_allowed_ids
):
raise exceptions.ValidationError(
_("Template not allowed for this type.")
)
59 changes: 53 additions & 6 deletions edi_exchange_template_oca/tests/test_backend_and_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# @author: Simone Orsi <simone.orsi@camptocamp.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).


from odoo.exceptions import ValidationError
from odoo.tests.common import SavepointCase
from odoo.tools import mute_logger


class TestExchangeType(SavepointCase):
Expand Down Expand Up @@ -47,21 +48,17 @@ def setUpClass(cls):
"code": "tmpl_test_type_out1",
"name": "Out 1",
"backend_type_id": cls.backend_type.id,
"type_id": cls.type_out1.id,
"template_id": qweb_tmpl.id,
"output_type": "txt",
"allowed_type_ids": [(6, 0, [cls.type_out1.id, cls.type_out2.id])],
}
)
cls.tmpl_out2 = model.create(
{
"code": "tmpl_test_type_out2",
"name": "Out 2",
"backend_type_id": cls.env.ref("edi_oca.demo_edi_backend_type").id,
"type_id": cls.type_out1.id,
"template_id": qweb_tmpl.id,
"output_type": "txt",
"allowed_type_ids": [(6, 0, [cls.type_out2.id])],
}
)
vals = {
Expand All @@ -79,7 +76,8 @@ def setUpClass(cls):
}
cls.record2 = cls.backend.create_record("test_type_out2", vals)

# TODO: getting a template via code is deprecated
# TODO: getting a template via code or relying on a fallback is deprecated
@mute_logger("odoo.addons.edi_exchange_template_oca.models.edi_backend")
def test_get_template_by_code(self):
self.assertEqual(
self.backend._get_output_template(self.record2, code=self.tmpl_out1.code),
Expand All @@ -94,6 +92,7 @@ def test_get_template_by_code(self):
self.backend._get_output_template(self.record2), self.tmpl_out2
)

@mute_logger("odoo.addons.edi_exchange_template_oca.models.edi_backend")
def test_get_template_by_fallback(self):
self.assertEqual(
self.backend._get_output_template(self.record2, code=self.tmpl_out1.code),
Expand All @@ -108,3 +107,51 @@ def test_get_template_by_fallback(self):
self.assertEqual(
self.backend._get_output_template(self.record2), self.tmpl_out1
)

@mute_logger("odoo.addons.edi_exchange_template_oca.models.edi_backend")
def test_get_template_allowed(self):
# No match by code on both templates
self.assertNotEqual(self.type_out1.code, self.tmpl_out1.code)
self.assertNotEqual(self.type_out1.code, self.tmpl_out2.code)
# Tmpl 2 is available for all types
self.assertFalse(self.tmpl_out2.allowed_type_ids)
# Tmpl 1 is explicitly set as allowed for type 1 -> we should get it 1st
self.tmpl_out1.allowed_type_ids = self.type_out1
self.assertEqual(
self.backend._get_output_template(self.record1),
self.tmpl_out1,
)
# Add a template, but still the 1st one is returned
self.tmpl_out1.allowed_type_ids += self.type_out2
self.assertEqual(
self.backend._get_output_template(self.record1), self.tmpl_out1
)

def test_template_validation(self):
self.tmpl_out1.allowed_type_ids = self.type_out1
self.assertIn(self.tmpl_out1, self.type_out1.output_template_allowed_ids)
with self.assertRaisesRegex(ValidationError, "Template not allowed"):
self.type_out2.output_template_id = self.tmpl_out1
with self.assertRaisesRegex(
ValidationError, "The selected type must appear among the allowed types"
):
self.tmpl_out1.type_id = self.type_out2

def test_get_template_selected(self):
self.type_out1.output_template_id = self.tmpl_out1
self.type_out2.output_template_id = self.tmpl_out2
self.assertEqual(
self.backend._get_output_template(self.record1), self.tmpl_out1
)
self.assertEqual(
self.backend._get_output_template(self.record2), self.tmpl_out2
)
# inverse
self.type_out1.output_template_id = self.tmpl_out2
self.type_out2.output_template_id = self.tmpl_out1
self.assertEqual(
self.backend._get_output_template(self.record1), self.tmpl_out2
)
self.assertEqual(
self.backend._get_output_template(self.record2), self.tmpl_out1
)
Loading

0 comments on commit f4ec1bc

Please sign in to comment.