refactor: organize code by domain-driven design

Refactoriza la estructura del proyecto siguiendo principios de Domain-Driven Design,
organizando serializers, API views y servicios por dominios de negocio.

Cambios principales:

## Serializers (serializers/)
- Dividido serializers.py en módulos por dominio:
  * products.py: ProductSerializer, ListProductSerializer
  * customers.py: CustomerSerializer, ListCustomerSerializer
  * sales.py: SaleSerializer, SaleLineSerializer, CatalogSaleSerializer, etc.
  * payments.py: ReconciliationJarSerializer, PaymentMethodSerializer
  * __init__.py: Exporta todos los serializers para mantener compatibilidad

## API Views (api/)
- Dividido api_views.py en módulos por dominio:
  * products.py: ProductView, ProductsFromTrytonView
  * customers.py: CustomerView, CustomersFromTrytonView
  * sales.py: SaleView, CatalogSaleView, SaleSummary, SalesForTrytonView, SalesToTrytonView
  * payments.py: ReconciliateJarView, ReconciliateJarModelView, PaymentMethodView, SalesForReconciliationView
  * admin.py: AdminCodeValidateView
  * __init__.py: Exporta todas las vistas para facilitar importaciones

## Services Layer (services/tryton/)
- Nueva capa de servicios para lógica de negocio Tryton:
  * client.py: get_tryton_client(), TrytonSale, TrytonLineSale, configuración
  * products.py: ProductTrytonService - sincronización de productos
  * customers.py: CustomerTrytonService - sincronización de clientes
  * sales.py: SaleTrytonService - sincronización de ventas
  * __init__.py: Exporta servicios y utilidades

## Actualización de URLs
- Actualizado urls.py para importar desde nuevos módulos
- Mantiene todas las rutas existentes sin cambios

## Eliminación de archivos antiguos
- Eliminado serializers.py (refactorizado a serializers/)
- Eliminado api_views.py (refactorizado a api/)

## Beneficios
 Cohesión: Código organizado por dominio de negocio
 Separación de responsabilidades: API, Serializers y Services separados
 Mantenibilidad: Archivos más pequeños y enfocados
 Escalabilidad: Fácil agregar nuevos dominios
 Testabilidad: Mejor organización para pruebas por dominio
 Reutilización: Servicios Tryton pueden usarse desde cualquier vista

## Estructura final:
- models/ (ya existía organizado por dominio)
- serializers/ (nuevo, organizado por dominio)
- api/ (nuevo, organizado por dominio)
- services/tryton/ (nuevo, capa de servicios)

Tests: 46 tests pasando ✓
This commit is contained in:
2026-05-29 00:16:18 -05:00
parent f526330f9e
commit 47e87e4204
19 changed files with 824 additions and 629 deletions

View File

@@ -0,0 +1,40 @@
from .products import ProductView, ProductsFromTrytonView
from .customers import CustomerView, CustomersFromTrytonView
from .sales import (
SaleView,
CatalogSaleView,
SaleSummary,
SalesForTrytonView,
SalesToTrytonView,
)
from .payments import (
ReconciliateJarView,
ReconciliateJarModelView,
PaymentMethodView,
SalesForReconciliationView,
Pagination,
)
from .admin import AdminCodeValidateView
__all__ = [
# Products
"ProductView",
"ProductsFromTrytonView",
# Customers
"CustomerView",
"CustomersFromTrytonView",
# Sales
"SaleView",
"CatalogSaleView",
"SaleSummary",
"SalesForTrytonView",
"SalesToTrytonView",
# Payments
"ReconciliateJarView",
"ReconciliateJarModelView",
"PaymentMethodView",
"SalesForReconciliationView",
"Pagination",
# Admin
"AdminCodeValidateView",
]

View File

@@ -0,0 +1,14 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from ..models.admin import AdminCode
from ..permissions import IsAdministrator
class AdminCodeValidateView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request, code):
codes = AdminCode.objects.filter(value=code)
return Response({"validCode": bool(codes)})

View File

