#58 feat: importar y vincular categorías de Tryton al sincronizar productos
- Campo external_id en ProductCategory para vincular con Tryton (migración 0055) - Import/upsert de categorías usadas por los productos sincronizados - Asignación M2M en productos creados, actualizados y sin cambios - Respuesta del import incluye created_categories y updated_categories con detalle - Fix: leer las categorías desde la clave agrupada "template." que devuelve el RPC de Tryton
This commit is contained in:
@@ -3,7 +3,7 @@ from decimal import Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from ..models.products import Product
|
||||
from ..models.products import Product, ProductCategory
|
||||
from .Mixins import LoginMixin
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
@@ -65,18 +66,21 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"list_price": Decimal("25000"),
|
||||
"name": "Producto 1",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
{
|
||||
"id": 191,
|
||||
"list_price": Decimal("6000"),
|
||||
"name": "Panela2",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
{
|
||||
"id": 192,
|
||||
"list_price": Decimal("4500"),
|
||||
"name": "Papa",
|
||||
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -107,6 +111,8 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"detail": "Nombre: Panela → Panela2, Precio: 5000 → 6000, Unidad: UNIT → Unit",
|
||||
}
|
||||
],
|
||||
"created_categories": [],
|
||||
"updated_categories": [],
|
||||
}
|
||||
self.assertEqual(content, expected_response)
|
||||
|
||||
@@ -150,6 +156,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
@@ -160,6 +167,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"list_price": Decimal("25000"),
|
||||
"name": self.product.name,
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -186,6 +194,8 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
}
|
||||
],
|
||||
"updated_products": [],
|
||||
"created_categories": [],
|
||||
"updated_categories": [],
|
||||
}
|
||||
self.assertEqual(content, expected_response)
|
||||
|
||||
@@ -215,6 +225,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
@@ -225,6 +236,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"list_price": None,
|
||||
"name": "ENVASES Y EMPAQUES",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -275,6 +287,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
@@ -285,12 +298,14 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
"list_price": Decimal("6000"),
|
||||
"name": "Panela",
|
||||
"default_uom.": None,
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
{
|
||||
"id": 192,
|
||||
"list_price": Decimal("4500"),
|
||||
"name": "Papa",
|
||||
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -311,3 +326,421 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
self.assertEqual(failure["name"], "Panela")
|
||||
self.assertIn("Error al actualizar producto", failure["error"])
|
||||
self.assertEqual(content["untouched_products"], [{"id": 2, "name": "Papa"}])
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_import_products_with_categories(self, mock_connect, mock_call):
|
||||
mock_connect.return_value = None
|
||||
|
||||
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 [190]
|
||||
|
||||
product_read = "model.product.product.read"
|
||||
product_args = (
|
||||
[190],
|
||||
[
|
||||
"id",
|
||||
"name",
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (product_read, product_args):
|
||||
return [
|
||||
{
|
||||
"id": 190,
|
||||
"list_price": Decimal("25000"),
|
||||
"name": "Producto 1",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": [5, 8]},
|
||||
},
|
||||
]
|
||||
|
||||
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(
|
||||
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["failed_products"], [])
|
||||
self.assertEqual(
|
||||
content["created_products"], [{"id": 3, "name": "Producto 1"}]
|
||||
)
|
||||
self.assertEqual(content["updated_categories"], [])
|
||||
self.assertEqual(
|
||||
content["created_categories"],
|
||||
[
|
||||
{"id": 1, "name": "Abarrotes", "external_id": "5"},
|
||||
{"id": 2, "name": "Bebidas", "external_id": "8"},
|
||||
],
|
||||
)
|
||||
|
||||
created_product = Product.objects.get(name="Producto 1")
|
||||
self.assertEqual(created_product.external_id, str(190))
|
||||
categories = created_product.categories.order_by("external_id")
|
||||
self.assertEqual(
|
||||
[(c.external_id, c.name) for c in categories],
|
||||
[("5", "Abarrotes"), ("8", "Bebidas")],
|
||||
)
|
||||
self.assertEqual(ProductCategory.objects.count(), 2)
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_import_updates_category_name_from_tryton(
|
||||
self, mock_connect, mock_call
|
||||
):
|
||||
mock_connect.return_value = None
|
||||
local_category = ProductCategory.objects.create(
|
||||
name="Granos", external_id="7"
|
||||
)
|
||||
|
||||
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 [190]
|
||||
|
||||
product_read = "model.product.product.read"
|
||||
product_args = (
|
||||
[190],
|
||||
[
|
||||
"id",
|
||||
"name",
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (product_read, product_args):
|
||||
return [
|
||||
{
|
||||
"id": 190,
|
||||
"list_price": Decimal("25000"),
|
||||
"name": "Producto 1",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": [7]},
|
||||
},
|
||||
]
|
||||
|
||||
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(
|
||||
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["created_categories"], [])
|
||||
self.assertEqual(
|
||||
content["updated_categories"],
|
||||
[
|
||||
{
|
||||
"id": local_category.pk,
|
||||
"name": "Cereales",
|
||||
"external_id": "7",
|
||||
"detail": "Nombre: Granos → Cereales",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
refreshed_category = ProductCategory.objects.get(
|
||||
pk=local_category.pk
|
||||
)
|
||||
self.assertEqual(refreshed_category.name, "Cereales")
|
||||
self.assertEqual(refreshed_category.external_id, "7")
|
||||
self.assertEqual(ProductCategory.objects.count(), 1)
|
||||
|
||||
created_product = Product.objects.get(name="Producto 1")
|
||||
self.assertEqual(
|
||||
list(created_product.categories.all()), [refreshed_category]
|
||||
)
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_import_links_existing_category_by_name(
|
||||
self, mock_connect, mock_call
|
||||
):
|
||||
mock_connect.return_value = None
|
||||
existing_category = ProductCategory.objects.create(name="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 [190]
|
||||
|
||||
product_read = "model.product.product.read"
|
||||
product_args = (
|
||||
[190],
|
||||
[
|
||||
"id",
|
||||
"name",
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (product_read, product_args):
|
||||
return [
|
||||
{
|
||||
"id": 190,
|
||||
"list_price": Decimal("25000"),
|
||||
"name": "Producto 1",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": [5]},
|
||||
},
|
||||
]
|
||||
|
||||
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(
|
||||
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["created_categories"], [])
|
||||
self.assertEqual(
|
||||
content["updated_categories"],
|
||||
[
|
||||
{
|
||||
"id": existing_category.pk,
|
||||
"name": "Abarrotes",
|
||||
"external_id": "5",
|
||||
"detail": "External ID: None → 5",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
ProductCategory.objects.filter(name="Abarrotes").count(), 1
|
||||
)
|
||||
linked_category = ProductCategory.objects.get(
|
||||
pk=existing_category.pk
|
||||
)
|
||||
self.assertEqual(linked_category.external_id, "5")
|
||||
|
||||
created_product = Product.objects.get(name="Producto 1")
|
||||
self.assertEqual(
|
||||
list(created_product.categories.all()), [linked_category]
|
||||
)
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_import_product_without_categories(
|
||||
self, mock_connect, mock_call
|
||||
):
|
||||
mock_connect.return_value = None
|
||||
|
||||
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 [190]
|
||||
|
||||
product_read = "model.product.product.read"
|
||||
product_args = (
|
||||
[190],
|
||||
[
|
||||
"id",
|
||||
"name",
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
"template.categories",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (product_read, product_args):
|
||||
return [
|
||||
{
|
||||
"id": 190,
|
||||
"list_price": Decimal("25000"),
|
||||
"name": "Producto 1",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
"template.": {"id": 999, "categories": []},
|
||||
},
|
||||
]
|
||||
|
||||
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["failed_products"], [])
|
||||
self.assertEqual(content["created_categories"], [])
|
||||
self.assertEqual(content["updated_categories"], [])
|
||||
created_product = Product.objects.get(name="Producto 1")
|
||||
self.assertEqual(created_product.categories.count(), 0)
|
||||
self.assertEqual(ProductCategory.objects.count(), 0)
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_import_assigns_categories_to_untouched_products(
|
||||
self, mock_connect, mock_call
|
||||
):
|
||||
mock_connect.return_value = None
|
||||
|
||||
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": [9]},
|
||||
},
|
||||
]
|
||||
|
||||
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(
|
||||
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"], [{"id": 2, "name": "Papa"}]
|
||||
)
|
||||
|
||||
untouched_product = Product.objects.get(id=2)
|
||||
categories = untouched_product.categories.all()
|
||||
self.assertEqual(len(categories), 1)
|
||||
self.assertEqual(categories[0].name, "Tubérculos")
|
||||
self.assertEqual(categories[0].external_id, "9")
|
||||
self.assertEqual(content["updated_categories"], [])
|
||||
self.assertEqual(
|
||||
content["created_categories"],
|
||||
[
|
||||
{
|
||||
"id": categories[0].id,
|
||||
"name": "Tubérculos",
|
||||
"external_id": "9",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user