feat: detalle de fallos y resultados enriquecidos en sincronización Tryton
- failed_products/failed_parties ahora incluyen {external_id, name, error} con causa legible (precio nulo, nombre duplicado, etc.)
- Los fallos de actualización de productos y clientes ya no abortan la sincronización
- created/updated/untouched/checked como objetos {id, name}; updated incluye detail con los cambios (Nombre/Precio/Unidad/Dirección)
- Ventas y catálogo: successful {id, code} y failed {id, code, error} (antes solo IDs, sin detalle)
This commit is contained in:
@@ -62,11 +62,25 @@ class TestCustomersFromTryton(TestCase, LoginMixin):
|
||||
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
expected_response = {
|
||||
"checked_tryton_parties": [5, 6, 7, 8],
|
||||
"created_customers": [3, 4],
|
||||
"untouched_customers": [2],
|
||||
"checked_tryton_parties": [
|
||||
{"id": 5, "name": "Carlos"},
|
||||
{"id": 6, "name": "Cristian"},
|
||||
{"id": 7, "name": "Ana"},
|
||||
{"id": 8, "name": "José"},
|
||||
],
|
||||
"created_customers": [
|
||||
{"id": 3, "name": "Ana"},
|
||||
{"id": 4, "name": "José"},
|
||||
],
|
||||
"untouched_customers": [{"id": 2, "name": "Cristian"}],
|
||||
"failed_parties": [],
|
||||
"updated_customers": [1],
|
||||
"updated_customers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Carlos",
|
||||
"detail": "Nombre: Calos → Carlos, Dirección: None → 303",
|
||||
}
|
||||
],
|
||||
}
|
||||
self.assertEqual(content, expected_response)
|
||||
|
||||
@@ -79,3 +93,115 @@ class TestCustomersFromTryton(TestCase, LoginMixin):
|
||||
self.assertEqual(updated_customer.external_id, str(5))
|
||||
self.assertEqual(updated_customer.name, "Carlos")
|
||||
self.assertIn(updated_customer.address_external_id, str(303))
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_create_failure_customer(self, mock_connect, mock_call):
|
||||
def fake_call(*args, **kwargs):
|
||||
party_search = "model.party.party.search"
|
||||
search_args = [
|
||||
[],
|
||||
0,
|
||||
1000,
|
||||
[["name", "ASC"], ["id", None]],
|
||||
{"company": 1},
|
||||
]
|
||||
|
||||
if args == (party_search, search_args):
|
||||
return [5, 9]
|
||||
|
||||
party_read = "model.party.party.read"
|
||||
read_args = (
|
||||
[5, 9],
|
||||
["id", "name", "addresses"],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (party_read, read_args):
|
||||
return [
|
||||
{"id": 5, "name": "Carlos", "addresses": [303]},
|
||||
{"id": 9, "name": None, "addresses": []},
|
||||
]
|
||||
|
||||
raise Exception(
|
||||
f"Sorry, args non expected on this test: {args}"
|
||||
)
|
||||
|
||||
mock_call.side_effect = fake_call
|
||||
|
||||
url = "/don_confiao/api/importar_clientes_de_tryton"
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
self.assertEqual(len(content["failed_parties"]), 1)
|
||||
failure = content["failed_parties"][0]
|
||||
self.assertEqual(failure["external_id"], 9)
|
||||
self.assertEqual(failure["name"], None)
|
||||
self.assertEqual(
|
||||
failure["error"],
|
||||
"Error al crear cliente: El cliente no tiene nombre en Tryton",
|
||||
)
|
||||
self.assertEqual(
|
||||
content["updated_customers"],
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Carlos",
|
||||
"detail": "Nombre: Calos → Carlos, Dirección: None → 303",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_update_failure_customer(self, mock_connect, mock_call):
|
||||
def fake_call(*args, **kwargs):
|
||||
party_search = "model.party.party.search"
|
||||
search_args = [
|
||||
[],
|
||||
0,
|
||||
1000,
|
||||
[["name", "ASC"], ["id", None]],
|
||||
{"company": 1},
|
||||
]
|
||||
|
||||
if args == (party_search, search_args):
|
||||
return [5, 6]
|
||||
|
||||
party_read = "model.party.party.read"
|
||||
read_args = (
|
||||
[5, 6],
|
||||
["id", "name", "addresses"],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (party_read, read_args):
|
||||
return [
|
||||
{"id": 5, "name": "Carlos", "addresses": [303]},
|
||||
{"id": 6, "name": None, "addresses": []},
|
||||
]
|
||||
|
||||
raise Exception(
|
||||
f"Sorry, args non expected on this test: {args}"
|
||||
)
|
||||
|
||||
mock_call.side_effect = fake_call
|
||||
|
||||
url = "/don_confiao/api/importar_clientes_de_tryton"
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
self.assertEqual(len(content["failed_parties"]), 1)
|
||||
failure = content["failed_parties"][0]
|
||||
self.assertEqual(failure["external_id"], 6)
|
||||
self.assertIn("Error al actualizar cliente", failure["error"])
|
||||
self.assertEqual(
|
||||
content["updated_customers"],
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Carlos",
|
||||
"detail": "Nombre: Calos → Carlos, Dirección: None → 303",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
@@ -79,7 +79,13 @@ class TestSendSalesToTryton(TestCase, LoginMixin):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
self.assertEqual(
|
||||
content, {"successful": [self.sale.id], "failed": []}
|
||||
content,
|
||||
{
|
||||
"successful": [
|
||||
{"id": self.sale.id, "code": self.sale.code}
|
||||
],
|
||||
"failed": [],
|
||||
},
|
||||
)
|
||||
|
||||
updated_sale = Sale.objects.get(id=self.sale.id)
|
||||
@@ -165,7 +171,15 @@ class TestSendSalesToTryton(TestCase, LoginMixin):
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
self.assertEqual(
|
||||
content,
|
||||
{"successful": [self.catalog_sale.id], "failed": []},
|
||||
{
|
||||
"successful": [
|
||||
{
|
||||
"id": self.catalog_sale.id,
|
||||
"code": self.catalog_sale.code,
|
||||
}
|
||||
],
|
||||
"failed": [],
|
||||
},
|
||||
)
|
||||
|
||||
updated = CatalogSale.objects.get(id=self.catalog_sale.id)
|
||||
@@ -247,3 +261,51 @@ class TestSendSalesToTryton(TestCase, LoginMixin):
|
||||
lines[1]["unit_price"],
|
||||
{"__class__": "Decimal", "decimal": "5000.00"},
|
||||
)
|
||||
|
||||
@patch("don_confiao.api.sales.get_tryton_client")
|
||||
def test_send_sales_to_tryton_failure(self, mock_get_tryton_client):
|
||||
mock_client = MagicMock()
|
||||
mock_client.call.side_effect = Exception("error de tryton")
|
||||
mock_get_tryton_client.return_value = mock_client
|
||||
|
||||
url = "/don_confiao/api/enviar_ventas_a_tryton"
|
||||
response = self.client.post(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
self.assertEqual(content["successful"], [])
|
||||
self.assertEqual(len(content["failed"]), 1)
|
||||
failure = content["failed"][0]
|
||||
self.assertEqual(failure["id"], self.sale.id)
|
||||
self.assertEqual(failure["code"], self.sale.code)
|
||||
self.assertEqual(
|
||||
failure["error"], "Error al enviar la venta: error de tryton"
|
||||
)
|
||||
|
||||
sale = Sale.objects.get(id=self.sale.id)
|
||||
self.assertIsNone(sale.external_id)
|
||||
|
||||
@patch("don_confiao.api.sales.get_tryton_client")
|
||||
def test_send_catalog_sales_to_tryton_failure(
|
||||
self, mock_get_tryton_client
|
||||
):
|
||||
mock_client = MagicMock()
|
||||
mock_client.call.side_effect = Exception("error de tryton")
|
||||
mock_get_tryton_client.return_value = mock_client
|
||||
|
||||
url = "/don_confiao/api/enviar_catalog_sales_a_tryton"
|
||||
response = self.client.post(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
self.assertEqual(content["successful"], [])
|
||||
self.assertEqual(len(content["failed"]), 1)
|
||||
failure = content["failed"][0]
|
||||
self.assertEqual(failure["id"], self.catalog_sale.id)
|
||||
self.assertEqual(failure["code"], self.catalog_sale.code)
|
||||
self.assertEqual(
|
||||
failure["error"], "Error al enviar la venta: error de tryton"
|
||||
)
|
||||
|
||||
catalog_sale = CatalogSale.objects.get(id=self.catalog_sale.id)
|
||||
self.assertIsNone(catalog_sale.external_id)
|
||||
|
||||
@@ -92,11 +92,21 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
expected_response = {
|
||||
"checked_tryton_products": [190, 191, 192],
|
||||
"created_products": [3],
|
||||
"untouched_products": [2],
|
||||
"checked_tryton_products": [
|
||||
{"id": 190, "name": "Producto 1"},
|
||||
{"id": 191, "name": "Panela2"},
|
||||
{"id": 192, "name": "Papa"},
|
||||
],
|
||||
"created_products": [{"id": 3, "name": "Producto 1"}],
|
||||
"untouched_products": [{"id": 2, "name": "Papa"}],
|
||||
"failed_products": [],
|
||||
"updated_products": [1],
|
||||
"updated_products": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Panela2",
|
||||
"detail": "Nombre: Panela → Panela2, Precio: 5000 → 6000, Unidad: UNIT → Unit",
|
||||
}
|
||||
],
|
||||
}
|
||||
self.assertEqual(content, expected_response)
|
||||
|
||||
@@ -165,10 +175,139 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
||||
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
expected_response = {
|
||||
"checked_tryton_products": [200],
|
||||
"checked_tryton_products": [{"id": 200, "name": "Panela"}],
|
||||
"created_products": [],
|
||||
"untouched_products": [],
|
||||
"failed_products": [200],
|
||||
"failed_products": [
|
||||
{
|
||||
"external_id": 200,
|
||||
"name": "Panela",
|
||||
"error": "Error al crear producto: Ya existe un producto con el nombre 'Panela'",
|
||||
}
|
||||
],
|
||||
"updated_products": [],
|
||||
}
|
||||
self.assertEqual(content, expected_response)
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_import_null_price_product(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 [201]
|
||||
|
||||
product_read = "model.product.product.read"
|
||||
product_args = (
|
||||
[201],
|
||||
[
|
||||
"id",
|
||||
"name",
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (product_read, product_args):
|
||||
return [
|
||||
{
|
||||
"id": 201,
|
||||
"list_price": None,
|
||||
"name": "ENVASES Y EMPAQUES",
|
||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||
},
|
||||
]
|
||||
|
||||
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_products"], [])
|
||||
self.assertEqual(len(content["failed_products"]), 1)
|
||||
failure = content["failed_products"][0]
|
||||
self.assertEqual(failure["external_id"], 201)
|
||||
self.assertEqual(failure["name"], "ENVASES Y EMPAQUES")
|
||||
self.assertEqual(
|
||||
failure["error"],
|
||||
"Error al crear producto: El producto no tiene precio en Tryton (list_price nulo)",
|
||||
)
|
||||
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||
def test_update_failure_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 [191, 192]
|
||||
|
||||
product_read = "model.product.product.read"
|
||||
product_args = (
|
||||
[191, 192],
|
||||
[
|
||||
"id",
|
||||
"name",
|
||||
"default_uom.id",
|
||||
"default_uom.rec_name",
|
||||
"list_price",
|
||||
],
|
||||
{"company": 1},
|
||||
)
|
||||
if args == (product_read, product_args):
|
||||
return [
|
||||
{
|
||||
"id": 191,
|
||||
"list_price": Decimal("6000"),
|
||||
"name": "Panela",
|
||||
"default_uom.": None,
|
||||
},
|
||||
{
|
||||
"id": 192,
|
||||
"list_price": Decimal("4500"),
|
||||
"name": "Papa",
|
||||
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
||||
},
|
||||
]
|
||||
|
||||
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(len(content["failed_products"]), 1)
|
||||
failure = content["failed_products"][0]
|
||||
self.assertEqual(failure["external_id"], 191)
|
||||
self.assertEqual(failure["name"], "Panela")
|
||||
self.assertIn("Error al actualizar producto", failure["error"])
|
||||
self.assertEqual(content["untouched_products"], [{"id": 2, "name": "Papa"}])
|
||||
|
||||
Reference in New Issue
Block a user