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:
40
tienda_ilusion/don_confiao/api/__init__.py
Normal file
40
tienda_ilusion/don_confiao/api/__init__.py
Normal 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",
|
||||
]
|
||||
14
tienda_ilusion/don_confiao/api/admin.py
Normal file
14
tienda_ilusion/don_confiao/api/admin.py
Normal 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)})
|
||||
25
tienda_ilusion/don_confiao/api/customers.py
Normal file
25
tienda_ilusion/don_confiao/api/customers.py
Normal 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)
|
||||
108
tienda_ilusion/don_confiao/api/payments.py
Normal file
108
tienda_ilusion/don_confiao/api/payments.py
Normal 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)
|
||||
53
tienda_ilusion/don_confiao/api/products.py
Normal file
53
tienda_ilusion/don_confiao/api/products.py
Normal 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)
|
||||
94
tienda_ilusion/don_confiao/api/sales.py
Normal file
94
tienda_ilusion/don_confiao/api/sales.py
Normal 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)
|
||||
Reference in New Issue
Block a user