diff --git a/tienda_ilusion/don_confiao/api/__init__.py b/tienda_ilusion/don_confiao/api/__init__.py new file mode 100644 index 0000000..18c0308 --- /dev/null +++ b/tienda_ilusion/don_confiao/api/__init__.py @@ -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", +] diff --git a/tienda_ilusion/don_confiao/api/admin.py b/tienda_ilusion/don_confiao/api/admin.py new file mode 100644 index 0000000..7397376 --- /dev/null +++ b/tienda_ilusion/don_confiao/api/admin.py @@ -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)}) diff --git a/tienda_ilusion/don_confiao/api/customers.py b/tienda_ilusion/don_confiao/api/customers.py new file mode 100644 index 0000000..01b136c --- /dev/null +++ b/tienda_ilusion/don_confiao/api/customers.py @@ -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) diff --git a/tienda_ilusion/don_confiao/api/payments.py b/tienda_ilusion/don_confiao/api/payments.py new file mode 100644 index 0000000..1ceeb1f --- /dev/null +++ b/tienda_ilusion/don_confiao/api/payments.py @@ -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) diff --git a/tienda_ilusion/don_confiao/api/products.py b/tienda_ilusion/don_confiao/api/products.py new file mode 100644 index 0000000..9f39195 --- /dev/null +++ b/tienda_ilusion/don_confiao/api/products.py @@ -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) diff --git a/tienda_ilusion/don_confiao/api/sales.py b/tienda_ilusion/don_confiao/api/sales.py new file mode 100644 index 0000000..e51db95 --- /dev/null +++ b/tienda_ilusion/don_confiao/api/sales.py @@ -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) diff --git a/tienda_ilusion/don_confiao/api_views.py b/tienda_ilusion/don_confiao/api_views.py deleted file mode 100644 index fa28919..0000000 --- a/tienda_ilusion/don_confiao/api_views.py +++ /dev/null @@ -1,525 +0,0 @@ -from rest_framework import viewsets -from rest_framework.response import Response -from rest_framework.status import HTTP_400_BAD_REQUEST -from rest_framework.views import APIView -from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import IsAuthenticated - -from .models.sales import Sale, SaleLine -from .models.customers import Customer -from .models.sales import ( - Sale, - SaleLine, - CatalogSale, - CatalogSaleLine, - Payment, -) -from .models.products import Product, ProductCategory -from .models.payments import PaymentMethods, ReconciliationJar -from .models.admin import AdminCode - -from .serializers import ( - SaleSerializer, - CatalogSaleSerializer, - ProductSerializer, - CustomerSerializer, - ReconciliationJarSerializer, - PaymentMethodSerializer, - SaleForRenconciliationSerializer, - SaleSummarySerializer, -) -from .views import sales_to_tryton_csv -from .permissions import IsAdministrator - -from decimal import Decimal -from sabatron_tryton_rpc_client.client import Client -import io -import csv -import os - -TRYTON_HOST = os.environ.get("TRYTON_HOST", "localhost") -TRYTON_DATABASE = os.environ.get("TRYTON_DATABASE", "tryton") -TRYTON_USERNAME = os.environ.get("TRYTON_USERNAME", "admin") -TRYTON_PASSWORD = os.environ.get("TRYTON_PASSWORD", "admin") -TRYTON_COP_CURRENCY = 31 -TRYTON_COMPANY_ID = 1 -TRYTON_SHOPS = [1] - - -class Pagination(PageNumberPagination): - page_size = 10 - page_size_query_param = "page_size" - - -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 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 CustomerView(viewsets.ModelViewSet): - queryset = Customer.objects.all() - serializer_class = CustomerSerializer - - -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 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) - - -class SaleSummary(APIView): - def get(self, request, id): - sale = Sale.objects.get(pk=id) - serializer = SaleSummarySerializer(sale) - return Response(serializer.data) - - -class AdminCodeValidateView(APIView): - permission_classes = [IsAuthenticated, IsAdministrator] - - def get(self, request, code): - codes = AdminCode.objects.filter(value=code) - return Response({"validCode": bool(codes)}) - - -class ReconciliateJarModelView(viewsets.ModelViewSet): - queryset = ReconciliationJar.objects.all().order_by("-date_time") - pagination_class = Pagination - serializer_class = ReconciliationJarSerializer - permission_classes = [IsAuthenticated, IsAdministrator] - - -class SalesForTrytonView(APIView): - permission_classes = [IsAuthenticated, IsAdministrator] - - def get(self, request): - sales = Sale.objects.all() - csv = self._generate_sales_CSV(sales) - return Response({"csv": csv}) - - 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 = Client( - hostname=TRYTON_HOST, - database=TRYTON_DATABASE, - username=TRYTON_USERNAME, - password=TRYTON_PASSWORD, - ) - tryton_client.connect() - method = "model.sale.sale.create" - tryton_context = { - "company": TRYTON_COMPANY_ID, - "shops": TRYTON_SHOPS, - } - - successful = [] - failed = [] - - sales = Sale.objects.filter(external_id=None) - for sale in sales: - try: - lines = SaleLine.objects.filter(sale=sale.id) - tryton_params = self.__to_tryton_params(sale, lines, tryton_context) - external_ids = tryton_client.call(method, tryton_params) - sale.external_id = external_ids[0] - sale.save() - successful.append(sale.id) - except Exception as e: - print(f"Error al enviar la venta: {e}venta_id: {sale.id}") - failed.append(sale.id) - continue - - return Response({"successful": successful, "failed": failed}, status=200) - - def __to_tryton_params(self, sale, lines, tryton_context): - sale_tryton = TrytonSale(sale, lines) - return [[sale_tryton.to_tryton()], tryton_context] - - -class TrytonSale: - def __init__(self, sale, lines): - self.sale = 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.sale.customer.address_external_id, - "invoice_address": self.sale.customer.address_external_id, - "currency": TRYTON_COP_CURRENCY, - "comment": self.sale.description or "", - "description": "Metodo pago: " + str(self.sale.payment_method or ""), - "party": self.sale.customer.external_id, - "reference": "don_confiao " + str(self.sale.id), - "sale_date": self._format_date(self.sale.date), - "lines": [ - [ - "create", - [TrytonLineSale(line).to_tryton() for line in self.lines], - ] - ], - "self_pick_up": True, - } - - -class TrytonLineSale: - def __init__(self, sale_line): - self.sale_line = sale_line - - def _format_decimal(self, number): - return {"__class__": "Decimal", "decimal": str(number)} - - def to_tryton(self): - return { - "product": self.sale_line.product.external_id, - "quantity": self._format_decimal(self.sale_line.quantity), - "type": "line", - "unit": self.sale_line.product.unit_external_id, - "unit_price": self._format_decimal(self.sale_line.unit_price), - } - - -class ProductsFromTrytonView(APIView): - permission_classes = [IsAuthenticated, IsAdministrator] - - def post(self, request): - tryton_client = Client( - hostname=TRYTON_HOST, - database=TRYTON_DATABASE, - username=TRYTON_USERNAME, - password=TRYTON_PASSWORD, - ) - tryton_client.connect() - method = "model.product.product.search" - context = {"company": 1} - params = [ - [["salable", "=", True]], - 0, - 1000, - [["rec_name", "ASC"], ["id", None]], - context, - ] - product_ids = tryton_client.call(method, params) - tryton_products = self.__get_product_datails_from_tryton( - product_ids, tryton_client, context - ) - checked_tryton_products = product_ids - failed_products = [] - updated_products = [] - created_products = [] - untouched_products = [] - - for tryton_product in tryton_products: - try: - product = Product.objects.get(external_id=tryton_product.get("id")) - except Product.DoesNotExist: - try: - product = self.__create_product(tryton_product) - created_products.append(product.id) - continue - except Exception as e: - print( - f"Error al importar productos: {e}El producto: {tryton_product}" - ) - failed_products.append(tryton_product.get("id")) - continue - - if self.__need_update(product, tryton_product): - self.__update_product(product, tryton_product) - updated_products.append(product.id) - else: - untouched_products.append(product.id) - - return Response( - { - "checked_tryton_products": checked_tryton_products, - "failed_products": failed_products, - "updated_products": updated_products, - "created_products": created_products, - "untouched_products": untouched_products, - }, - status=200, - ) - - def __get_product_datails_from_tryton(self, product_ids, tryton_client, context): - tryton_fields = [ - "id", - "name", - "default_uom.id", - "default_uom.rec_name", - "list_price", - ] - method = "model.product.product.read" - params = (product_ids, tryton_fields, context) - response = tryton_client.call(method, params) - return response - - def __need_update(self, product, tryton_product): - if not product.name == tryton_product.get("name"): - return True - if not product.price == tryton_product.get("list_price"): - return True - unit = tryton_product.get("default_uom.") - if not product.measuring_unit == unit.get("rec_name"): - return True - - def __create_product(self, tryton_product): - product = Product() - product.name = tryton_product.get("name") - product.price = tryton_product.get("list_price") - product.external_id = tryton_product.get("id") - unit = tryton_product.get("default_uom.") - product.measuring_unit = unit.get("rec_name") - product.unit_external_id = unit.get("id") - product.save() - return product - - def __update_product(self, product, tryton_product): - product.name = tryton_product.get("name") - product.price = tryton_product.get("list_price") - product.external_id = tryton_product.get("id") - unit = tryton_product.get("default_uom.") - product.measuring_unit = unit.get("rec_name") - product.unit_external_id = unit.get("id") - product.save() - - -class CustomersFromTrytonView(APIView): - permission_classes = [IsAuthenticated, IsAdministrator] - - def post(self, request): - tryton_client = Client( - hostname=TRYTON_HOST, - database=TRYTON_DATABASE, - username=TRYTON_USERNAME, - password=TRYTON_PASSWORD, - ) - tryton_client.connect() - method = "model.party.party.search" - context = {"company": 1} - params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context] - party_ids = tryton_client.call(method, params) - tryton_parties = self.__get_party_datails(party_ids, tryton_client, context) - checked_tryton_parties = party_ids - failed_parties = [] - updated_customers = [] - created_customers = [] - untouched_customers = [] - - for tryton_party in tryton_parties: - try: - customer = Customer.objects.get(external_id=tryton_party.get("id")) - except Customer.DoesNotExist: - customer = self.__create_customer(tryton_party) - created_customers.append(customer.id) - continue - if self.__need_update(customer, tryton_party): - self.__update_customer(customer, tryton_party) - updated_customers.append(customer.id) - else: - untouched_customers.append(customer.id) - - return Response( - { - "checked_tryton_parties": checked_tryton_parties, - "failed_parties": failed_parties, - "updated_customers": updated_customers, - "created_customers": created_customers, - "untouched_customers": untouched_customers, - }, - status=200, - ) - - def __get_party_datails(self, party_ids, tryton_client, context): - tryton_fields = ["id", "name", "addresses"] - method = "model.party.party.read" - params = (party_ids, tryton_fields, context) - response = tryton_client.call(method, params) - return response - - def __need_update(self, customer, tryton_party): - if not customer.name == tryton_party.get("name"): - return True - if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: - if not customer.address_external_id == str( - tryton_party.get("addresses")[0] - ): - return True - - def __create_customer(self, tryton_party): - customer = Customer() - customer.name = tryton_party.get("name") - customer.external_id = tryton_party.get("id") - if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: - customer.address_external_id = tryton_party.get("addresses")[0] - customer.save() - return customer - - def __update_customer(self, customer, tryton_party): - customer.name = tryton_party.get("name") - customer.external_id = tryton_party.get("id") - if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: - customer.address_external_id = tryton_party.get("addresses")[0] - customer.save() diff --git a/tienda_ilusion/don_confiao/serializers/__init__.py b/tienda_ilusion/don_confiao/serializers/__init__.py new file mode 100644 index 0000000..cc3ba6e --- /dev/null +++ b/tienda_ilusion/don_confiao/serializers/__init__.py @@ -0,0 +1,35 @@ +from .products import ProductSerializer, ListProductSerializer +from .customers import CustomerSerializer, ListCustomerSerializer +from .sales import ( + SaleSerializer, + SaleLineSerializer, + CatalogSaleSerializer, + CatalogSaleLineSerializer, + SummarySaleLineSerializer, + SaleSummarySerializer, + SaleForRenconciliationSerializer, +) +from .payments import ( + ReconciliationJarSerializer, + PaymentMethodSerializer, +) + +__all__ = [ + # Products + "ProductSerializer", + "ListProductSerializer", + # Customers + "CustomerSerializer", + "ListCustomerSerializer", + # Sales + "SaleSerializer", + "SaleLineSerializer", + "CatalogSaleSerializer", + "CatalogSaleLineSerializer", + "SummarySaleLineSerializer", + "SaleSummarySerializer", + "SaleForRenconciliationSerializer", + # Payments + "ReconciliationJarSerializer", + "PaymentMethodSerializer", +] diff --git a/tienda_ilusion/don_confiao/serializers/customers.py b/tienda_ilusion/don_confiao/serializers/customers.py new file mode 100644 index 0000000..e368719 --- /dev/null +++ b/tienda_ilusion/don_confiao/serializers/customers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from ..models.customers import Customer + + +class CustomerSerializer(serializers.ModelSerializer): + class Meta: + model = Customer + fields = ["id", "name", "address", "email", "phone", "external_id"] + + +class ListCustomerSerializer(serializers.ModelSerializer): + class Meta: + model = Customer + fields = ["id", "name"] diff --git a/tienda_ilusion/don_confiao/serializers/payments.py b/tienda_ilusion/don_confiao/serializers/payments.py new file mode 100644 index 0000000..32d4826 --- /dev/null +++ b/tienda_ilusion/don_confiao/serializers/payments.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from ..models.payments import ReconciliationJar, PaymentMethods +from .sales import SaleSerializer + + +class ReconciliationJarSerializer(serializers.ModelSerializer): + Sales = SaleSerializer(many=True, read_only=True) + + class Meta: + model = ReconciliationJar + fields = [ + "id", + "date_time", + "reconcilier", + "cash_taken", + "cash_discrepancy", + "total_cash_purchases", + "Sales", + ] + + +class PaymentMethodSerializer(serializers.Serializer): + text = serializers.CharField() + value = serializers.CharField() + + def to_representation(self, instance): + return { + "text": instance[1], + "value": instance[0], + } diff --git a/tienda_ilusion/don_confiao/serializers/products.py b/tienda_ilusion/don_confiao/serializers/products.py new file mode 100644 index 0000000..62d7c88 --- /dev/null +++ b/tienda_ilusion/don_confiao/serializers/products.py @@ -0,0 +1,23 @@ +from rest_framework import serializers + +from ..models.products import Product, ProductCategory + + +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = [ + "id", + "name", + "active", + "price", + "measuring_unit", + "categories", + "external_id", + ] + + +class ListProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = ["id", "name"] diff --git a/tienda_ilusion/don_confiao/serializers.py b/tienda_ilusion/don_confiao/serializers/sales.py similarity index 57% rename from tienda_ilusion/don_confiao/serializers.py rename to tienda_ilusion/don_confiao/serializers/sales.py index 423a5de..f3ca25b 100644 --- a/tienda_ilusion/don_confiao/serializers.py +++ b/tienda_ilusion/don_confiao/serializers/sales.py @@ -1,16 +1,14 @@ from rest_framework import serializers -from .models.sales import Sale, SaleLine -from .models.customers import Customer -from .models.sales import ( +from ..models.sales import ( Sale, SaleLine, CatalogSale, CatalogSaleLine, Payment, ) -from .models.products import Product, ProductCategory -from .models.payments import ReconciliationJar +from .products import ListProductSerializer +from .customers import ListCustomerSerializer class SaleLineSerializer(serializers.ModelSerializer): @@ -49,9 +47,7 @@ class CatalogSaleLineSerializer(serializers.ModelSerializer): class CatalogSaleSerializer(serializers.ModelSerializer): - catalogsaleline_set = CatalogSaleLineSerializer( - many=True, required=False - ) + catalogsaleline_set = CatalogSaleLineSerializer(many=True, required=False) total = serializers.ReadOnlyField(source="get_total") class Meta: @@ -69,89 +65,11 @@ class CatalogSaleSerializer(serializers.ModelSerializer): catalog_sale = CatalogSale.objects.create(**validated_data) for line_data in lines_data: - CatalogSaleLine.objects.create( - catalog_sale=catalog_sale, **line_data - ) + CatalogSaleLine.objects.create(catalog_sale=catalog_sale, **line_data) return catalog_sale -class ProductSerializer(serializers.ModelSerializer): - class Meta: - model = Product - fields = [ - "id", - "name", - "active", - "price", - "measuring_unit", - "categories", - "external_id", - ] - - -class CustomerSerializer(serializers.ModelSerializer): - class Meta: - model = Customer - fields = ["id", "name", "address", "email", "phone", "external_id"] - - -class ReconciliationJarSerializer(serializers.ModelSerializer): - Sales = SaleSerializer(many=True, read_only=True) - - class Meta: - model = ReconciliationJar - fields = [ - "id", - "date_time", - "reconcilier", - "cash_taken", - "cash_discrepancy", - "total_cash_purchases", - "Sales", - ] - - -class PaymentMethodSerializer(serializers.Serializer): - text = serializers.CharField() - value = serializers.CharField() - - def to_representation(self, instance): - return { - "text": instance[1], - "value": instance[0], - } - - -class SaleForRenconciliationSerializer(serializers.Serializer): - id = serializers.IntegerField() - date = serializers.DateTimeField() - payment_method = serializers.CharField() - customer = serializers.SerializerMethodField() - total = serializers.SerializerMethodField() - - def get_customer(self, sale): - return { - "id": sale.customer.id, - "name": sale.customer.name, - } - - def get_total(self, sale): - return sale.get_total() - - -class ListCustomerSerializer(serializers.ModelSerializer): - class Meta: - model = Customer - fields = ["id", "name"] - - -class ListProductSerializer(serializers.ModelSerializer): - class Meta: - model = Product - fields = ["id", "name"] - - class SummarySaleLineSerializer(serializers.ModelSerializer): product = ListProductSerializer() @@ -167,3 +85,20 @@ class SaleSummarySerializer(serializers.ModelSerializer): class Meta: model = Sale fields = ["id", "date", "customer", "payment_method", "lines"] + + +class SaleForRenconciliationSerializer(serializers.Serializer): + id = serializers.IntegerField() + date = serializers.DateTimeField() + payment_method = serializers.CharField() + customer = serializers.SerializerMethodField() + total = serializers.SerializerMethodField() + + def get_customer(self, sale): + return { + "id": sale.customer.id, + "name": sale.customer.name, + } + + def get_total(self, sale): + return sale.get_total() diff --git a/tienda_ilusion/don_confiao/services/__init__.py b/tienda_ilusion/don_confiao/services/__init__.py new file mode 100644 index 0000000..d0669fe --- /dev/null +++ b/tienda_ilusion/don_confiao/services/__init__.py @@ -0,0 +1,13 @@ +from .tryton import ( + get_tryton_client, + ProductTrytonService, + CustomerTrytonService, + SaleTrytonService, +) + +__all__ = [ + "get_tryton_client", + "ProductTrytonService", + "CustomerTrytonService", + "SaleTrytonService", +] diff --git a/tienda_ilusion/don_confiao/services/tryton/__init__.py b/tienda_ilusion/don_confiao/services/tryton/__init__.py new file mode 100644 index 0000000..bf7434f --- /dev/null +++ b/tienda_ilusion/don_confiao/services/tryton/__init__.py @@ -0,0 +1,13 @@ +from .client import get_tryton_client, TrytonSale, TrytonLineSale +from .products import ProductTrytonService +from .customers import CustomerTrytonService +from .sales import SaleTrytonService + +__all__ = [ + "get_tryton_client", + "TrytonSale", + "TrytonLineSale", + "ProductTrytonService", + "CustomerTrytonService", + "SaleTrytonService", +] diff --git a/tienda_ilusion/don_confiao/services/tryton/client.py b/tienda_ilusion/don_confiao/services/tryton/client.py new file mode 100644 index 0000000..6d6d027 --- /dev/null +++ b/tienda_ilusion/don_confiao/services/tryton/client.py @@ -0,0 +1,77 @@ +import os +from sabatron_tryton_rpc_client.client import Client + +TRYTON_HOST = os.environ.get("TRYTON_HOST", "localhost") +TRYTON_DATABASE = os.environ.get("TRYTON_DATABASE", "tryton") +TRYTON_USERNAME = os.environ.get("TRYTON_USERNAME", "admin") +TRYTON_PASSWORD = os.environ.get("TRYTON_PASSWORD", "admin") +TRYTON_COP_CURRENCY = 31 +TRYTON_COMPANY_ID = 1 +TRYTON_SHOPS = [1] + + +def get_tryton_client(): + """Factory para crear cliente Tryton conectado""" + client = Client( + hostname=TRYTON_HOST, + database=TRYTON_DATABASE, + username=TRYTON_USERNAME, + password=TRYTON_PASSWORD, + ) + client.connect() + return client + + +class TrytonSale: + """Representa una venta para exportación a Tryton""" + + def __init__(self, sale, lines): + self.sale = 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.sale.customer.address_external_id, + "invoice_address": self.sale.customer.address_external_id, + "currency": TRYTON_COP_CURRENCY, + "comment": self.sale.description or "", + "description": "Metodo pago: " + str(self.sale.payment_method or ""), + "party": self.sale.customer.external_id, + "reference": "don_confiao " + str(self.sale.id), + "sale_date": self._format_date(self.sale.date), + "lines": [ + [ + "create", + [TrytonLineSale(line).to_tryton() for line in self.lines], + ] + ], + "self_pick_up": True, + } + + +class TrytonLineSale: + """Representa una línea de venta para exportación a Tryton""" + + def __init__(self, sale_line): + self.sale_line = sale_line + + def _format_decimal(self, number): + return {"__class__": "Decimal", "decimal": str(number)} + + def to_tryton(self): + return { + "product": self.sale_line.product.external_id, + "quantity": self._format_decimal(self.sale_line.quantity), + "type": "line", + "unit": self.sale_line.product.unit_external_id, + "unit_price": self._format_decimal(self.sale_line.unit_price), + } diff --git a/tienda_ilusion/don_confiao/services/tryton/customers.py b/tienda_ilusion/don_confiao/services/tryton/customers.py new file mode 100644 index 0000000..ffdf677 --- /dev/null +++ b/tienda_ilusion/don_confiao/services/tryton/customers.py @@ -0,0 +1,81 @@ +from ...models.customers import Customer + + +class CustomerTrytonService: + """Servicio para sincronización de clientes con Tryton""" + + def __init__(self, tryton_client): + self.client = tryton_client + + def import_from_tryton(self): + """Importa clientes desde Tryton""" + method = "model.party.party.search" + context = {"company": 1} + params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context] + party_ids = self.client.call(method, params) + tryton_parties = self._get_party_details(party_ids, context) + + checked_tryton_parties = party_ids + failed_parties = [] + updated_customers = [] + created_customers = [] + untouched_customers = [] + + for tryton_party in tryton_parties: + try: + customer = Customer.objects.get(external_id=tryton_party.get("id")) + except Customer.DoesNotExist: + customer = self._create_customer(tryton_party) + created_customers.append(customer.id) + continue + + if self._need_update(customer, tryton_party): + self._update_customer(customer, tryton_party) + updated_customers.append(customer.id) + else: + untouched_customers.append(customer.id) + + return { + "checked_tryton_parties": checked_tryton_parties, + "failed_parties": failed_parties, + "updated_customers": updated_customers, + "created_customers": created_customers, + "untouched_customers": untouched_customers, + } + + def _get_party_details(self, party_ids, context): + """Obtiene detalles de clientes desde Tryton""" + tryton_fields = ["id", "name", "addresses"] + method = "model.party.party.read" + params = (party_ids, tryton_fields, context) + response = self.client.call(method, params) + return response + + def _need_update(self, customer, tryton_party): + """Verifica si el cliente necesita actualización""" + if not customer.name == tryton_party.get("name"): + return True + if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: + if not customer.address_external_id == str( + tryton_party.get("addresses")[0] + ): + return True + return False + + def _create_customer(self, tryton_party): + """Crea un nuevo cliente desde datos de Tryton""" + customer = Customer() + customer.name = tryton_party.get("name") + customer.external_id = tryton_party.get("id") + if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: + customer.address_external_id = tryton_party.get("addresses")[0] + customer.save() + return customer + + def _update_customer(self, customer, tryton_party): + """Actualiza un cliente existente con datos de Tryton""" + customer.name = tryton_party.get("name") + customer.external_id = tryton_party.get("id") + if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: + customer.address_external_id = tryton_party.get("addresses")[0] + customer.save() diff --git a/tienda_ilusion/don_confiao/services/tryton/products.py b/tienda_ilusion/don_confiao/services/tryton/products.py new file mode 100644 index 0000000..1510bba --- /dev/null +++ b/tienda_ilusion/don_confiao/services/tryton/products.py @@ -0,0 +1,104 @@ +from ...models.products import Product + + +class ProductTrytonService: + """Servicio para sincronización de productos con Tryton""" + + def __init__(self, tryton_client): + self.client = tryton_client + + def import_from_tryton(self): + """Importa productos desde Tryton""" + method = "model.product.product.search" + context = {"company": 1} + params = [ + [["salable", "=", True]], + 0, + 1000, + [["rec_name", "ASC"], ["id", None]], + context, + ] + product_ids = self.client.call(method, params) + tryton_products = self._get_product_details(product_ids, context) + + checked_tryton_products = product_ids + failed_products = [] + updated_products = [] + created_products = [] + untouched_products = [] + + for tryton_product in tryton_products: + try: + product = Product.objects.get(external_id=tryton_product.get("id")) + except Product.DoesNotExist: + try: + product = self._create_product(tryton_product) + created_products.append(product.id) + continue + except Exception as e: + print( + f"Error al importar productos: {e}El producto: {tryton_product}" + ) + failed_products.append(tryton_product.get("id")) + continue + + if self._need_update(product, tryton_product): + self._update_product(product, tryton_product) + updated_products.append(product.id) + else: + untouched_products.append(product.id) + + return { + "checked_tryton_products": checked_tryton_products, + "failed_products": failed_products, + "updated_products": updated_products, + "created_products": created_products, + "untouched_products": untouched_products, + } + + def _get_product_details(self, product_ids, context): + """Obtiene detalles de productos desde Tryton""" + tryton_fields = [ + "id", + "name", + "default_uom.id", + "default_uom.rec_name", + "list_price", + ] + method = "model.product.product.read" + params = (product_ids, tryton_fields, context) + response = self.client.call(method, params) + return response + + def _need_update(self, product, tryton_product): + """Verifica si el producto necesita actualización""" + if not product.name == tryton_product.get("name"): + return True + if not product.price == tryton_product.get("list_price"): + return True + unit = tryton_product.get("default_uom.") + if not product.measuring_unit == unit.get("rec_name"): + return True + return False + + def _create_product(self, tryton_product): + """Crea un nuevo producto desde datos de Tryton""" + product = Product() + product.name = tryton_product.get("name") + product.price = tryton_product.get("list_price") + product.external_id = tryton_product.get("id") + unit = tryton_product.get("default_uom.") + product.measuring_unit = unit.get("rec_name") + product.unit_external_id = unit.get("id") + product.save() + return product + + def _update_product(self, product, tryton_product): + """Actualiza un producto existente con datos de Tryton""" + product.name = tryton_product.get("name") + product.price = tryton_product.get("list_price") + product.external_id = tryton_product.get("id") + unit = tryton_product.get("default_uom.") + product.measuring_unit = unit.get("rec_name") + product.unit_external_id = unit.get("id") + product.save() diff --git a/tienda_ilusion/don_confiao/services/tryton/sales.py b/tienda_ilusion/don_confiao/services/tryton/sales.py new file mode 100644 index 0000000..1e1f35a --- /dev/null +++ b/tienda_ilusion/don_confiao/services/tryton/sales.py @@ -0,0 +1,41 @@ +from ...models.sales import Sale, SaleLine +from .client import TrytonSale, TRYTON_COMPANY_ID, TRYTON_SHOPS + + +class SaleTrytonService: + """Servicio para sincronización de ventas con Tryton""" + + def __init__(self, tryton_client): + self.client = tryton_client + + def send_to_tryton(self): + """Envía ventas sin external_id a Tryton""" + method = "model.sale.sale.create" + tryton_context = { + "company": TRYTON_COMPANY_ID, + "shops": TRYTON_SHOPS, + } + + successful = [] + failed = [] + + sales = Sale.objects.filter(external_id=None) + for sale in sales: + try: + lines = SaleLine.objects.filter(sale=sale.id) + tryton_params = self._to_tryton_params(sale, lines, tryton_context) + external_ids = self.client.call(method, tryton_params) + sale.external_id = external_ids[0] + sale.save() + successful.append(sale.id) + except Exception as e: + print(f"Error al enviar la venta: {e}venta_id: {sale.id}") + failed.append(sale.id) + continue + + return {"successful": successful, "failed": failed} + + def _to_tryton_params(self, sale, lines, tryton_context): + """Convierte venta a parámetros para Tryton""" + sale_tryton = TrytonSale(sale, lines) + return [[sale_tryton.to_tryton()], tryton_context] diff --git a/tienda_ilusion/don_confiao/urls.py b/tienda_ilusion/don_confiao/urls.py index a381834..2406bc8 100644 --- a/tienda_ilusion/don_confiao/urls.py +++ b/tienda_ilusion/don_confiao/urls.py @@ -2,20 +2,38 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter from . import views -from . import api_views +from .api import ( + # Products + ProductView, + ProductsFromTrytonView, + # Customers + CustomerView, + CustomersFromTrytonView, + # Sales + SaleView, + CatalogSaleView, + SaleSummary, + SalesForTrytonView, + SalesToTrytonView, + # Payments + ReconciliateJarView, + ReconciliateJarModelView, + PaymentMethodView, + SalesForReconciliationView, + # Admin + AdminCodeValidateView, +) app_name = "don_confiao" router = DefaultRouter() -router.register(r"sales", api_views.SaleView, basename="sale") -router.register( - r"catalog_sales", api_views.CatalogSaleView, basename="catalog_sale" -) -router.register(r"customers", api_views.CustomerView, basename="customer") -router.register(r"products", api_views.ProductView, basename="product") +router.register(r"sales", SaleView, basename="sale") +router.register(r"catalog_sales", CatalogSaleView, basename="catalog_sale") +router.register(r"customers", CustomerView, basename="customer") +router.register(r"products", ProductView, basename="product") router.register( r"reconciliate_jar", - api_views.ReconciliateJarModelView, + ReconciliateJarModelView, basename="reconciliate_jar", ) @@ -23,39 +41,39 @@ urlpatterns = [ path("productos", views.products, name="products"), path( "resumen_compra_json/", - api_views.SaleSummary.as_view(), + SaleSummary.as_view(), name="purchase_json_summary", ), path( "payment_methods/all/select_format", - api_views.PaymentMethodView.as_view(), + PaymentMethodView.as_view(), name="payment_methods_to_select", ), path( "purchases/for_reconciliation", - api_views.SalesForReconciliationView.as_view(), + SalesForReconciliationView.as_view(), name="sales_for_reconciliation", ), - path("reconciliate_jar", api_views.ReconciliateJarView.as_view()), + path("reconciliate_jar", ReconciliateJarView.as_view()), path("api/", include(router.urls)), path( "api/importar_productos_de_tryton", - api_views.ProductsFromTrytonView.as_view(), + ProductsFromTrytonView.as_view(), name="products_from_tryton", ), path( "api/importar_clientes_de_tryton", - api_views.CustomersFromTrytonView.as_view(), + CustomersFromTrytonView.as_view(), name="customers_from_tryton", ), path( "api/enviar_ventas_a_tryton", - api_views.SalesToTrytonView.as_view(), + SalesToTrytonView.as_view(), name="send_tryton", ), path( "api/admin_code/validate/", - api_views.AdminCodeValidateView.as_view(), + AdminCodeValidateView.as_view(), ), - path("api/sales/for_tryton", api_views.SalesForTrytonView.as_view()), + path("api/sales/for_tryton", SalesForTrytonView.as_view()), ]