#51 fix: detectar cambios de categorias al sincronizar productos
Issue #51: discriminacion y administracion de categorias de productos. - _need_update compara las categorias (external_id) local vs Tryton - _describe_changes reporta categorias agregadas (+Nombre) y removidas (-Nombre) - Actualiza test que asumia asignacion de categorias sin marcar update
This commit is contained in:
@@ -214,8 +214,50 @@ class ProductTrytonService:
|
|||||||
changes.append(
|
changes.append(
|
||||||
f"Unidad: {product.measuring_unit} → {unit_name}"
|
f"Unidad: {product.measuring_unit} → {unit_name}"
|
||||||
)
|
)
|
||||||
|
category_changes = self._describe_category_changes(
|
||||||
|
product, tryton_product
|
||||||
|
)
|
||||||
|
if category_changes:
|
||||||
|
changes.append(category_changes)
|
||||||
return ", ".join(changes)
|
return ", ".join(changes)
|
||||||
|
|
||||||
|
def _describe_category_changes(self, product, tryton_product):
|
||||||
|
"""Describe los cambios de categorías entre el producto local y Tryton"""
|
||||||
|
tryton_ids = set(
|
||||||
|
str(category_id)
|
||||||
|
for category_id in self._extract_category_ids(tryton_product)
|
||||||
|
)
|
||||||
|
local = {
|
||||||
|
external_id: name
|
||||||
|
for external_id, name in product.categories.values_list(
|
||||||
|
"external_id", "name"
|
||||||
|
)
|
||||||
|
if external_id
|
||||||
|
}
|
||||||
|
local_ids = set(local)
|
||||||
|
|
||||||
|
added = sorted(tryton_ids - local_ids)
|
||||||
|
removed = sorted(local_ids - tryton_ids)
|
||||||
|
if not added and not removed:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if added:
|
||||||
|
names = {}
|
||||||
|
for external_id in added:
|
||||||
|
category = (
|
||||||
|
ProductCategory.objects.filter(
|
||||||
|
external_id=external_id
|
||||||
|
)
|
||||||
|
.values_list("name", flat=True)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
names[external_id] = category or external_id
|
||||||
|
parts.append(", ".join(f"+{names[e]}" for e in added))
|
||||||
|
if removed:
|
||||||
|
parts.append(", ".join(f"-{local[e]}" for e in removed))
|
||||||
|
return f"Categoría: {', '.join(parts)}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_price(value):
|
def _format_price(value):
|
||||||
"""Formatea un precio sin ceros decimales innecesarios"""
|
"""Formatea un precio sin ceros decimales innecesarios"""
|
||||||
@@ -269,6 +311,17 @@ class ProductTrytonService:
|
|||||||
unit = tryton_product.get("default_uom.")
|
unit = tryton_product.get("default_uom.")
|
||||||
if not product.measuring_unit == unit.get("rec_name"):
|
if not product.measuring_unit == unit.get("rec_name"):
|
||||||
return True
|
return True
|
||||||
|
tryton_category_ids = set(
|
||||||
|
str(category_id)
|
||||||
|
for category_id in self._extract_category_ids(tryton_product)
|
||||||
|
)
|
||||||
|
local_category_ids = set(
|
||||||
|
product.categories.exclude(
|
||||||
|
external_id__isnull=True
|
||||||
|
).values_list("external_id", flat=True)
|
||||||
|
)
|
||||||
|
if tryton_category_ids != local_category_ids:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _create_product(self, tryton_product):
|
def _create_product(self, tryton_product):
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
|
|
||||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
def test_import_assigns_categories_to_untouched_products(
|
def test_import_assigns_categories_marks_product_as_updated(
|
||||||
self, mock_connect, mock_call
|
self, mock_connect, mock_call
|
||||||
):
|
):
|
||||||
mock_connect.return_value = None
|
mock_connect.return_value = None
|
||||||
@@ -724,12 +724,20 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
content = json.loads(response.content.decode("utf-8"))
|
content = json.loads(response.content.decode("utf-8"))
|
||||||
|
self.assertEqual(content["untouched_products"], [])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
content["untouched_products"], [{"id": 2, "name": "Papa"}]
|
content["updated_products"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Papa",
|
||||||
|
"detail": "Categoría: +Tubérculos",
|
||||||
|
}
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
untouched_product = Product.objects.get(id=2)
|
updated_product = Product.objects.get(id=2)
|
||||||
categories = untouched_product.categories.all()
|
categories = updated_product.categories.all()
|
||||||
self.assertEqual(len(categories), 1)
|
self.assertEqual(len(categories), 1)
|
||||||
self.assertEqual(categories[0].name, "Tubérculos")
|
self.assertEqual(categories[0].name, "Tubérculos")
|
||||||
self.assertEqual(categories[0].external_id, "9")
|
self.assertEqual(categories[0].external_id, "9")
|
||||||
@@ -744,3 +752,104 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_category_change_marks_product_as_updated(
|
||||||
|
self, mock_connect, mock_call
|
||||||
|
):
|
||||||
|
mock_connect.return_value = None
|
||||||
|
local_abarrotes = ProductCategory.objects.create(
|
||||||
|
name="Abarrotes", external_id="5"
|
||||||
|
)
|
||||||
|
self.product2.categories.add(local_abarrotes)
|
||||||
|
|
||||||
|
def fake_call(*args, **kwargs):
|
||||||
|
product_search = "model.product.product.search"
|
||||||
|
search_args = [
|
||||||
|
[["salable", "=", True]],
|
||||||
|
0,
|
||||||
|
1000,
|
||||||
|
[["rec_name", "ASC"], ["id", None]],
|
||||||
|
{"company": 1},
|
||||||
|
]
|
||||||
|
if args == (product_search, search_args):
|
||||||
|
return [192]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[192],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (product_read, product_args):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": 192,
|
||||||
|
"list_price": Decimal("4500"),
|
||||||
|
"name": "Papa",
|
||||||
|
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
||||||
|
"template.": {"id": 999, "categories": [8]},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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(
|
||||||
|
f"Sorry, args non expected on this test: {args}"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_call.side_effect = fake_call
|
||||||
|
|
||||||
|
url = "/don_confiao/api/importar_productos_de_tryton"
|
||||||
|
response = self.client.post(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
content = json.loads(response.content.decode("utf-8"))
|
||||||
|
self.assertEqual(content["untouched_products"], [])
|
||||||
|
self.assertEqual(content["failed_products"], [])
|
||||||
|
self.assertEqual(
|
||||||
|
content["updated_products"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": self.product2.pk,
|
||||||
|
"name": "Papa",
|
||||||
|
"detail": "Categoría: +Bebidas, -Abarrotes",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
content["created_categories"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": ProductCategory.objects.get(
|
||||||
|
external_id="8"
|
||||||
|
).pk,
|
||||||
|
"name": "Bebidas",
|
||||||
|
"external_id": "8",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
refreshed_product = Product.objects.get(pk=self.product2.pk)
|
||||||
|
categories = list(refreshed_product.categories.all())
|
||||||
|
self.assertEqual(
|
||||||
|
[(c.external_id, c.name) for c in categories],
|
||||||
|
[("8", "Bebidas")],
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user