#51 refactor: traer categorias anidadas en un solo request de productos

Issue #51: discriminacion y administracion de categorias de productos.

- product.read ahora solicita template.categories.name; se elimina la
  llamada separada model.product.category.read

- ProductTrytonService y CategoryTrytonService construyen el mapa
  {id_tryton: nombre} desde 'template.categories.' de los productos leídos
- import_from_external pasa de 3 llamadas RPC a 2
This commit is contained in:
2026-10-10 17:52:21 -05:00
parent 128ba215dd
commit 6e14dd9f36
5 changed files with 112 additions and 111 deletions

View File

@@ -53,15 +53,14 @@ class CategoryTrytonService:
return self._empty_report() return self._empty_report()
tryton_products = self._get_product_details(product_ids, context) tryton_products = self._get_product_details(product_ids, context)
category_ids = set() category_names = self._extract_category_names(tryton_products)
for tryton_product in tryton_products: if not category_names:
category_ids.update(self._extract_category_ids(tryton_product))
if not category_ids:
return self._empty_report() return self._empty_report()
tryton_categories = self._get_category_details( tryton_categories = [
sorted(category_ids), context {"id": category_id, "name": category_names[category_id]}
) for category_id in sorted(category_names)
]
checked = [ checked = [
{"id": c.get("id"), "name": c.get("name")} {"id": c.get("id"), "name": c.get("name")}
@@ -121,21 +120,27 @@ class CategoryTrytonService:
return self.client.call(method, params) or [] return self.client.call(method, params) or []
def _get_product_details(self, product_ids, context): def _get_product_details(self, product_ids, context):
tryton_fields = ["id", "name", "template.categories"] tryton_fields = [
"id",
"name",
"template.categories",
"template.categories.name",
]
method = "model.product.product.read" method = "model.product.product.read"
params = (product_ids, tryton_fields, context) params = (product_ids, tryton_fields, context)
return self.client.call(method, params) return self.client.call(method, params)
@staticmethod @staticmethod
def _extract_category_ids(tryton_product): def _extract_category_names(tryton_products):
"""Extrae los IDs de categorías de la respuesta RPC de Tryton""" """Extrae el mapa {id_tryton: nombre} de las categorías anidadas en
template = tryton_product.get("template.") or {} la respuesta de product.read (clave 'template.categories.name',
return template.get("categories") or [] agrupada bajo 'template.' como 'categories.')"""
names = {}
def _get_category_details(self, category_ids, context): for tryton_product in tryton_products:
method = "model.product.category.read" template = tryton_product.get("template.") or {}
params = (category_ids, ["id", "name"], context) for category in template.get("categories.") or []:
return self.client.call(method, params) names[category.get("id")] = category.get("name")
return names
def _empty_report(self): def _empty_report(self):
return { return {

View File

@@ -107,19 +107,16 @@ class ProductTrytonService:
reporte {"created": [...], "updated": [...]} reporte {"created": [...], "updated": [...]}
""" """
empty_report = {"created": [], "updated": []} empty_report = {"created": [], "updated": []}
category_ids = { category_names = self._extract_category_names(tryton_products)
category_id if not category_names:
for tryton_product in tryton_products
for category_id in self._extract_category_ids(tryton_product)
}
if not category_ids:
return {}, empty_report return {}, empty_report
categories = {} categories = {}
report = {"created": [], "updated": []} report = {"created": [], "updated": []}
tryton_categories = self._get_category_details( tryton_categories = [
sorted(category_ids), context {"id": category_id, "name": category_names[category_id]}
) for category_id in sorted(category_names)
]
for tryton_category in tryton_categories: for tryton_category in tryton_categories:
try: try:
category, status, changes = upsert_category(tryton_category) category, status, changes = upsert_category(tryton_category)
@@ -149,11 +146,17 @@ class ProductTrytonService:
template = tryton_product.get("template.") or {} template = tryton_product.get("template.") or {}
return template.get("categories") or [] return template.get("categories") or []
def _get_category_details(self, category_ids, context): @staticmethod
"""Obtiene los nombres de categorías desde Tryton""" def _extract_category_names(tryton_products):
method = "model.product.category.read" """Extrae el mapa {id_tryton: nombre} de las categorías anidadas en
params = (category_ids, ["id", "name"], context) la respuesta de product.read (clave 'template.categories.name',
return self.client.call(method, params) agrupada bajo 'template.' como 'categories.')"""
names = {}
for tryton_product in tryton_products:
template = tryton_product.get("template.") or {}
for category in template.get("categories.") or []:
names[category.get("id")] = category.get("name")
return names
def _sync_product_categories(self, product, tryton_product, categories): def _sync_product_categories(self, product, tryton_product, categories):
"""Vincula al producto las categorías importadas desde Tryton""" """Vincula al producto las categorías importadas desde Tryton"""
@@ -271,6 +274,7 @@ class ProductTrytonService:
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
] ]
method = "model.product.product.read" method = "model.product.product.read"
params = (product_ids, tryton_fields, context) params = (product_ids, tryton_fields, context)

View File

@@ -30,19 +30,22 @@ class TestCategoryTrytonImport(TestCase):
{ {
"id": 1, "id": 1,
"name": "Producto 1", "name": "Producto 1",
"template.": {"id": 999, "categories": [6]}, "template.": {
"id": 999,
"categories": [6],
"categories.": [{"id": 6, "name": "Alimentos"}],
},
}, },
{ {
"id": 2, "id": 2,
"name": "Producto 2", "name": "Producto 2",
"template.": {"id": 999, "categories": [7]}, "template.": {
"id": 999,
"categories": [7],
"categories.": [{"id": 7, "name": "Verduras"}],
},
}, },
] ]
if args[0] == "model.product.category.read":
return [
{"id": 6, "name": "Alimentos"},
{"id": 7, "name": "Verduras"},
]
raise Exception(f"args no esperados: {args}") raise Exception(f"args no esperados: {args}")
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
@@ -93,11 +96,15 @@ class TestCategoryTrytonImport(TestCase):
{ {
"id": 1, "id": 1,
"name": "Producto 1", "name": "Producto 1",
"template.": {"id": 999, "categories": [7]}, "template.": {
"id": 999,
"categories": [7],
"categories.": [
{"id": 7, "name": "Cereales y Granos"}
],
},
}, },
] ]
if args[0] == "model.product.category.read":
return [{"id": 7, "name": "Cereales y Granos"}]
raise Exception(f"args no esperados: {args}") raise Exception(f"args no esperados: {args}")
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
@@ -135,11 +142,13 @@ class TestCategoryTrytonImport(TestCase):
{ {
"id": 1, "id": 1,
"name": "Producto 1", "name": "Producto 1",
"template.": {"id": 999, "categories": [6]}, "template.": {
"id": 999,
"categories": [6],
"categories.": [{"id": 6, "name": "Alimentos"}],
},
}, },
] ]
if args[0] == "model.product.category.read":
return [{"id": 6, "name": "Alimentos"}]
raise Exception(f"args no esperados: {args}") raise Exception(f"args no esperados: {args}")
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
@@ -182,11 +191,13 @@ class TestCategoryTrytonImport(TestCase):
{ {
"id": 1, "id": 1,
"name": "Producto 1", "name": "Producto 1",
"template.": {"id": 999, "categories": [6]}, "template.": {
"id": 999,
"categories": [6],
"categories.": [{"id": 6, "name": None}],
},
}, },
] ]
if args[0] == "model.product.category.read":
return [{"id": 6, "name": None}]
raise Exception(f"args no esperados: {args}") raise Exception(f"args no esperados: {args}")
mock_call.side_effect = fake_call mock_call.side_effect = fake_call

