Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions product_variant_configurator/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import hooks
from . import models
42 changes: 42 additions & 0 deletions product_variant_configurator/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import fields
from odoo.models import NewId


def _patch_many2one_no_wrap_existing_parent():
"""Patch ``Many2one.convert_to_cache`` so that linking a new child
record to an existing parent through a delegate (``_inherits``) field
keeps the parent's real id instead of wrapping it in :class:`NewId`.

The standard Odoo behaviour wraps the parent id whenever the child
record is new, on the assumption that the parent is being created
together with the child. That fits Odoo's native flow (a new
variant auto-created alongside a new template) but breaks the
variant configurator's flow where a new variant is attached to an
*existing* template: the wrap then makes
``Field._compute_company_dependent`` reads on the parent return
empty, because its result dict is keyed by real ids while the loop
uses the record's ``NewId`` as the lookup key.
"""
if getattr(fields.Many2one, "_no_wrap_existing_parent_patched", False):
return
_original_convert_to_cache = fields.Many2one.convert_to_cache

def convert_to_cache(self, value, record, validate=True):
if (
self.delegate
and record
and not any(record._ids)
and isinstance(value, int)
and not isinstance(value, NewId)
and value > 0
):
return value
return _original_convert_to_cache(self, value, record, validate)

fields.Many2one.convert_to_cache = convert_to_cache
fields.Many2one._no_wrap_existing_parent_patched = True


_patch_many2one_no_wrap_existing_parent()
2 changes: 2 additions & 0 deletions product_variant_configurator/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from . import fake_models # noqa: F401
from . import test_product_variant_configurator
from . import test_product_configurator_attribute
from . import test_product_pricelist
from . import test_product_variants
from . import test_company_dependent_inherited
12 changes: 12 additions & 0 deletions product_variant_configurator/tests/fake_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from odoo import fields, models


class ProductTemplate(models.Model):
# Test-only company_dependent field
_inherit = "product.template" # pylint: disable=consider-merging-classes-inherited

x_test_cd_partner = fields.Many2one(
comodel_name="res.partner",
string="Test CD Partner",
company_dependent=True,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from odoo.tests import tagged
from odoo.tests.common import TransactionCase

from .fake_models import ProductTemplate as _TestProductTemplate


@tagged("post_install", "-at_install")
class TestCompanyDependentInherited(TransactionCase):
_test_field_name = "x_test_cd_partner"

@classmethod
def setUpClass(cls):
super().setUpClass()
_TestProductTemplate._build_model(cls.env.registry, cls.env.cr)
cls.env.registry.setup_models(cls.env.cr)
ctx = dict(cls.env.context, update_custom_fields=True)
cls.env.registry.init_models(cls.env.cr, ["product.template"], ctx)

cls.partner_a = cls.env["res.partner"].create({"name": "Partner A"})
cls.partner_default = cls.env["res.partner"].create({"name": "Partner Default"})
cls.tmpl = cls.env["product.template"].create(
{"name": "Tpl A", "no_create_variants": "yes"}
)
cls.tmpl.x_test_cd_partner = cls.partner_a

@classmethod
def tearDownClass(cls):
Template = cls.env["product.template"]
if cls._test_field_name in Template._fields:
Template._pop_field(cls._test_field_name)
cls.env.registry.setup_models(cls.env.cr)
super().tearDownClass()

def test_new_variant_keeps_existing_template_id_unwrapped(self):
new_variant = self.env["product.product"].new({"product_tmpl_id": self.tmpl.id})
self.assertEqual(new_variant.product_tmpl_id, self.tmpl)
self.assertEqual(new_variant.product_tmpl_id._ids, (self.tmpl.id,))

def test_new_variant_reads_template_company_dependent_value(self):
new_variant = self.env["product.product"].new({"product_tmpl_id": self.tmpl.id})
self.assertEqual(new_variant.x_test_cd_partner, self.partner_a)

def test_new_variant_reads_template_value_over_company_default(self):
self.env["ir.property"]._set_default(
self._test_field_name,
"product.template",
self.partner_default,
)
new_variant = self.env["product.product"].new({"product_tmpl_id": self.tmpl.id})
self.assertEqual(new_variant.x_test_cd_partner, self.partner_a)
Loading