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:
@@ -15,7 +15,10 @@ class CustomerTrytonService:
|
||||
party_ids = self.client.call(method, params)
|
||||
tryton_parties = self._get_party_details(party_ids, context)
|
||||
|
||||
checked_tryton_parties = party_ids
|
||||
checked_tryton_parties = [
|
||||
{"id": p.get("id"), "name": p.get("name")}
|
||||
for p in tryton_parties
|
||||
]
|
||||
failed_parties = []
|
||||
updated_customers = []
|
||||
created_customers = []
|
||||
@@ -25,15 +28,35 @@ class CustomerTrytonService:
|
||||
try:
|
||||
customer = Customer.objects.get(external_id=tryton_party.get("id"))
|
||||
except Customer.DoesNotExist:
|
||||
customer = self._create_customer(tryton_party)
|
||||
created_customers.append(customer.id)
|
||||
continue
|
||||
|
||||
if self._need_update(customer, tryton_party):
|
||||
self._update_customer(customer, tryton_party)
|
||||
updated_customers.append(customer.id)
|
||||
else:
|
||||
untouched_customers.append(customer.id)
|
||||
try:
|
||||
customer = self._create_customer(tryton_party)
|
||||
created_customers.append(self._build_item(customer))
|
||||
continue
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error al importar clientes: {e}El cliente: {tryton_party}"
|
||||
)
|
||||
failed_parties.append(
|
||||
self._build_failure(tryton_party, "crear", e)
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
if self._need_update(customer, tryton_party):
|
||||
changes = self._describe_changes(customer, tryton_party)
|
||||
self._update_customer(customer, tryton_party)
|
||||
updated_customers.append(
|
||||
{**self._build_item(customer), "detail": changes}
|
||||
)
|
||||
else:
|
||||
untouched_customers.append(self._build_item(customer))
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error al importar clientes: {e}El cliente: {tryton_party}"
|
||||
)
|
||||
failed_parties.append(
|
||||
self._build_failure(tryton_party, "actualizar", e)
|
||||
)
|
||||
|
||||
return {
|
||||
"checked_tryton_parties": checked_tryton_parties,
|
||||
@@ -43,6 +66,41 @@ class CustomerTrytonService:
|
||||
"untouched_customers": untouched_customers,
|
||||
}
|
||||
|
||||
def _build_item(self, customer):
|
||||
"""Construye el objeto de un cliente sincronizado"""
|
||||
return {"id": customer.id, "name": customer.name}
|
||||
|
||||
def _describe_changes(self, customer, tryton_party):
|
||||
"""Describe los cambios detectados entre el cliente local y Tryton"""
|
||||
changes = []
|
||||
name = tryton_party.get("name")
|
||||
if customer.name != name:
|
||||
changes.append(f"Nombre: {customer.name} → {name}")
|
||||
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
||||
new_address = str(tryton_party.get("addresses")[0])
|
||||
if customer.address_external_id != new_address:
|
||||
changes.append(
|
||||
f"Dirección: {customer.address_external_id} → {new_address}"
|
||||
)
|
||||
return ", ".join(changes)
|
||||
|
||||
def _build_failure(self, tryton_party, operation, exc):
|
||||
"""Construye el detalle de un cliente que falló al sincronizar"""
|
||||
error = self._friendly_error(tryton_party, str(exc))
|
||||
return {
|
||||
"external_id": tryton_party.get("id"),
|
||||
"name": tryton_party.get("name"),
|
||||
"error": f"Error al {operation} cliente: {error}",
|
||||
}
|
||||
|
||||
def _friendly_error(self, tryton_party, message):
|
||||
"""Traduce causas conocidas de error a mensajes legibles"""
|
||||
if "name" in message and (
|
||||
"not-null constraint" in message or "NOT NULL constraint" in message
|
||||
):
|
||||
return "El cliente no tiene nombre en Tryton"
|
||||
return message
|
||||
|
||||
def _get_party_details(self, party_ids, context):
|
||||
"""Obtiene detalles de clientes desde Tryton"""
|
||||
tryton_fields = ["id", "name", "addresses"]
|
||||
|
||||
@@ -21,7 +21,10 @@ class ProductTrytonService:
|
||||
product_ids = self.client.call(method, params)
|
||||
tryton_products = self._get_product_details(product_ids, context)
|
||||
|
||||
checked_tryton_products = product_ids
|
||||
checked_tryton_products = [
|
||||
{"id": p.get("id"), "name": p.get("name")}
|
||||
for p in tryton_products
|
||||
]
|
||||
failed_products = []
|
||||
updated_products = []
|
||||
created_products = []
|
||||
@@ -33,20 +36,33 @@ class ProductTrytonService:
|
||||
except Product.DoesNotExist:
|
||||
try:
|
||||
product = self._create_product(tryton_product)
|
||||
created_products.append(product.id)
|
||||
created_products.append(self._build_item(product))
|
||||
continue
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error al importar productos: {e}El producto: {tryton_product}"
|
||||
)
|
||||
failed_products.append(tryton_product.get("id"))
|
||||
failed_products.append(
|
||||
self._build_failure(tryton_product, "crear", e)
|
||||
)
|
||||
continue
|
||||
|
||||
if self._need_update(product, tryton_product):
|
||||
self._update_product(product, tryton_product)
|
||||
updated_products.append(product.id)
|
||||
else:
|
||||
untouched_products.append(product.id)
|
||||
try:
|
||||
if self._need_update(product, tryton_product):
|
||||
changes = self._describe_changes(product, tryton_product)
|
||||
self._update_product(product, tryton_product)
|
||||
updated_products.append(
|
||||
{**self._build_item(product), "detail": changes}
|
||||
)
|
||||
else:
|
||||
untouched_products.append(self._build_item(product))
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error al importar productos: {e}El producto: {tryton_product}"
|
||||
)
|
||||
failed_products.append(
|
||||
self._build_failure(tryton_product, "actualizar", e)
|
||||
)
|
||||
|
||||
return {
|
||||
"checked_tryton_products": checked_tryton_products,
|
||||
@@ -56,6 +72,59 @@ class ProductTrytonService:
|
||||
"untouched_products": untouched_products,
|
||||
}
|
||||
|
||||
def _build_item(self, product):
|
||||
"""Construye el objeto de un producto sincronizado"""
|
||||
return {"id": product.id, "name": product.name}
|
||||
|
||||
def _describe_changes(self, product, tryton_product):
|
||||
"""Describe los cambios detectados entre el producto local y Tryton"""
|
||||
changes = []
|
||||
name = tryton_product.get("name")
|
||||
if product.name != name:
|
||||
changes.append(f"Nombre: {product.name} → {name}")
|
||||
price = tryton_product.get("list_price")
|
||||
if product.price != price:
|
||||
changes.append(
|
||||
f"Precio: {self._format_price(product.price)} → "
|
||||
f"{self._format_price(price)}"
|
||||
)
|
||||
unit = tryton_product.get("default_uom.")
|
||||
if unit:
|
||||
unit_name = unit.get("rec_name")
|
||||
if product.measuring_unit != unit_name:
|
||||
changes.append(
|
||||
f"Unidad: {product.measuring_unit} → {unit_name}"
|
||||
)
|
||||
return ", ".join(changes)
|
||||
|
||||
@staticmethod
|
||||
def _format_price(value):
|
||||
"""Formatea un precio sin ceros decimales innecesarios"""
|
||||
return f"{value:.2f}".rstrip("0").rstrip(".")
|
||||
|
||||
def _build_failure(self, tryton_product, operation, exc):
|
||||
"""Construye el detalle de un producto que falló al sincronizar"""
|
||||
error = self._friendly_error(tryton_product, str(exc))
|
||||
return {
|
||||
"external_id": tryton_product.get("id"),
|
||||
"name": tryton_product.get("name"),
|
||||
"error": f"Error al {operation} producto: {error}",
|
||||
}
|
||||
|
||||
def _friendly_error(self, tryton_product, message):
|
||||
"""Traduce causas conocidas de error a mensajes legibles"""
|
||||
if "price" in message and (
|
||||
"not-null constraint" in message or "NOT NULL constraint" in message
|
||||
):
|
||||
return "El producto no tiene precio en Tryton (list_price nulo)"
|
||||
if "name" in message and (
|
||||
"duplicate key" in message or "UNIQUE constraint" in message
|
||||
):
|
||||
return (
|
||||
f"Ya existe un producto con el nombre '{tryton_product.get('name')}'"
|
||||
)
|
||||
return message
|
||||
|
||||
def _get_product_details(self, product_ids, context):
|
||||
"""Obtiene detalles de productos desde Tryton"""
|
||||
tryton_fields = [
|
||||
|
||||
@@ -27,10 +27,10 @@ class SaleTrytonService:
|
||||
external_ids = self.client.call(method, tryton_params)
|
||||
sale.external_id = external_ids[0]
|
||||
sale.save()
|
||||
successful.append(sale.id)
|
||||
successful.append(self._build_item(sale))
|
||||
except Exception as e:
|
||||
print(f"Error al enviar la venta: {e}venta_id: {sale.id}")
|
||||
failed.append(sale.id)
|
||||
failed.append(self._build_failure(sale, e))
|
||||
continue
|
||||
|
||||
return {"successful": successful, "failed": failed}
|
||||
@@ -61,16 +61,28 @@ class SaleTrytonService:
|
||||
external_ids = self.client.call(method, tryton_params)
|
||||
catalog_sale.external_id = external_ids[0]
|
||||
catalog_sale.save()
|
||||
successful.append(catalog_sale.id)
|
||||
successful.append(self._build_item(catalog_sale))
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error al enviar catalog sale: {e}, catalog_sale_id: {catalog_sale.id}"
|
||||
)
|
||||
failed.append(catalog_sale.id)
|
||||
failed.append(self._build_failure(catalog_sale, e))
|
||||
continue
|
||||
|
||||
return {"successful": successful, "failed": failed}
|
||||
|
||||
def _build_item(self, sale):
|
||||
"""Construye el objeto de una venta sincronizada"""
|
||||
return {"id": sale.id, "code": sale.code}
|
||||
|
||||
def _build_failure(self, sale, exc):
|
||||
"""Construye el detalle de una venta que falló al sincronizar"""
|
||||
return {
|
||||
"id": sale.id,
|
||||
"code": sale.code,
|
||||
"error": f"Error al enviar la venta: {exc}",
|
||||
}
|
||||
|
||||
def _catalog_sale_to_tryton_params(self, catalog_sale, lines, tryton_context):
|
||||
"""Convierte catalog sale a parámetros para Tryton"""
|
||||
sale_tryton = TrytonCatalogSale(catalog_sale, lines)
|
||||
|
||||
Reference in New Issue
Block a user