View File

@@ -157,11 +157,13 @@ class ProductCategoryApiTest(TestCase):
{ {
"id": 1, "id": 1,
"name": "Producto 1", "name": "Producto 1",
"template.": {"id": 999, "categories": [6]}, "template.": {
"id": 999,
"categories": [6],
"categories.": [{"id": 6, "name": "Alimentos"}],
},
}, },
] ]
if args[0] == "model.product.category.read":
return [{"id": 6, "name": "Alimentos"}]
raise Exception(f"args no esperados: {args}") raise Exception(f"args no esperados: {args}")
mock_call.side_effect = fake_call mock_call.side_effect = fake_call

View File

@@ -56,6 +56,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -157,6 +158,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -226,6 +228,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -288,6 +291,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -354,6 +358,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -364,22 +369,17 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"list_price": Decimal("25000"), "list_price": Decimal("25000"),
"name": "Producto 1", "name": "Producto 1",
"default_uom.": {"id": 1, "rec_name": "Unit"}, "default_uom.": {"id": 1, "rec_name": "Unit"},
"template.": {"id": 999, "categories": [5, 8]}, "template.": {
"id": 999,
"categories": [5, 8],
"categories.": [
{"id": 5, "name": "Abarrotes"},
{"id": 8, "name": "Bebidas"},
],
},
}, },
] ]
category_read = "model.product.category.read"
category_args = (
[5, 8],
["id", "name"],
{"company": 1},
)
if args == (category_read, category_args):
return [
{"id": 5, "name": "Abarrotes"},
{"id": 8, "name": "Bebidas"},
]
raise Exception( raise Exception(
f"Sorry, args non expected on this test: {args}" f"Sorry, args non expected on this test: {args}"
) )
@@ -445,6 +445,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -455,21 +456,14 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"list_price": Decimal("25000"), "list_price": Decimal("25000"),
"name": "Producto 1", "name": "Producto 1",
"default_uom.": {"id": 1, "rec_name": "Unit"}, "default_uom.": {"id": 1, "rec_name": "Unit"},
"template.": {"id": 999, "categories": [7]}, "template.": {
"id": 999,
"categories": [7],
"categories.": [{"id": 7, "name": "Cereales"}],
},
}, },
] ]
category_read = "model.product.category.read"
category_args = (
[7],
["id", "name"],
{"company": 1},
)
if args == (category_read, category_args):
return [
{"id": 7, "name": "Cereales"},
]
raise Exception( raise Exception(
f"Sorry, args non expected on this test: {args}" f"Sorry, args non expected on this test: {args}"
) )
@@ -536,6 +530,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -546,21 +541,14 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"list_price": Decimal("25000"), "list_price": Decimal("25000"),
"name": "Producto 1", "name": "Producto 1",
"default_uom.": {"id": 1, "rec_name": "Unit"}, "default_uom.": {"id": 1, "rec_name": "Unit"},
"template.": {"id": 999, "categories": [5]}, "template.": {
"id": 999,
"categories": [5],
"categories.": [{"id": 5, "name": "Abarrotes"}],
},
}, },
] ]
category_read = "model.product.category.read"
category_args = (
[5],
["id", "name"],
{"company": 1},
)
if args == (category_read, category_args):
return [
{"id": 5, "name": "Abarrotes"},
]
raise Exception( raise Exception(
f"Sorry, args non expected on this test: {args}" f"Sorry, args non expected on this test: {args}"
) )
@@ -627,6 +615,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -688,6 +677,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -698,21 +688,16 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"list_price": Decimal("4500"), "list_price": Decimal("4500"),
"name": "Papa", "name": "Papa",
"default_uom.": {"id": 2, "rec_name": "Kilogram"}, "default_uom.": {"id": 2, "rec_name": "Kilogram"},
"template.": {"id": 999, "categories": [9]}, "template.": {
"id": 999,
"categories": [9],
"categories.": [
{"id": 9, "name": "Tubérculos"}
],
},
}, },
] ]
category_read = "model.product.category.read"
category_args = (
[9],
["id", "name"],
{"company": 1},
)
if args == (category_read, category_args):
return [
{"id": 9, "name": "Tubérculos"},
]
raise Exception( raise Exception(
f"Sorry, args non expected on this test: {args}" f"Sorry, args non expected on this test: {args}"
) )
@@ -786,6 +771,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"default_uom.rec_name", "default_uom.rec_name",
"list_price", "list_price",
"template.categories", "template.categories",
"template.categories.name",
], ],
{"company": 1}, {"company": 1},
) )
@@ -796,21 +782,14 @@ class TestProductsFromTryton(TestCase, LoginMixin):
"list_price": Decimal("4500"), "list_price": Decimal("4500"),
"name": "Papa", "name": "Papa",
"default_uom.": {"id": 2, "rec_name": "Kilogram"}, "default_uom.": {"id": 2, "rec_name": "Kilogram"},
"template.": {"id": 999, "categories": [8]}, "template.": {
"id": 999,
"categories": [8],
"categories.": [{"id": 8, "name": "Bebidas"}],
},
}, },
] ]
category_read = "model.product.category.read"
category_args = (
[8],
["id", "name"],
{"company": 1},
)
if args == (category_read, category_args):
return [
{"id": 8, "name": "Bebidas"},
]
raise Exception( raise Exception(
f"Sorry, args non expected on this test: {args}" f"Sorry, args non expected on this test: {args}"
) )