Add Tryton synchronization for CatalogSale

- Add external_id field to CatalogSale model for tracking synced sales
- Create migration 0047 for external_id field
- Add TrytonCatalogSale and TrytonCatalogSaleLine classes for Tryton RPC format
- Add send_catalog_sales_to_tryton() method to SaleTrytonService
- Create CatalogSalesToTrytonView API endpoint (POST)
- Register endpoint at /don_confiao/api/enviar_catalog_sales_a_tryton
- Add test for external_id field functionality
- Catalog sales sync to same Tryton model as Sale (model.sale.sale.create)
- Differentiated by reference 'don_confiao_catalog X' and description 'Venta de catálogo'
- Filters only catalog sales without external_id to avoid duplicates
This commit is contained in:
2026-05-30 21:01:29 -05:00
parent d4a61b8340
commit 5e811c802a
8 changed files with 152 additions and 11 deletions

View File

@@ -7,6 +7,7 @@ from .sales import (
CatalogSaleSummary,
SalesForTrytonView,
SalesToTrytonView,
CatalogSalesToTrytonView,
)
from .payments import (
ReconciliateJarView,
@@ -31,6 +32,7 @@ __all__ = [
"CatalogSaleSummary",
"SalesForTrytonView",
"SalesToTrytonView",
"CatalogSalesToTrytonView",
# Payments
"ReconciliateJarView",
"ReconciliateJarModelView",

View File

@@ -100,3 +100,13 @@ class SalesToTrytonView(APIView):
service = SaleTrytonService(tryton_client)
result = service.send_to_tryton()
return Response(result, status=200)
class CatalogSalesToTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
tryton_client = get_tryton_client()
service = SaleTrytonService(tryton_client)
result = service.send_catalog_sales_to_tryton()
return Response(result, status=200)

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2026-05-31 01:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0046_product_active'),
]
operations = [
migrations.AddField(
model_name='catalogsale',
name='external_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -20,9 +20,7 @@ class SaleLineAbstractModel(models.Model):
product = models.ForeignKey(
Product, null=False, blank=False, on_delete=models.CASCADE
)
quantity = models.DecimalField(
max_digits=10, decimal_places=2, null=True
)
quantity = models.DecimalField(max_digits=10, decimal_places=2, null=True)
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
description = models.CharField(max_length=255, null=True, blank=True)
@@ -55,9 +53,7 @@ class Sale(SaleAbstractModel):
def clean(self):
if self.payment_method not in PaymentMethods.values:
raise ValidationError(
{"payment_method": "Invalid payment method"}
)
raise ValidationError({"payment_method": "Invalid payment method"})
@classmethod
def sale_header_csv(cls):
@@ -67,7 +63,6 @@ class Sale(SaleAbstractModel):
class SaleLine(SaleLineAbstractModel):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
def __str__(self):
@@ -75,6 +70,7 @@ class SaleLine(SaleLineAbstractModel):
class CatalogSale(SaleAbstractModel):
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return f"{self.date} {self.customer}"

View File

@@ -24,7 +24,7 @@ def get_tryton_client():
class TrytonSale:
"""Representa una venta para exportación a Tryton"""
def __init__(self, sale, lines):
self.sale = sale
self.lines = lines
@@ -60,7 +60,7 @@ class TrytonSale:
class TrytonLineSale:
"""Representa una línea de venta para exportación a Tryton"""
def __init__(self, sale_line):
self.sale_line = sale_line
@@ -75,3 +75,58 @@ class TrytonLineSale:
"unit": self.sale_line.product.unit_external_id,
"unit_price": self._format_decimal(self.sale_line.unit_price),
}
class TrytonCatalogSale:
"""Representa una catalog sale para exportación a Tryton"""
def __init__(self, catalog_sale, lines):
self.catalog_sale = catalog_sale
self.lines = lines
def _format_date(self, _date):
return {
"__class__": "date",
"year": _date.year,
"month": _date.month,
"day": _date.day,
}
def to_tryton(self):
return {
"company": TRYTON_COMPANY_ID,
"shipment_address": self.catalog_sale.customer.address_external_id,
"invoice_address": self.catalog_sale.customer.address_external_id,
"currency": TRYTON_COP_CURRENCY,
"comment": self.catalog_sale.description or "",
"description": "Venta de catálogo",
"party": self.catalog_sale.customer.external_id,
"reference": "don_confiao_catalog " + str(self.catalog_sale.id),
"sale_date": self._format_date(self.catalog_sale.date),
"lines": [
[
"create",
[TrytonCatalogSaleLine(line).to_tryton() for line in self.lines],
]
],
"self_pick_up": True,
}
class TrytonCatalogSaleLine:
"""Representa una línea de catalog sale para exportación a Tryton"""
def __init__(self, catalog_sale_line):
self.catalog_sale_line = catalog_sale_line
def _format_decimal(self, number):
return {"__class__": "Decimal", "decimal": str(number)}
def to_tryton(self):
return {
"product": self.catalog_sale_line.product.external_id,
"quantity": self._format_decimal(self.catalog_sale_line.quantity),
"type": "line",
"unit": self.catalog_sale_line.product.unit_external_id,
"unit_price": self._format_decimal(self.catalog_sale_line.unit_price),
}

View File

@@ -1,5 +1,5 @@
from ...models.sales import Sale, SaleLine
from .client import TrytonSale, TRYTON_COMPANY_ID, TRYTON_SHOPS
from ...models.sales import Sale, SaleLine, CatalogSale, CatalogSaleLine
from .client import TrytonSale, TrytonCatalogSale, TRYTON_COMPANY_ID, TRYTON_SHOPS
class SaleTrytonService:
@@ -39,3 +39,39 @@ class SaleTrytonService:
"""Convierte venta a parámetros para Tryton"""
sale_tryton = TrytonSale(sale, lines)
return [[sale_tryton.to_tryton()], tryton_context]
def send_catalog_sales_to_tryton(self):
"""Envía catalog sales sin external_id a Tryton"""
method = "model.sale.sale.create"
tryton_context = {
"company": TRYTON_COMPANY_ID,
"shops": TRYTON_SHOPS,
}
successful = []
failed = []
catalog_sales = CatalogSale.objects.filter(external_id=None)
for catalog_sale in catalog_sales:
try:
lines = CatalogSaleLine.objects.filter(catalog_sale=catalog_sale.id)
tryton_params = self._catalog_sale_to_tryton_params(
catalog_sale, lines, tryton_context
)
external_ids = self.client.call(method, tryton_params)
catalog_sale.external_id = external_ids[0]
catalog_sale.save()
successful.append(catalog_sale.id)
except Exception as e:
print(
f"Error al enviar catalog sale: {e}, catalog_sale_id: {catalog_sale.id}"
)
failed.append(catalog_sale.id)
continue
return {"successful": successful, "failed": failed}
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)
return [[sale_tryton.to_tryton()], tryton_context]

View File

@@ -129,6 +129,24 @@ class TestAPI(APITestCase, LoginMixin):
self.assertEqual(line2["quantity"], "3.00")
self.assertEqual(line2["unit_price"], "5000.00")
def test_catalog_sale_has_external_id_field(self):
"""Verifica que CatalogSale tiene el campo external_id"""
response = self._create_catalog_sale()
content = json.loads(response.content.decode("utf-8"))
catalog_sale_id = content["id"]
catalog_sale = CatalogSale.objects.get(pk=catalog_sale_id)
# Debe tener el campo external_id
self.assertIsNone(catalog_sale.external_id)
# Se puede asignar un valor
catalog_sale.external_id = "123"
catalog_sale.save()
# Verificar que se guardó
catalog_sale.refresh_from_db()
self.assertEqual(catalog_sale.external_id, "123")
def test_csv_structure_in_sales_for_tryton(self):
url = "/don_confiao/api/sales/for_tryton"
self._create_sale()

View File

@@ -16,6 +16,7 @@ from .api import (
CatalogSaleSummary,
SalesForTrytonView,
SalesToTrytonView,
CatalogSalesToTrytonView,
# Payments
ReconciliateJarView,
ReconciliateJarModelView,
@@ -77,6 +78,11 @@ urlpatterns = [
SalesToTrytonView.as_view(),
name="send_tryton",
),
path(
"api/enviar_catalog_sales_a_tryton",
CatalogSalesToTrytonView.as_view(),
name="send_catalog_sales_tryton",
),
path(
"api/admin_code/validate/<code>",
AdminCodeValidateView.as_view(),