From 5e811c802a0526d3b07290960cee693c074842e6 Mon Sep 17 00:00:00 2001 From: aserrador Date: Sat, 30 May 2026 21:01:29 -0500 Subject: [PATCH] Add Tryton synchronization for CatalogSale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- tienda_ilusion/don_confiao/api/__init__.py | 2 + tienda_ilusion/don_confiao/api/sales.py | 10 ++++ .../0047_catalogsale_external_id.py | 18 ++++++ tienda_ilusion/don_confiao/models/sales.py | 10 +--- .../don_confiao/services/tryton/client.py | 59 ++++++++++++++++++- .../don_confiao/services/tryton/sales.py | 40 ++++++++++++- tienda_ilusion/don_confiao/tests/test_api.py | 18 ++++++ tienda_ilusion/don_confiao/urls.py | 6 ++ 8 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 tienda_ilusion/don_confiao/migrations/0047_catalogsale_external_id.py diff --git a/tienda_ilusion/don_confiao/api/__init__.py b/tienda_ilusion/don_confiao/api/__init__.py index dc71190..d6f5226 100644 --- a/tienda_ilusion/don_confiao/api/__init__.py +++ b/tienda_ilusion/don_confiao/api/__init__.py @@ -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", diff --git a/tienda_ilusion/don_confiao/api/sales.py b/tienda_ilusion/don_confiao/api/sales.py index fc0400e..ef4cde7 100644 --- a/tienda_ilusion/don_confiao/api/sales.py +++ b/tienda_ilusion/don_confiao/api/sales.py @@ -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) diff --git a/tienda_ilusion/don_confiao/migrations/0047_catalogsale_external_id.py b/tienda_ilusion/don_confiao/migrations/0047_catalogsale_external_id.py new file mode 100644 index 0000000..97ad0dd --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0047_catalogsale_external_id.py @@ -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), + ), + ] diff --git a/tienda_ilusion/don_confiao/models/sales.py b/tienda_ilusion/don_confiao/models/sales.py index 17061b5..4adabb1 100644 --- a/tienda_ilusion/don_confiao/models/sales.py +++ b/tienda_ilusion/don_confiao/models/sales.py @@ -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}" diff --git a/tienda_ilusion/don_confiao/services/tryton/client.py b/tienda_ilusion/don_confiao/services/tryton/client.py index 6d6d027..919734f 100644 --- a/tienda_ilusion/don_confiao/services/tryton/client.py +++ b/tienda_ilusion/don_confiao/services/tryton/client.py @@ -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), + } diff --git a/tienda_ilusion/don_confiao/services/tryton/sales.py b/tienda_ilusion/don_confiao/services/tryton/sales.py index 1e1f35a..4a71dd9 100644 --- a/tienda_ilusion/don_confiao/services/tryton/sales.py +++ b/tienda_ilusion/don_confiao/services/tryton/sales.py @@ -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] diff --git a/tienda_ilusion/don_confiao/tests/test_api.py b/tienda_ilusion/don_confiao/tests/test_api.py index 1aba605..4c16852 100644 --- a/tienda_ilusion/don_confiao/tests/test_api.py +++ b/tienda_ilusion/don_confiao/tests/test_api.py @@ -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() diff --git a/tienda_ilusion/don_confiao/urls.py b/tienda_ilusion/don_confiao/urls.py index 96e831d..63e408a 100644 --- a/tienda_ilusion/don_confiao/urls.py +++ b/tienda_ilusion/don_confiao/urls.py @@ -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/", AdminCodeValidateView.as_view(),