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,
|
CatalogSaleSummary,
|
||||||
SalesForTrytonView,
|
SalesForTrytonView,
|
||||||
SalesToTrytonView,
|
SalesToTrytonView,
|
||||||
|
CatalogSalesToTrytonView,
|
||||||
)
|
)
|
||||||
from .payments import (
|
from .payments import (
|
||||||
ReconciliateJarView,
|
ReconciliateJarView,
|
||||||
@@ -31,6 +32,7 @@ __all__ = [
|
|||||||
"CatalogSaleSummary",
|
"CatalogSaleSummary",
|
||||||
"SalesForTrytonView",
|
"SalesForTrytonView",
|
||||||
"SalesToTrytonView",
|
"SalesToTrytonView",
|
||||||
|
"CatalogSalesToTrytonView",
|
||||||
# Payments
|
# Payments
|
||||||
"ReconciliateJarView",
|
"ReconciliateJarView",
|
||||||
"ReconciliateJarModelView",
|
"ReconciliateJarModelView",
|
||||||
|
|||||||
@@ -100,3 +100,13 @@ class SalesToTrytonView(APIView):
|
|||||||
service = SaleTrytonService(tryton_client)
|
service = SaleTrytonService(tryton_client)
|
||||||
result = service.send_to_tryton()
|
result = service.send_to_tryton()
|
||||||
return Response(result, status=200)
|
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 = models.ForeignKey(
|
||||||
Product, null=False, blank=False, on_delete=models.CASCADE
|
Product, null=False, blank=False, on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
quantity = models.DecimalField(
|
quantity = models.DecimalField(max_digits=10, decimal_places=2, null=True)
|
||||||
max_digits=10, decimal_places=2, null=True
|
|
||||||
)
|
|
||||||
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
|
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
|
||||||
description = models.CharField(max_length=255, null=True, blank=True)
|
description = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
@@ -55,9 +53,7 @@ class Sale(SaleAbstractModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.payment_method not in PaymentMethods.values:
|
if self.payment_method not in PaymentMethods.values:
|
||||||
raise ValidationError(
|
raise ValidationError({"payment_method": "Invalid payment method"})
|
||||||
{"payment_method": "Invalid payment method"}
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sale_header_csv(cls):
|
def sale_header_csv(cls):
|
||||||
@@ -67,7 +63,6 @@ class Sale(SaleAbstractModel):
|
|||||||
|
|
||||||
|
|
||||||
class SaleLine(SaleLineAbstractModel):
|
class SaleLine(SaleLineAbstractModel):
|
||||||
|
|
||||||
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
|
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -75,6 +70,7 @@ class SaleLine(SaleLineAbstractModel):
|
|||||||
|
|
||||||
|
|
||||||
class CatalogSale(SaleAbstractModel):
|
class CatalogSale(SaleAbstractModel):
|
||||||
|
external_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.date} {self.customer}"
|
return f"{self.date} {self.customer}"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def get_tryton_client():
|
|||||||
|
|
||||||
class TrytonSale:
|
class TrytonSale:
|
||||||
"""Representa una venta para exportación a Tryton"""
|
"""Representa una venta para exportación a Tryton"""
|
||||||
|
|
||||||
def __init__(self, sale, lines):
|
def __init__(self, sale, lines):
|
||||||
self.sale = sale
|
self.sale = sale
|
||||||
self.lines = lines
|
self.lines = lines
|
||||||
@@ -60,7 +60,7 @@ class TrytonSale:
|
|||||||
|
|
||||||
class TrytonLineSale:
|
class TrytonLineSale:
|
||||||
"""Representa una línea de venta para exportación a Tryton"""
|
"""Representa una línea de venta para exportación a Tryton"""
|
||||||
|
|
||||||
def __init__(self, sale_line):
|
def __init__(self, sale_line):
|
||||||
self.sale_line = sale_line
|
self.sale_line = sale_line
|
||||||
|
|
||||||
@@ -75,3 +75,58 @@ class TrytonLineSale:
|
|||||||
"unit": self.sale_line.product.unit_external_id,
|
"unit": self.sale_line.product.unit_external_id,
|
||||||
"unit_price": self._format_decimal(self.sale_line.unit_price),
|
"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 ...models.sales import Sale, SaleLine, CatalogSale, CatalogSaleLine
|
||||||
from .client import TrytonSale, TRYTON_COMPANY_ID, TRYTON_SHOPS
|
from .client import TrytonSale, TrytonCatalogSale, TRYTON_COMPANY_ID, TRYTON_SHOPS
|
||||||
|
|
||||||
|
|
||||||
class SaleTrytonService:
|
class SaleTrytonService:
|
||||||
@@ -39,3 +39,39 @@ class SaleTrytonService:
|
|||||||
"""Convierte venta a parámetros para Tryton"""
|
"""Convierte venta a parámetros para Tryton"""
|
||||||
sale_tryton = TrytonSale(sale, lines)
|
sale_tryton = TrytonSale(sale, lines)
|
||||||
return [[sale_tryton.to_tryton()], tryton_context]
|
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["quantity"], "3.00")
|
||||||
self.assertEqual(line2["unit_price"], "5000.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):
|
def test_csv_structure_in_sales_for_tryton(self):
|
||||||
url = "/don_confiao/api/sales/for_tryton"
|
url = "/don_confiao/api/sales/for_tryton"
|
||||||
self._create_sale()
|
self._create_sale()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .api import (
|
|||||||
CatalogSaleSummary,
|
CatalogSaleSummary,
|
||||||
SalesForTrytonView,
|
SalesForTrytonView,
|
||||||
SalesToTrytonView,
|
SalesToTrytonView,
|
||||||
|
CatalogSalesToTrytonView,
|
||||||
# Payments
|
# Payments
|
||||||
ReconciliateJarView,
|
ReconciliateJarView,
|
||||||
ReconciliateJarModelView,
|
ReconciliateJarModelView,
|
||||||
@@ -77,6 +78,11 @@ urlpatterns = [
|
|||||||
SalesToTrytonView.as_view(),
|
SalesToTrytonView.as_view(),
|
||||||
name="send_tryton",
|
name="send_tryton",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"api/enviar_catalog_sales_a_tryton",
|
||||||
|
CatalogSalesToTrytonView.as_view(),
|
||||||
|
name="send_catalog_sales_tryton",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"api/admin_code/validate/<code>",
|
"api/admin_code/validate/<code>",
|
||||||
AdminCodeValidateView.as_view(),
|
AdminCodeValidateView.as_view(),
|
||||||
|
|||||||
Reference in New Issue
Block a user