@@ -0,0 +1,25 @@
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from ..models.customers import Customer
from ..serializers import CustomerSerializer
from ..permissions import IsAdministrator
from ..services.tryton.customers import CustomerTrytonService
from ..services.tryton.client import get_tryton_client
class CustomerView(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
class CustomersFromTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
tryton_client = get_tryton_client()
service = CustomerTrytonService(tryton_client)
result = service.import_from_tryton()
return Response(result, status=200)

View File

@@ -0,0 +1,108 @@
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from decimal import Decimal
from ..models.sales import Sale
from ..models.payments import ReconciliationJar, PaymentMethods
from ..serializers import (
ReconciliationJarSerializer,
PaymentMethodSerializer,
SaleForRenconciliationSerializer,
)
from ..permissions import IsAdministrator
class Pagination(PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
class ReconciliateJarView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
data = request.data
cash_purchases_id = data.get("cash_purchases")
serializer = ReconciliationJarSerializer(data=data)
if serializer.is_valid():
cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id)
if not self._is_valid_total(
cash_purchases, data.get("total_cash_purchases")
):
return Response(
{
"error": "total_cash_purchases not equal to sum of all purchases."
},
status=HTTP_400_BAD_REQUEST,
)
reconciliation = serializer.save()
other_purchases = self._get_other_purchases(data.get("other_totals"))
self._link_purchases(reconciliation, cash_purchases, other_purchases)
return Response({"id": reconciliation.id})
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
def get(self, request):
reconciliations = ReconciliationJar.objects.all()
serializer = ReconciliationJarSerializer(reconciliations, many=True)
return Response(serializer.data)
def _is_valid_total(self, purchases, total):
calculated_total = sum(p.get_total() for p in purchases)
return Decimal(calculated_total).quantize(Decimal(".0001")) == (
Decimal(total).quantize(Decimal(".0001"))
)
def _get_other_purchases(self, other_totals):
if not other_totals:
return []
purchases = []
for method in other_totals:
purchases.extend(other_totals[method]["purchases"])
if purchases:
return Sale.objects.filter(pk__in=purchases)
return []
def _link_purchases(self, reconciliation, cash_purchases, other_purchases):
for purchase in cash_purchases:
purchase.reconciliation = reconciliation
purchase.clean()
purchase.save()
for purchase in other_purchases:
purchase.reconciliation = reconciliation
purchase.clean()
purchase.save()
class ReconciliateJarModelView(viewsets.ModelViewSet):
queryset = ReconciliationJar.objects.all().order_by("-date_time")
pagination_class = Pagination
serializer_class = ReconciliationJarSerializer
permission_classes = [IsAuthenticated, IsAdministrator]
class PaymentMethodView(APIView):
def get(self, request):
serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True)
return Response(serializer.data)
class SalesForReconciliationView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request):
sales = Sale.objects.filter(reconciliation=None)
grouped_sales = {}
for sale in sales:
if sale.payment_method not in grouped_sales.keys():
grouped_sales[sale.payment_method] = []
serializer = SaleForRenconciliationSerializer(sale)
grouped_sales[sale.payment_method].append(serializer.data)
return Response(grouped_sales)

View File

@@ -0,0 +1,53 @@
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from ..models.products import Product
from ..serializers import ProductSerializer
from ..permissions import IsAdministrator
from ..services.tryton.products import ProductTrytonService
from ..services.tryton.client import get_tryton_client
class ProductView(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self):
"""
Filters products by active status for list operations.
Detail operations (retrieve, update, destroy) return all products.
Query params for list:
- active=true (default): Only active products
- active=false: Only inactive products
- active=all: All products regardless of status
"""
queryset = Product.objects.all()
# Only filter for list action, not for detail operations
if self.action != "list":
return queryset
active_param = self.request.query_params.get("active", "true")
if active_param.lower() == "all":
return queryset
elif active_param.lower() in ["true", "1", "yes"]:
return queryset.filter(active=True)
elif active_param.lower() in ["false", "0", "no"]:
return queryset.filter(active=False)
else:
# Default behavior: return only active products
return queryset.filter(active=True)
class ProductsFromTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
tryton_client = get_tryton_client()
service = ProductTrytonService(tryton_client)
result = service.import_from_tryton()
return Response(result, status=200)

View File

@@ -0,0 +1,94 @@
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
import io
import csv
from ..models.sales import Sale, SaleLine, CatalogSale
from ..models.customers import Customer
from ..models.products import Product
from ..serializers import (
SaleSerializer,
CatalogSaleSerializer,
SaleSummarySerializer,
)
from ..permissions import IsAdministrator
from ..services.tryton.sales import SaleTrytonService
from ..services.tryton.client import get_tryton_client
from ..views import sales_to_tryton_csv
class SaleView(viewsets.ModelViewSet):
queryset = Sale.objects.all()
serializer_class = SaleSerializer
def create(self, request):
data = request.data
customer = Customer.objects.get(pk=data["customer"])
date = data["date"]
lines = data["saleline_set"]
payment_method = data["payment_method"]
description = data.get("notes", "")
sale = Sale.objects.create(
customer=customer,
date=date,
payment_method=payment_method,
description=description,
)
for line in lines:
product = Product.objects.get(pk=line["product"])
quantity = line["quantity"]
unit_price = line["unit_price"]
SaleLine.objects.create(
sale=sale,
product=product,
quantity=quantity,
unit_price=unit_price,
)
return Response(
{"id": sale.id, "message": "Venta creada con exito"},
status=201,
)
class CatalogSaleView(viewsets.ModelViewSet):
queryset = CatalogSale.objects.all()
serializer_class = CatalogSaleSerializer
class SaleSummary(APIView):
def get(self, request, id):
sale = Sale.objects.get(pk=id)
serializer = SaleSummarySerializer(sale)
return Response(serializer.data)
class SalesForTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request):
sales = Sale.objects.all()
csv_data = self._generate_sales_CSV(sales)
return Response({"csv": csv_data})
def _generate_sales_CSV(self, sales):
output = io.StringIO()
writer = csv.writer(output)
csv_data = sales_to_tryton_csv(sales)
for row in csv_data:
writer.writerow(row)
return output.getvalue()
class SalesToTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
tryton_client = get_tryton_client()
service = SaleTrytonService(tryton_client)
result = service.send_to_tryton()
return Response(result, status=200)