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:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user