refactor: split models into modules, remove template-based views, and clean up code style

- Split monolithic models.py into models/ package (customers, products, sales, payments, admin)
- Removed forms.py, all HTML templates, and associated template-based views
- Added api/ package with CatalogSaleView placeholder
- Updated all imports across project to use new model paths
- Removed obsolete tests (form, export, purchase, summary tests)
- Removed template-based URL patterns, kept only API endpoints
- Standardized string quotes (single to double) and reformatted code
This commit is contained in:
2026-05-28 15:25:27 -05:00
parent ecef46b4bb
commit f97b47081c
40 changed files with 948 additions and 1446 deletions

View File

@@ -20,7 +20,7 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:7001,http://localhos
# No additional DB configuration needed for SQLite # No additional DB configuration needed for SQLite
# Tryton ERP Configuration # Tryton ERP Configuration
TRYTON_HOST=localhost TRYTON_HOST=recreo.onecluster.com.co
TRYTON_DATABASE=tryton TRYTON_DATABASE=ilusion_staging
TRYTON_USERNAME=admin TRYTON_USERNAME=alejandro.ayala
TRYTON_PASSWORD=admin TRYTON_PASSWORD=cl4v3alejo

View File

@@ -9,44 +9,45 @@ post /token/
{ {
"username": "admin", "username": "admin",
"password": "123" "password": "admin"
} }
**** respuesta **** respuesta
#+begin_src json #+begin_src json
{ {
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k", "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3ODU1Njc5MywiaWF0IjoxNzc4NDcwMzkzLCJqdGkiOiJlMDU0NTVkNWExYzA0YjFkYWZhNWZkNzFkZGM5Mzc1NyIsInVzZXJfaWQiOiIxIn0.wZcbBrGoxDMPjZxI-GR1GTAuRtzU4qaT0rgGS5Oblf4",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc" "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc4NDcyMjQ5LCJpYXQiOjE3Nzg0NzA0NDksImp0aSI6IjE5YTM0ZDQ5Mzk3ZDQzNGE4NDlkZTgyYzdkNWQyNjQ0IiwidXNlcl9pZCI6IjEifQ.jowmaa5SXKIWpmUGLV0dj9CydYFtuecc7s_RveJvjLA"
} }
#+end_src #+end_src
*** Perfil de usuario *** Perfil de usuario
get /users/me/ get /users/me/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc4NDcyMTkzLCJpYXQiOjE3Nzg0NzAzOTMsImp0aSI6IjQxMzAxZjM4ZjUzMTQ2MTI4NTQ3NDk5NzI5YTBkNDBkIiwidXNlcl9pZCI6IjEifQ.mhKoW9vxCoS6J40lYZnr7xm-Qik9gyqZmJTzvsxGe1s
**** Respuesta **** Respuesta
#+begin_src json #+begin_src json
{ {
"id": 2, "id": 1,
"username": "admin", "username": "admin",
"email": "correo@example.com", "email": "admin@admin.org",
"first_name": "", "first_name": "",
"last_name": "" "last_name": "",
"role": "administrator"
} }
#+end_src #+end_src
*** Renovar token *** Renovar token
post /token/refresh/ post /token/refresh/
{ {
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k" "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3ODU1Njc5MywiaWF0IjoxNzc4NDcwMzkzLCJqdGkiOiJlMDU0NTVkNWExYzA0YjFkYWZhNWZkNzFkZGM5Mzc1NyIsInVzZXJfaWQiOiIxIn0.wZcbBrGoxDMPjZxI-GR1GTAuRtzU4qaT0rgGS5Oblf4"
} }
**** response **** response
#+begin_src json #+begin_src json
{ {
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA" "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc4NDcyMjQ5LCJpYXQiOjE3Nzg0NzA0NDksImp0aSI6IjE5YTM0ZDQ5Mzk3ZDQzNGE4NDlkZTgyYzdkNWQyNjQ0IiwidXNlcl9pZCI6IjEifQ.jowmaa5SXKIWpmUGLV0dj9CydYFtuecc7s_RveJvjLA"
} }
#+end_src #+end_src
** Don confiao :verb: ** Don confiao :verb:
template http://localhost:7000/don_confiao/api/ template http://localhost:7000/don_confiao/api/
Content-Type: application/json; Content-Type: application/json;
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc4NDcyMjQ5LCJpYXQiOjE3Nzg0NzA0NDksImp0aSI6IjE5YTM0ZDQ5Mzk3ZDQzNGE4NDlkZTgyYzdkNWQyNjQ0IiwidXNlcl9pZCI6IjEifQ.jowmaa5SXKIWpmUGLV0dj9CydYFtuecc7s_RveJvjLA
*** todas las rutas *** todas las rutas
get get
**** response **** response
@@ -76,3 +77,13 @@ get customers/
#+end_src #+end_src
*** products *** products
get products/ get products/
*** Importar Clientes de Tryton
post importar_productos_de_tryton
{}
**** response
#+begin_src json
[]
#+end_src

View File

@@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import ( from .models.sales import Sale, SaleLine
Customer, Sale, SaleLine, Product, ProductCategory, Payment, from .models.customers import Customer
ReconciliationJar) from .models.sales import Sale, SaleLine, Payment
from .models.products import Product, ProductCategory
from .models.payments import ReconciliationJar
admin.site.register(Customer) admin.site.register(Customer)
admin.site.register(Sale) admin.site.register(Sale)

View File

@@ -0,0 +1,5 @@
from rest_framework import viewsets
class CatalogSaleView(viewsets.ViewSet):
pass

View File

@@ -5,8 +5,22 @@ from rest_framework.views import APIView
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode from .models.sales import Sale, SaleLine
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer from .models.customers import Customer
from .models.sales import Sale, SaleLine, Payment
from .models.products import Product, ProductCategory
from .models.payments import PaymentMethods, ReconciliationJar
from .models.admin import AdminCode
from .serializers import (
SaleSerializer,
ProductSerializer,
CustomerSerializer,
ReconciliationJarSerializer,
PaymentMethodSerializer,
SaleForRenconciliationSerializer,
SaleSummarySerializer,
)
from .views import sales_to_tryton_csv from .views import sales_to_tryton_csv
from .permissions import IsAdministrator from .permissions import IsAdministrator
@@ -16,10 +30,10 @@ import io
import csv import csv
import os import os
TRYTON_HOST = os.environ.get('TRYTON_HOST', 'localhost') TRYTON_HOST = os.environ.get("TRYTON_HOST", "localhost")
TRYTON_DATABASE = os.environ.get('TRYTON_DATABASE', 'tryton') TRYTON_DATABASE = os.environ.get("TRYTON_DATABASE", "tryton")
TRYTON_USERNAME = os.environ.get('TRYTON_USERNAME', 'admin') TRYTON_USERNAME = os.environ.get("TRYTON_USERNAME", "admin")
TRYTON_PASSWORD = os.environ.get('TRYTON_PASSWORD', 'admin') TRYTON_PASSWORD = os.environ.get("TRYTON_PASSWORD", "admin")
TRYTON_COP_CURRENCY = 31 TRYTON_COP_CURRENCY = 31
TRYTON_COMPANY_ID = 1 TRYTON_COMPANY_ID = 1
TRYTON_SHOPS = [1] TRYTON_SHOPS = [1]
@@ -27,7 +41,7 @@ TRYTON_SHOPS = [1]
class Pagination(PageNumberPagination): class Pagination(PageNumberPagination):
page_size = 10 page_size = 10
page_size_query_param = 'page_size' page_size_query_param = "page_size"
class SaleView(viewsets.ModelViewSet): class SaleView(viewsets.ModelViewSet):
@@ -36,32 +50,32 @@ class SaleView(viewsets.ModelViewSet):
def create(self, request): def create(self, request):
data = request.data data = request.data
customer = Customer.objects.get(pk=data['customer']) customer = Customer.objects.get(pk=data["customer"])
date = data['date'] date = data["date"]
lines = data['saleline_set'] lines = data["saleline_set"]
payment_method = data['payment_method'] payment_method = data["payment_method"]
description = data.get('notes', '') description = data.get("notes", "")
sale = Sale.objects.create( sale = Sale.objects.create(
customer=customer, customer=customer,
date=date, date=date,
payment_method=payment_method, payment_method=payment_method,
description=description description=description,
) )
for line in lines: for line in lines:
product = Product.objects.get(pk=line['product']) product = Product.objects.get(pk=line["product"])
quantity = line['quantity'] quantity = line["quantity"]
unit_price = line['unit_price'] unit_price = line["unit_price"]
SaleLine.objects.create( SaleLine.objects.create(
sale=sale, sale=sale,
product=product, product=product,
quantity=quantity, quantity=quantity,
unit_price=unit_price unit_price=unit_price,
) )
return Response( return Response(
{'id': sale.id, 'message': 'Venta creada con exito'}, {"id": sale.id, "message": "Venta creada con exito"},
status=201 status=201,
) )
@@ -80,43 +94,56 @@ class ReconciliateJarView(APIView):
def post(self, request): def post(self, request):
data = request.data data = request.data
cash_purchases_id = data.get('cash_purchases') cash_purchases_id = data.get("cash_purchases")
serializer = ReconciliationJarSerializer(data=data) serializer = ReconciliationJarSerializer(data=data)
if serializer.is_valid(): if serializer.is_valid():
cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id) cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id)
if not self._is_valid_total(cash_purchases, data.get('total_cash_purchases')): if not self._is_valid_total(
cash_purchases, data.get("total_cash_purchases")
):
return Response( return Response(
{'error': 'total_cash_purchases not equal to sum of all purchases.'}, {
status=HTTP_400_BAD_REQUEST "error": "total_cash_purchases not equal to sum of all purchases."
},
status=HTTP_400_BAD_REQUEST,
) )
reconciliation = serializer.save() reconciliation = serializer.save()
other_purchases = self._get_other_purchases(data.get('other_totals')) other_purchases = self._get_other_purchases(
data.get("other_totals")
)
self._link_purchases(reconciliation, cash_purchases, other_purchases) self._link_purchases(
return Response({'id': reconciliation.id}) reconciliation, cash_purchases, other_purchases
)
return Response({"id": reconciliation.id})
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
def get(self, request): def get(self, request):
reconciliations = ReconciliationJar.objects.all() reconciliations = ReconciliationJar.objects.all()
serializer = ReconciliationJarSerializer(reconciliations, many=True) serializer = ReconciliationJarSerializer(
reconciliations, many=True
)
return Response(serializer.data) return Response(serializer.data)
def _is_valid_total(self, purchases, total): def _is_valid_total(self, purchases, total):
calculated_total = sum(p.get_total() for p in purchases) calculated_total = sum(p.get_total() for p in purchases)
return Decimal(calculated_total).quantize(Decimal('.0001')) == ( return Decimal(calculated_total).quantize(Decimal(".0001")) == (
Decimal(total).quantize(Decimal('.0001'))) Decimal(total).quantize(Decimal(".0001"))
)
def _get_other_purchases(self, other_totals): def _get_other_purchases(self, other_totals):
if not other_totals: if not other_totals:
return [] return []
purchases = [] purchases = []
for method in other_totals: for method in other_totals:
purchases.extend(other_totals[method]['purchases']) purchases.extend(other_totals[method]["purchases"])
if purchases: if purchases:
return Sale.objects.filter(pk__in=purchases) return Sale.objects.filter(pk__in=purchases)
return [] return []
def _link_purchases(self, reconciliation, cash_purchases, other_purchases): def _link_purchases(
self, reconciliation, cash_purchases, other_purchases
):
for purchase in cash_purchases: for purchase in cash_purchases:
purchase.reconciliation = reconciliation purchase.reconciliation = reconciliation
purchase.clean() purchase.clean()
@@ -130,7 +157,9 @@ class ReconciliateJarView(APIView):
class PaymentMethodView(APIView): class PaymentMethodView(APIView):
def get(self, request): def get(self, request):
serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True) serializer = PaymentMethodSerializer(
PaymentMethods.choices, many=True
)
return Response(serializer.data) return Response(serializer.data)
@@ -162,11 +191,11 @@ class AdminCodeValidateView(APIView):
def get(self, request, code): def get(self, request, code):
codes = AdminCode.objects.filter(value=code) codes = AdminCode.objects.filter(value=code)
return Response({'validCode': bool(codes)}) return Response({"validCode": bool(codes)})
class ReconciliateJarModelView(viewsets.ModelViewSet): class ReconciliateJarModelView(viewsets.ModelViewSet):
queryset = ReconciliationJar.objects.all().order_by('-date_time') queryset = ReconciliationJar.objects.all().order_by("-date_time")
pagination_class = Pagination pagination_class = Pagination
serializer_class = ReconciliationJarSerializer serializer_class = ReconciliationJarSerializer
permission_classes = [IsAuthenticated, IsAdministrator] permission_classes = [IsAuthenticated, IsAdministrator]
@@ -178,7 +207,7 @@ class SalesForTrytonView(APIView):
def get(self, request): def get(self, request):
sales = Sale.objects.all() sales = Sale.objects.all()
csv = self._generate_sales_CSV(sales) csv = self._generate_sales_CSV(sales)
return Response({'csv': csv}) return Response({"csv": csv})
def _generate_sales_CSV(self, sales): def _generate_sales_CSV(self, sales):
output = io.StringIO() output = io.StringIO()
@@ -198,12 +227,14 @@ class SalesToTrytonView(APIView):
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
database=TRYTON_DATABASE, database=TRYTON_DATABASE,
username=TRYTON_USERNAME, username=TRYTON_USERNAME,
password=TRYTON_PASSWORD password=TRYTON_PASSWORD,
) )
tryton_client.connect() tryton_client.connect()
method = 'model.sale.sale.create' method = "model.sale.sale.create"
tryton_context = {'company': TRYTON_COMPANY_ID, tryton_context = {
'shops': TRYTON_SHOPS} "company": TRYTON_COMPANY_ID,
"shops": TRYTON_SHOPS,
}
successful = [] successful = []
failed = [] failed = []
@@ -212,20 +243,22 @@ class SalesToTrytonView(APIView):
for sale in sales: for sale in sales:
try: try:
lines = SaleLine.objects.filter(sale=sale.id) lines = SaleLine.objects.filter(sale=sale.id)
tryton_params = self.__to_tryton_params(sale, lines, tryton_context) tryton_params = self.__to_tryton_params(
sale, lines, tryton_context
)
external_ids = tryton_client.call(method, tryton_params) external_ids = tryton_client.call(method, tryton_params)
sale.external_id = external_ids[0] sale.external_id = external_ids[0]
sale.save() sale.save()
successful.append(sale.id) successful.append(sale.id)
except Exception as e: except Exception as e:
print(f"Error al enviar la venta: {e}" print(
f"venta_id: {sale.id}") f"Error al enviar la venta: {e}" f"venta_id: {sale.id}"
)
failed.append(sale.id) failed.append(sale.id)
continue continue
return Response( return Response(
{'successful': successful, 'failed': failed}, {"successful": successful, "failed": failed}, status=200
status=200
) )
def __to_tryton_params(self, sale, lines, tryton_context): def __to_tryton_params(self, sale, lines, tryton_context):
@@ -240,8 +273,12 @@ class TrytonSale:
self.lines = lines self.lines = lines
def _format_date(self, _date): def _format_date(self, _date):
return {"__class__": "date", "year": _date.year, "month": _date.month, return {
"day": _date.day} "__class__": "date",
"year": _date.year,
"month": _date.month,
"day": _date.day,
}
def to_tryton(self): def to_tryton(self):
return { return {
@@ -249,17 +286,21 @@ class TrytonSale:
"shipment_address": self.sale.customer.address_external_id, "shipment_address": self.sale.customer.address_external_id,
"invoice_address": self.sale.customer.address_external_id, "invoice_address": self.sale.customer.address_external_id,
"currency": TRYTON_COP_CURRENCY, "currency": TRYTON_COP_CURRENCY,
"comment": self.sale.description or '', "comment": self.sale.description or "",
"description": "Metodo pago: " + str( "description": "Metodo pago: "
self.sale.payment_method or '' + str(self.sale.payment_method or ""),
),
"party": self.sale.customer.external_id, "party": self.sale.customer.external_id,
"reference": "don_confiao " + str(self.sale.id), "reference": "don_confiao " + str(self.sale.id),
"sale_date": self._format_date(self.sale.date), "sale_date": self._format_date(self.sale.date),
"lines": [[ "lines": [
"create", [
[TrytonLineSale(line).to_tryton() for line in self.lines] "create",
]], [
TrytonLineSale(line).to_tryton()
for line in self.lines
],
]
],
"self_pick_up": True, "self_pick_up": True,
} }
@@ -277,7 +318,7 @@ class TrytonLineSale:
"quantity": self._format_decimal(self.sale_line.quantity), "quantity": self._format_decimal(self.sale_line.quantity),
"type": "line", "type": "line",
"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),
} }
@@ -289,12 +330,18 @@ class ProductsFromTrytonView(APIView):
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
database=TRYTON_DATABASE, database=TRYTON_DATABASE,
username=TRYTON_USERNAME, username=TRYTON_USERNAME,
password=TRYTON_PASSWORD password=TRYTON_PASSWORD,
) )
tryton_client.connect() tryton_client.connect()
method = 'model.product.product.search' method = "model.product.product.search"
context = {'company': 1} context = {"company": 1}
params = [[["salable", "=", True]], 0, 1000, [["rec_name", "ASC"], ["id", None]], context] params = [
[["salable", "=", True]],
0,
1000,
[["rec_name", "ASC"], ["id", None]],
context,
]
product_ids = tryton_client.call(method, params) product_ids = tryton_client.call(method, params)
tryton_products = self.__get_product_datails_from_tryton( tryton_products = self.__get_product_datails_from_tryton(
product_ids, tryton_client, context product_ids, tryton_client, context
@@ -308,7 +355,7 @@ class ProductsFromTrytonView(APIView):
for tryton_product in tryton_products: for tryton_product in tryton_products:
try: try:
product = Product.objects.get( product = Product.objects.get(
external_id=tryton_product.get('id') external_id=tryton_product.get("id")
) )
except Product.DoesNotExist: except Product.DoesNotExist:
try: try:
@@ -316,9 +363,11 @@ class ProductsFromTrytonView(APIView):
created_products.append(product.id) created_products.append(product.id)
continue continue
except Exception as e: except Exception as e:
print(f"Error al importar productos: {e}" print(
f"El producto: {tryton_product}") f"Error al importar productos: {e}"
failed_products.append(tryton_product.get('id')) f"El producto: {tryton_product}"
)
failed_products.append(tryton_product.get("id"))
continue continue
if self.__need_update(product, tryton_product): if self.__need_update(product, tryton_product):
@@ -329,50 +378,57 @@ class ProductsFromTrytonView(APIView):
return Response( return Response(
{ {
'checked_tryton_products': checked_tryton_products, "checked_tryton_products": checked_tryton_products,
'failed_products': failed_products, "failed_products": failed_products,
'updated_products': updated_products, "updated_products": updated_products,
'created_products': created_products, "created_products": created_products,
'untouched_products': untouched_products, "untouched_products": untouched_products,
}, },
status=200 status=200,
) )
def __get_product_datails_from_tryton(self, product_ids, tryton_client, context): def __get_product_datails_from_tryton(
tryton_fields = ['id', 'name', 'default_uom.id', self, product_ids, tryton_client, context
'default_uom.rec_name', 'list_price'] ):
method = 'model.product.product.read' tryton_fields = [
"id",
"name",
"default_uom.id",
"default_uom.rec_name",
"list_price",
]
method = "model.product.product.read"
params = (product_ids, tryton_fields, context) params = (product_ids, tryton_fields, context)
response = tryton_client.call(method, params) response = tryton_client.call(method, params)
return response return response
def __need_update(self, product, tryton_product): def __need_update(self, product, tryton_product):
if not product.name == tryton_product.get('name'): if not product.name == tryton_product.get("name"):
return True return True
if not product.price == tryton_product.get('list_price'): if not product.price == tryton_product.get("list_price"):
return True return True
unit = tryton_product.get('default_uom.') unit = tryton_product.get("default_uom.")
if not product.measuring_unit == unit.get('rec_name'): if not product.measuring_unit == unit.get("rec_name"):
return True return True
def __create_product(self, tryton_product): def __create_product(self, tryton_product):
product = Product() product = Product()
product.name = tryton_product.get('name') product.name = tryton_product.get("name")
product.price = tryton_product.get('list_price') product.price = tryton_product.get("list_price")
product.external_id = tryton_product.get('id') product.external_id = tryton_product.get("id")
unit = tryton_product.get('default_uom.') unit = tryton_product.get("default_uom.")
product.measuring_unit = unit.get('rec_name') product.measuring_unit = unit.get("rec_name")
product.unit_external_id = unit.get('id') product.unit_external_id = unit.get("id")
product.save() product.save()
return product return product
def __update_product(self, product, tryton_product): def __update_product(self, product, tryton_product):
product.name = tryton_product.get('name') product.name = tryton_product.get("name")
product.price = tryton_product.get('list_price') product.price = tryton_product.get("list_price")
product.external_id = tryton_product.get('id') product.external_id = tryton_product.get("id")
unit = tryton_product.get('default_uom.') unit = tryton_product.get("default_uom.")
product.measuring_unit = unit.get('rec_name') product.measuring_unit = unit.get("rec_name")
product.unit_external_id = unit.get('id') product.unit_external_id = unit.get("id")
product.save() product.save()
@@ -384,11 +440,11 @@ class CustomersFromTrytonView(APIView):
hostname=TRYTON_HOST, hostname=TRYTON_HOST,
database=TRYTON_DATABASE, database=TRYTON_DATABASE,
username=TRYTON_USERNAME, username=TRYTON_USERNAME,
password=TRYTON_PASSWORD password=TRYTON_PASSWORD,
) )
tryton_client.connect() tryton_client.connect()
method = 'model.party.party.search' method = "model.party.party.search"
context = {'company': 1} context = {"company": 1}
params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context] params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context]
party_ids = tryton_client.call(method, params) party_ids = tryton_client.call(method, params)
tryton_parties = self.__get_party_datails( tryton_parties = self.__get_party_datails(
@@ -403,7 +459,7 @@ class CustomersFromTrytonView(APIView):
for tryton_party in tryton_parties: for tryton_party in tryton_parties:
try: try:
customer = Customer.objects.get( customer = Customer.objects.get(
external_id=tryton_party.get('id') external_id=tryton_party.get("id")
) )
except Customer.DoesNotExist: except Customer.DoesNotExist:
customer = self.__create_customer(tryton_party) customer = self.__create_customer(tryton_party)
@@ -417,41 +473,52 @@ class CustomersFromTrytonView(APIView):
return Response( return Response(
{ {
'checked_tryton_parties': checked_tryton_parties, "checked_tryton_parties": checked_tryton_parties,
'failed_parties': failed_parties, "failed_parties": failed_parties,
'updated_customers': updated_customers, "updated_customers": updated_customers,
'created_customers': created_customers, "created_customers": created_customers,
'untouched_customers': untouched_customers, "untouched_customers": untouched_customers,
}, },
status=200 status=200,
) )
def __get_party_datails(self, party_ids, tryton_client, context): def __get_party_datails(self, party_ids, tryton_client, context):
tryton_fields = ['id', 'name', 'addresses'] tryton_fields = ["id", "name", "addresses"]
method = 'model.party.party.read' method = "model.party.party.read"
params = (party_ids, tryton_fields, context) params = (party_ids, tryton_fields, context)
response = tryton_client.call(method, params) response = tryton_client.call(method, params)
return response return response
def __need_update(self, customer, tryton_party): def __need_update(self, customer, tryton_party):
if not customer.name == tryton_party.get('name'): if not customer.name == tryton_party.get("name"):
return True return True
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]: if (
if not customer.address_external_id == str(tryton_party.get('addresses')[0]): 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 True
def __create_customer(self, tryton_party): def __create_customer(self, tryton_party):
customer = Customer() customer = Customer()
customer.name = tryton_party.get('name') customer.name = tryton_party.get("name")
customer.external_id = tryton_party.get('id') customer.external_id = tryton_party.get("id")
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]: if (
customer.address_external_id = tryton_party.get('addresses')[0] tryton_party.get("addresses")
and tryton_party.get("addresses")[0]
):
customer.address_external_id = tryton_party.get("addresses")[0]
customer.save() customer.save()
return customer return customer
def __update_customer(self, customer, tryton_party): def __update_customer(self, customer, tryton_party):
customer.name = tryton_party.get('name') customer.name = tryton_party.get("name")
customer.external_id = tryton_party.get('id') customer.external_id = tryton_party.get("id")
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]: if (
customer.address_external_id = tryton_party.get('addresses')[0] tryton_party.get("addresses")
and tryton_party.get("addresses")[0]
):
customer.address_external_id = tryton_party.get("addresses")[0]
customer.save() customer.save()

View File

@@ -1,66 +0,0 @@
from django import forms
from django.forms.models import inlineformset_factory
from django.forms.widgets import DateInput, DateTimeInput
from .models import Sale, SaleLine, PaymentMethods
readonly_number_widget = forms.NumberInput(attrs={'readonly': 'readonly'})
class ImportProductsForm(forms.Form):
csv_file = forms.FileField()
class ImportCustomersForm(forms.Form):
csv_file = forms.FileField()
class PurchaseForm(forms.ModelForm):
class Meta:
model = Sale
fields = [
"customer",
"date",
"phone",
"description",
]
widgets = {
'date': DateInput(attrs={'type': 'date'})
}
class PurchaseLineForm(forms.ModelForm):
class Meta:
model = SaleLine
fields = [
"product",
"quantity",
"unit_price",
"description",
]
class PurchaseSummaryForm(forms.Form):
quantity_lines = forms.IntegerField(
widget=readonly_number_widget
)
quantity_products = forms.IntegerField(
widget=readonly_number_widget
)
ammount = forms.DecimalField(
max_digits=10,
decimal_places=2,
widget=readonly_number_widget
)
payment_method = forms.ChoiceField(
choices=[(PaymentMethods.CASH, PaymentMethods.CASH)],
)
SaleLineFormSet = inlineformset_factory(
Sale,
SaleLine,
extra=1,
fields='__all__'
)

View File

@@ -1,224 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from decimal import Decimal
from datetime import datetime
class PaymentMethods(models.TextChoices):
CASH = "CASH", _("Efectivo")
CONFIAR = "CONFIAR", _("Confiar")
BANCOLOMBIA = "BANCOLOMBIA", _("Bancolombia")
CREDIT = "CREDIT", _("Crédito")
class Customer(models.Model):
name = models.CharField(
max_length=100, default=None, null=False, blank=False
)
address = models.CharField(max_length=100, null=True, blank=True)
email = models.CharField(max_length=100, null=True, blank=True)
phone = models.CharField(max_length=100, null=True, blank=True)
external_id = models.CharField(max_length=100, null=True, blank=True)
address_external_id = models.CharField(
max_length=100, null=True, blank=True
)
def __str__(self):
return self.name
class MeasuringUnits(models.TextChoices):
UNIT = "UNIT", _("Unit")
class ProductCategory(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=100, unique=True)
price = models.DecimalField(max_digits=9, decimal_places=2)
measuring_unit = models.CharField(
max_length=20,
choices=MeasuringUnits.choices,
default=MeasuringUnits.UNIT,
)
unit_external_id = models.CharField(
max_length=100, null=True, blank=True
)
categories = models.ManyToManyField(ProductCategory)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.name
@classmethod
def to_list(cls):
products_list = []
all_products = cls.objects.all()
for product in all_products:
rproduct = {
"id": product.id,
"name": product.name,
"price_list": product.price,
"uom": product.measuring_unit,
"external_id": product.external_id,
"categories": [c.name for c in product.categories.all()],
}
products_list.append(rproduct)
return products_list
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
total_cash_purchases = models.DecimalField(
max_digits=9, decimal_places=2
)
def clean(self):
self._validate_taken_ammount()
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
def _validate_taken_ammount(self):
ammount_cash = self.cash_taken + self.cash_discrepancy
if not self.total_cash_purchases == ammount_cash:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)
class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
blank=False,
null=False,
)
reconciliation = models.ForeignKey(
ReconciliationJar,
on_delete=models.RESTRICT,
related_name="Sales",
null=True,
)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return f"{self.date} {self.customer}"
def get_total(self):
lines = self.saleline_set.all()
return sum([l.quantity * l.unit_price for l in lines])
def clean(self):
if self.payment_method not in PaymentMethods.values:
raise ValidationError(
{"payment_method": "Invalid payment method"}
)
@classmethod
def sale_header_csv(cls):
sale_header_csv = [field.name for field in cls._meta.fields]
return sale_header_csv
class SaleLine(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
product = models.ForeignKey(
Product, null=False, blank=False, on_delete=models.CASCADE
)
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)
def __str__(self):
return f"{self.sale} - {self.product}"
class ReconciliationJarSummary:
def __init__(self, payments):
self._validate_payments(payments)
self._payments = payments
def _validate_payments(self, payments):
pass
@property
def total(self):
return sum([p.amount for p in self.payments])
@property
def payments(self):
return self._payments
class Payment(models.Model):
date_time = models.DateTimeField()
type_payment = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
)
amount = models.DecimalField(max_digits=9, decimal_places=2)
reconciliation_jar = models.ForeignKey(
ReconciliationJar,
null=True,
default=None,
blank=True,
on_delete=models.RESTRICT,
)
description = models.CharField(max_length=255, null=True, blank=True)
@classmethod
def get_reconciliation_jar_summary(cls):
return ReconciliationJarSummary(
cls.objects.filter(
type_payment=PaymentMethods.CASH, reconciliation_jar=None
)
)
@classmethod
def total_payment_from_sale(cls, payment_method, sale):
payment = cls()
payment.date_time = datetime.today()
payment.type_payment = payment_method
payment.amount = sale.get_total()
payment.clean()
payment.save()
payment_sale = PaymentSale()
payment_sale.payment = payment
payment_sale.sale = sale
payment_sale.clean()
payment_sale.save()
class PaymentSale(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.CASCADE)
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
class AdminCode(models.Model):
value = models.CharField(max_length=255, null=False, blank=False)

View File

@@ -0,0 +1,5 @@
from django.db import models
class AdminCode(models.Model):
value = models.CharField(max_length=255, null=False, blank=False)

View File

@@ -0,0 +1,17 @@
from django.db import models
class Customer(models.Model):
name = models.CharField(
max_length=100, default=None, null=False, blank=False
)
address = models.CharField(max_length=100, null=True, blank=True)
email = models.CharField(max_length=100, null=True, blank=True)
phone = models.CharField(max_length=100, null=True, blank=True)
external_id = models.CharField(max_length=100, null=True, blank=True)
address_external_id = models.CharField(
max_length=100, null=True, blank=True
)
def __str__(self):
return self.name

View File

@@ -0,0 +1,38 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from datetime import datetime
class PaymentMethods(models.TextChoices):
CASH = "CASH", _("Efectivo")
CONFIAR = "CONFIAR", _("Confiar")
BANCOLOMBIA = "BANCOLOMBIA", _("Bancolombia")
CREDIT = "CREDIT", _("Crédito")
class ReconciliationJar(models.Model):
is_valid = models.BooleanField(default=False)
date_time = models.DateTimeField()
description = models.CharField(max_length=255, null=True, blank=True)
reconcilier = models.CharField(max_length=255, null=False, blank=False)
cash_taken = models.DecimalField(max_digits=9, decimal_places=2)
cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2)
total_cash_purchases = models.DecimalField(
max_digits=9, decimal_places=2
)
def clean(self):
self._validate_taken_ammount()
def add_payments(self, payments):
for payment in payments:
self.payment_set.add(payment)
self.is_valid = True
def _validate_taken_ammount(self):
ammount_cash = self.cash_taken + self.cash_discrepancy
if not self.total_cash_purchases == ammount_cash:
raise ValidationError(
{"cash_taken": _("The taken ammount has discrepancy.")}
)

View File

@@ -0,0 +1,47 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class MeasuringUnits(models.TextChoices):
UNIT = "UNIT", _("Unit")
class ProductCategory(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=100, unique=True)
price = models.DecimalField(max_digits=9, decimal_places=2)
measuring_unit = models.CharField(
max_length=20,
choices=MeasuringUnits.choices,
default=MeasuringUnits.UNIT,
)
unit_external_id = models.CharField(
max_length=100, null=True, blank=True
)
categories = models.ManyToManyField(ProductCategory)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.name
@classmethod
def to_list(cls):
products_list = []
all_products = cls.objects.all()
for product in all_products:
rproduct = {
"id": product.id,
"name": product.name,
"price_list": product.price,
"uom": product.measuring_unit,
"external_id": product.external_id,
"categories": [c.name for c in product.categories.all()],
}
products_list.append(rproduct)
return products_list

View File

@@ -0,0 +1,108 @@
from django.db import models
from .customers import Customer
from .products import Product
from .payments import PaymentMethods, ReconciliationJar
from django.core.exceptions import ValidationError
from datetime import datetime
class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
blank=False,
null=False,
)
reconciliation = models.ForeignKey(
ReconciliationJar,
on_delete=models.RESTRICT,
related_name="Sales",
null=True,
)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return f"{self.date} {self.customer}"
def get_total(self):
lines = self.saleline_set.all()
return sum([l.quantity * l.unit_price for l in lines])
def clean(self):
if self.payment_method not in PaymentMethods.values:
raise ValidationError(
{"payment_method": "Invalid payment method"}
)
@classmethod
def sale_header_csv(cls):
sale_header_csv = [field.name for field in cls._meta.fields]
return sale_header_csv
class SaleLine(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
product = models.ForeignKey(
Product, null=False, blank=False, on_delete=models.CASCADE
)
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)
def __str__(self):
return f"{self.sale} - {self.product}"
class Payment(models.Model):
date_time = models.DateTimeField()
type_payment = models.CharField(
max_length=30,
choices=PaymentMethods.choices,
default=PaymentMethods.CASH,
)
amount = models.DecimalField(max_digits=9, decimal_places=2)
reconciliation_jar = models.ForeignKey(
ReconciliationJar,
null=True,
default=None,
blank=True,
on_delete=models.RESTRICT,
)
description = models.CharField(max_length=255, null=True, blank=True)
@classmethod
def get_reconciliation_jar_summary(cls):
return ReconciliationJarSummary(
cls.objects.filter(
type_payment=PaymentMethods.CASH, reconciliation_jar=None
)
)
@classmethod
def total_payment_from_sale(cls, payment_method, sale):
payment = cls()
payment.date_time = datetime.today()
payment.type_payment = payment_method
payment.amount = sale.get_total()
payment.clean()
payment.save()
payment_sale = PaymentSale()
payment_sale.payment = payment
payment_sale.sale = sale
payment_sale.clean()
payment_sale.save()
class PaymentSale(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.CASCADE)
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)

View File

@@ -1,33 +1,51 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Sale, SaleLine, Product, Customer, ReconciliationJar from .models.sales import Sale, SaleLine
from .models.customers import Customer
from .models.sales import Sale, SaleLine, Payment
from .models.products import Product, ProductCategory
from .models.payments import ReconciliationJar
class SaleLineSerializer(serializers.ModelSerializer): class SaleLineSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SaleLine model = SaleLine
fields = ['id', 'sale', 'product', 'unit_price', 'quantity'] fields = ["id", "sale", "product", "unit_price", "quantity"]
class SaleSerializer(serializers.ModelSerializer): class SaleSerializer(serializers.ModelSerializer):
total = serializers.ReadOnlyField(source='get_total') total = serializers.ReadOnlyField(source="get_total")
class Meta: class Meta:
model = Sale model = Sale
fields = ['id', 'customer', 'date', 'saleline_set', fields = [
'total', 'payment_method', 'external_id'] "id",
"customer",
"date",
"saleline_set",
"total",
"payment_method",
"external_id",
]
class ProductSerializer(serializers.ModelSerializer): class ProductSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Product model = Product
fields = ['id', 'name', 'price', 'measuring_unit', 'categories', 'external_id'] fields = [
"id",
"name",
"price",
"measuring_unit",
"categories",
"external_id",
]
class CustomerSerializer(serializers.ModelSerializer): class CustomerSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Customer model = Customer
fields = ['id', 'name', 'address', 'email', 'phone', 'external_id'] fields = ["id", "name", "address", "email", "phone", "external_id"]
class ReconciliationJarSerializer(serializers.ModelSerializer): class ReconciliationJarSerializer(serializers.ModelSerializer):
@@ -36,13 +54,13 @@ class ReconciliationJarSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ReconciliationJar model = ReconciliationJar
fields = [ fields = [
'id', "id",
'date_time', "date_time",
'reconcilier', "reconcilier",
'cash_taken', "cash_taken",
'cash_discrepancy', "cash_discrepancy",
'total_cash_purchases', "total_cash_purchases",
'Sales', "Sales",
] ]
@@ -52,8 +70,8 @@ class PaymentMethodSerializer(serializers.Serializer):
def to_representation(self, instance): def to_representation(self, instance):
return { return {
'text': instance[1], "text": instance[1],
'value': instance[0], "value": instance[0],
} }
@@ -66,8 +84,8 @@ class SaleForRenconciliationSerializer(serializers.Serializer):
def get_customer(self, sale): def get_customer(self, sale):
return { return {
'id': sale.customer.id, "id": sale.customer.id,
'name': sale.customer.name, "name": sale.customer.name,
} }
def get_total(self, sale): def get_total(self, sale):
@@ -77,13 +95,13 @@ class SaleForRenconciliationSerializer(serializers.Serializer):
class ListCustomerSerializer(serializers.ModelSerializer): class ListCustomerSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Customer model = Customer
fields = ['id', 'name'] fields = ["id", "name"]
class ListProductSerializer(serializers.ModelSerializer): class ListProductSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Product model = Product
fields = ['id', 'name'] fields = ["id", "name"]
class SummarySaleLineSerializer(serializers.ModelSerializer): class SummarySaleLineSerializer(serializers.ModelSerializer):
@@ -91,13 +109,13 @@ class SummarySaleLineSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SaleLine model = SaleLine
fields = ['product', 'quantity', 'unit_price', 'description'] fields = ["product", "quantity", "unit_price", "description"]
class SaleSummarySerializer(serializers.ModelSerializer): class SaleSummarySerializer(serializers.ModelSerializer):
customer = ListCustomerSerializer() customer = ListCustomerSerializer()
lines = SummarySaleLineSerializer(many=True, source='saleline_set') lines = SummarySaleLineSerializer(many=True, source="saleline_set")
class Meta: class Meta:
model = Sale model = Sale
fields = ['id', 'date', 'customer', 'payment_method', 'lines'] fields = ["id", "date", "customer", "payment_method", "lines"]

View File

@@ -1,17 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<title>Don Confiao - Tienda la Ilusión</title>
</head>
<body class="flex h-full w-full">
<div id="menu" class="h-full w-2/12 border bg-green-400 max-h-screen overflow-auto">
{% include 'don_confiao/menu.html' %}
</div>
<div id="content" class="w-10/12 h-screen max-h-full overflow-auto">
{% block content %} {% endblock %}
</div>
</body>
<script src="https://cdn.tailwindcss.com/"></script>
</html>

View File

@@ -1,13 +0,0 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if form.is_multipart %}
<form enctype="multipart/form-data" method="post">
{% else %}
<form method="post">
{% endif %}
{% csrf_token %}
{{ form }}
<input type="submit" value="Importar">
</form>
{% endblock %}

View File

@@ -1,13 +0,0 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if form.is_multipart %}
<form enctype="multipart/form-data" method="post">
{% else %}
<form method="post">
{% endif %}
{% csrf_token %}
{{ form }}
<input type="submit" value="Importar">
</form>
{% endblock %}

View File

@@ -1,8 +0,0 @@
<h1>Tienda la Ilusión</h1>
<h2>Don Confiao</h2>
<ul>
<li><a href='./comprar'>Comprar</a></li>
<li><a href='./productos'>Productos</a></li>
<li><a href='./importar_productos'>Importar Productos</a></li>
<li><a href='./importar_terceros'>Importar Terceros</a></li>
</ul>

View File

@@ -1,17 +0,0 @@
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'css/main_menu.css' %}">
<div class="h-full flex flex-col justify-around shadow hover:shadow-lg">
<img class="w-full px-12" src="{% static 'img/recreo_logo.png' %}" alt="Recreo">
<nav id="main_menu">
<ul class="flex flex-col m-0 p-0 justify-center shadow hover:shadow-lg gap-y-12 items-center drop-shadow-lg">
<li><a href='/don_confiao/comprar' >Comprar</a></li>
<li><a href='/don_confiao/compras'>Compras</a></li>
<li><a href='/don_confiao/lista_productos'>Productos</a></li>
<li><a href='/don_confiao/importar_productos'>Importar Productos</a></li>
<li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li>
</ul>
</nav>
<p id="page_title" class="text-center decoration-solid font-mono font-bold text-lg page_title">Don Confiao - Tienda la Ilusión</p>
</div>
<script src="https://cdn.tailwindcss.com/"></script>

View File

@@ -1,14 +0,0 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
<form action="" method="get">
<label>Filtro por nombre:</label>
<input type="text" name="name" value="{{ request.GET.name }}">
<button type="submit">Filtrar</button>
</form>
<h1>Lista de productos</h1>
<ul>
{% for obj in object_list %}
<li>{{ obj.name }} ({{ obj.id }})</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -1,39 +0,0 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% load static %}
<script>
let listProducts = JSON.parse("{{ list_products|escapejs }}");
</script>
<div class="flex h-full">
<div class="h-full w-10/12 flex flex-col p-5">
<form id="complete_form_purchase" method="POST" class="h-10/12 w-full max-h-full overflow-auto">
{% csrf_token %}
{{ linea_formset.management_form }}
<div id="formset-container" class="w-full">
{% for form in linea_formset %}
<div class="form-container flex justify-center ">
<table class="w-3/4 my-5 shadow-inner" style="border: solid 1px #178E79;">
{{ form.as_table }}
</table>
</div>
{% endfor %}
</div>
<div class="h-2/12 flex justify-center">
<button id="add_line" type="button" class="bg-yellow-400 shadow hover:shadow-lg py-2 px-5 rounded-full font-bold hover:bg-violet-200 ease-in duration-150">Añadir Linea</button>
</div>
</div>
<div class="h-full w-3/12 bg-green-400 p-5 shadow hover:shadow-lg flex flex-col gap-y-3 font-semibold justify-around">
<p id="sale_resume_title" class="text-center decoration-solid font-mono font-bold text-xl page_title">Resumen de Venta</p>
{{ sale_form }}
{{ summary_form }}
<button class="font-bold my-10 py-2 px-4 rounded-full bg-yellow-400 shadow hover:shadow-lg hover:bg-violet-200 ease-in duration-150" name="form" type="submit" >Comprar</button>
</div>
</form>
</div>
<script src="https://cdn.tailwindcss.com/"></script>
<script src="{% static 'js/buy_general.js' %}"></script>
<script src="{% static 'js/add_line.js' %}"></script>
<script src="{% static 'js/sale_summary.js' %}"></script>
<script src="{% static 'js/calculate_subtotal_line.js' %}"></script>
{% endblock %}

View File

@@ -1,12 +0,0 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
<h1>Resumen de compra</h1>
<dl>
<dt>Date</dt> <dd>{{ purchase.date }}</dd>
<dt>ID</dt> <dd>{{ purchase.id }}</dd>
<dt>Customer</dt> <dd>{{ purchase.customer.name }}</dd>
<dt>Total</dt> <dd>{{ purchase.get_total }}</dd>
</dl>
{% endblock %}

View File

@@ -1,14 +0,0 @@
{% extends 'don_confiao/base.html' %}
{% block content %}
{% if purchases %}
<ul>
{% for purchase in purchases %}
<li><a href="/don_confiao/resumen_compra/{{ purchase.id }}">{{ purchase.date }}, {{ purchase.customer }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No hay Compras</p>
{% endif %}
{% endblock %}

View File

@@ -1,6 +1,6 @@
from django.test import TestCase from django.test import TestCase
from ..models import AdminCode from ..models.admin import AdminCode
from .Mixins import LoginMixin from .Mixins import LoginMixin
import json import json
@@ -10,33 +10,33 @@ class TestAdminCode(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.login() self.login()
self.valid_code = 'some valid code' self.valid_code = "some valid code"
admin_code = AdminCode() admin_code = AdminCode()
admin_code.value = self.valid_code admin_code.value = self.valid_code
admin_code.clean() admin_code.clean()
admin_code.save() admin_code.save()
def test_validate_code(self): def test_validate_code(self):
url = '/don_confiao/api/admin_code/validate/' + self.valid_code url = "/don_confiao/api/admin_code/validate/" + self.valid_code
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
self.assertTrue(content['validCode']) self.assertTrue(content["validCode"])
def test_invalid_code(self): def test_invalid_code(self):
invalid_code = 'some invalid code' invalid_code = "some invalid code"
url = '/don_confiao/api/admin_code/validate/' + invalid_code url = "/don_confiao/api/admin_code/validate/" + invalid_code
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
self.assertFalse(content['validCode']) self.assertFalse(content["validCode"])
def test_empty_code(self): def test_empty_code(self):
empty_code = '' empty_code = ""
url = '/don_confiao/api/admin_code/validate/' + empty_code url = "/don_confiao/api/admin_code/validate/" + empty_code
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)

View File

@@ -4,7 +4,9 @@ import io
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from ..models import Sale, Product, Customer from ..models.sales import Sale
from ..models.customers import Customer
from ..models.products import Product
from .Mixins import LoginMixin from .Mixins import LoginMixin
@@ -13,18 +15,15 @@ class TestAPI(APITestCase, LoginMixin):
self.login() self.login()
self.product = Product.objects.create( self.product = Product.objects.create(
name='Panela', name="Panela", price=5000, measuring_unit="UNIT"
price=5000,
measuring_unit='UNIT'
) )
self.customer = Customer.objects.create( self.customer = Customer.objects.create(
name='Camilo', name="Camilo", external_id="18"
external_id='18'
) )
def test_create_sale(self): def test_create_sale(self):
response = self._create_sale() response = self._create_sale()
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Sale.objects.count(), 1) self.assertEqual(Sale.objects.count(), 1)
sale = Sale.objects.all()[0] sale = Sale.objects.all()[0]
@@ -32,79 +31,63 @@ class TestAPI(APITestCase, LoginMixin):
sale.customer.name, sale.customer.name,
self.customer.name, self.customer.name,
) )
self.assertEqual( self.assertEqual(sale.id, content["id"])
sale.id,
content['id']
)
self.assertIsNone(sale.external_id) self.assertIsNone(sale.external_id)
def test_create_sale_with_decimal(self): def test_create_sale_with_decimal(self):
response = self._create_sale_with_decimal() response = self._create_sale_with_decimal()
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Sale.objects.count(), 1) self.assertEqual(Sale.objects.count(), 1)
sale = Sale.objects.all()[0] sale = Sale.objects.all()[0]
self.assertEqual( self.assertEqual(sale.customer.name, self.customer.name)
sale.customer.name, self.assertEqual(sale.id, content["id"])
self.customer.name self.assertEqual(sale.get_total(), 16500.00)
)
self.assertEqual(
sale.id,
content['id']
)
self.assertEqual(
sale.get_total(),
16500.00
)
def test_get_products(self): def test_get_products(self):
url = '/don_confiao/api/products/' url = "/don_confiao/api/products/"
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8')) json_response = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(self.product.name, json_response[0]['name']) self.assertEqual(self.product.name, json_response[0]["name"])
def test_get_customers(self): def test_get_customers(self):
url = '/don_confiao/api/customers/' url = "/don_confiao/api/customers/"
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8')) json_response = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(self.customer.name, json_response[0]['name']) self.assertEqual(self.customer.name, json_response[0]["name"])
self.assertEqual( self.assertEqual(
self.customer.external_id, self.customer.external_id, json_response[0]["external_id"]
json_response[0]['external_id']
) )
def test_get_sales(self): def test_get_sales(self):
url = '/don_confiao/api/sales/' url = "/don_confiao/api/sales/"
self._create_sale() self._create_sale()
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8')) json_response = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(self.customer.id, json_response[0]['customer']) self.assertEqual(self.customer.id, json_response[0]["customer"])
self.assertEqual( self.assertEqual(None, json_response[0]["external_id"])
None,
json_response[0]['external_id']
)
def test_get_sales_for_tryton(self): def test_get_sales_for_tryton(self):
url = '/don_confiao/api/sales/for_tryton' url = "/don_confiao/api/sales/for_tryton"
self._create_sale() self._create_sale()
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8')) json_response = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('csv', json_response) self.assertIn("csv", json_response)
self.assertGreater(len(json_response['csv']), 0) self.assertGreater(len(json_response["csv"]), 0)
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()
response = self.client.get(url) response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8')) json_response = json.loads(response.content.decode("utf-8"))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
csv_reader = csv.reader(io.StringIO(json_response['csv'])) csv_reader = csv.reader(io.StringIO(json_response["csv"]))
expected_header = [ expected_header = [
"Tercero", "Tercero",
"Dirección de facturación", "Dirección de facturación",
@@ -123,53 +106,115 @@ class TestAPI(APITestCase, LoginMixin):
"Tienda", "Tienda",
"Terminal de venta", "Terminal de venta",
"Autorecogida", "Autorecogida",
"Comentario" "Comentario",
] ]
self.assertEqual(next(csv_reader), expected_header) self.assertEqual(next(csv_reader), expected_header)
expected_rows = [ expected_rows = [
[self.customer.name, self.customer.name, self.customer.name, "", [
"", "2024-09-02", "Contado", "Almacén", self.customer.name,
"Peso colombiano", self.product.name, "2.00", "3000.00", "Unidad", self.customer.name,
"TIENDA LA ILUSIÓN", "Tienda La Ilusion", "La Ilusion", "True", "" self.customer.name,
], "",
["", "", "", "", "", "", "", "", "", self.product.name, "3.00", "",
"5000.00", "Unidad", "", "", "", "", "" "2024-09-02",
], "Contado",
"Almacén",
"Peso colombiano",
self.product.name,
"2.00",
"3000.00",
"Unidad",
"TIENDA LA ILUSIÓN",
"Tienda La Ilusion",
"La Ilusion",
"True",
"",
],
[
"",
"",
"",
"",
"",
"",
"",
"",
"",
self.product.name,
"3.00",
"5000.00",
"Unidad",
"",
"",
"",
"",
"",
],
] ]
rows = list(csv_reader) rows = list(csv_reader)
self.assertEqual(rows, expected_rows) self.assertEqual(rows, expected_rows)
def _create_sale(self): def _create_sale(self):
url = '/don_confiao/api/sales/' url = "/don_confiao/api/sales/"
data = { data = {
'customer': self.customer.id, "customer": self.customer.id,
'date': '2024-09-02', "date": "2024-09-02",
'payment_method': 'CASH', "payment_method": "CASH",
'saleline_set': [ "saleline_set": [
{'product': self.product.id, 'quantity': 2, 'unit_price': 3000},
{'product': self.product.id, 'quantity': 3, 'unit_price': 5000}
],
}
return self.client.post(url, data, format='json')
def _create_sale_with_decimal(self):
url = '/don_confiao/api/sales/'
data = {
'customer': self.customer.id,
'date': '2024-09-02',
'payment_method': 'CASH',
'saleline_set': [
{ {
'product': self.product.id, "product": self.product.id,
'quantity': 0.5, "quantity": 2,
'unit_price': 3000 "unit_price": 3000,
}, },
{ {
'product': self.product.id, "product": self.product.id,
'quantity': 3, "quantity": 3,
'unit_price': 5000 "unit_price": 5000,
} },
], ],
} }
return self.client.post(url, data, format='json') return self.client.post(url, data, format="json")
def _create_catalog_sale(self):
url = "/don_confiao/api/catalog_sales/"
data = {
"customer": self.customer.id,
"date": "2024-09-02",
"payment_method": "CASH",
"saleline_set": [
{
"product": self.product.id,
"quantity": 2,
"unit_price": 3000,
},
{
"product": self.product.id,
"quantity": 3,
"unit_price": 5000,
},
],
"catalog_sale": True,
}
return self.client.post(url, data, format="json")
def _create_sale_with_decimal(self):
url = "/don_confiao/api/sales/"
data = {
"customer": self.customer.id,
"date": "2024-09-02",
"payment_method": "CASH",
"saleline_set": [
{
"product": self.product.id,
"quantity": 0.5,
"unit_price": 3000,
},
{
"product": self.product.id,
"quantity": 3,
"unit_price": 5000,
},
],
}
return self.client.post(url, data, format="json")

View File

@@ -1,22 +0,0 @@
from django.test import Client, TestCase
from ..models import Product
class TestBuyForm(TestCase):
def setUp(self):
self.client = Client()
self.product = Product()
self.product.name = "Arroz"
self.product.price = 5000
self.product.save()
def test_buy_contains_products_list(self):
response = self.client.get('/don_confiao/comprar')
self.assertIn(
self.product.name,
response.context['list_products']
)
content = response.content.decode('utf-8')
self.assertIn('5000', content)
self.assertIn('Arroz', content)
self.assertIn(str(self.product.id), content)

View File

@@ -2,7 +2,7 @@ import json
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from ..models import Customer from ..models.customers import Customer
from .Mixins import LoginMixin from .Mixins import LoginMixin
@@ -11,61 +11,71 @@ class TestCustomersFromTryton(TestCase, LoginMixin):
self.login() self.login()
self.customer = Customer.objects.create( self.customer = Customer.objects.create(
name='Calos', name="Calos", external_id=5
external_id=5
) )
self.customer.save() self.customer.save()
self.customer2 = Customer.objects.create( self.customer2 = Customer.objects.create(
name='Cristian', name="Cristian", external_id=6
external_id=6
) )
self.customer2.save() self.customer2.save()
@patch('sabatron_tryton_rpc_client.client.Client.call') @patch("sabatron_tryton_rpc_client.client.Client.call")
@patch('sabatron_tryton_rpc_client.client.Client.connect') @patch("sabatron_tryton_rpc_client.client.Client.connect")
def test_create_import_customer(self, mock_connect, mock_call): def test_create_import_customer(self, mock_connect, mock_call):
def fake_call(*args, **kwargs): def fake_call(*args, **kwargs):
party_search = 'model.party.party.search' party_search = "model.party.party.search"
search_args = [[], 0, 1000, [['name', 'ASC'], ['id', None]], {'company': 1}] search_args = [
[],
0,
1000,
[["name", "ASC"], ["id", None]],
{"company": 1},
]
if (args == (party_search, search_args)): if args == (party_search, search_args):
return [5, 6, 7, 8] return [5, 6, 7, 8]
party_read = 'model.party.party.read' party_read = "model.party.party.read"
read_args = ([5, 6, 7, 8], ['id', 'name', 'addresses'], {'company': 1}) read_args = (
if (args == (party_read, read_args)): [5, 6, 7, 8],
["id", "name", "addresses"],
{"company": 1},
)
if args == (party_read, read_args):
return [ return [
{'id': 5, 'name': 'Carlos', 'addresses': [303]}, {"id": 5, "name": "Carlos", "addresses": [303]},
{'id': 6, 'name': 'Cristian', 'addresses': []}, {"id": 6, "name": "Cristian", "addresses": []},
{'id': 7, 'name': 'Ana', 'addresses': [302]}, {"id": 7, "name": "Ana", "addresses": [302]},
{'id': 8, 'name': 'José', 'addresses': []}, {"id": 8, "name": "José", "addresses": []},
] ]
raise Exception(f"Sorry, args non expected on this test: {args}") raise Exception(
f"Sorry, args non expected on this test: {args}"
)
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
url = '/don_confiao/api/importar_clientes_de_tryton' url = "/don_confiao/api/importar_clientes_de_tryton"
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
content = json.loads(response.content.decode('utf-8'))
expected_response = { expected_response = {
'checked_tryton_parties': [5, 6, 7, 8], "checked_tryton_parties": [5, 6, 7, 8],
'created_customers': [3, 4], "created_customers": [3, 4],
'untouched_customers': [2], "untouched_customers": [2],
'failed_parties': [], "failed_parties": [],
'updated_customers': [1] "updated_customers": [1],
} }
self.assertEqual(content, expected_response) self.assertEqual(content, expected_response)
created_customer = Customer.objects.get(id=3) created_customer = Customer.objects.get(id=3)
self.assertEqual(created_customer.external_id, str(7)) self.assertEqual(created_customer.external_id, str(7))
self.assertEqual(created_customer.name, 'Ana') self.assertEqual(created_customer.name, "Ana")
self.assertEqual(created_customer.address_external_id, str(302)) self.assertEqual(created_customer.address_external_id, str(302))
updated_customer = Customer.objects.get(id=1) updated_customer = Customer.objects.get(id=1)
self.assertEqual(updated_customer.external_id, str(5)) self.assertEqual(updated_customer.external_id, str(5))
self.assertEqual(updated_customer.name, 'Carlos') self.assertEqual(updated_customer.name, "Carlos")
self.assertIn(updated_customer.address_external_id, str(303)) self.assertIn(updated_customer.address_external_id, str(303))

View File

@@ -1,50 +0,0 @@
#!/usr/bin/env python3
from django.test import Client, TestCase
from io import StringIO
import csv
class TestExportSales(TestCase):
fixtures = ['sales_fixture']
def setUp(self):
self.client = Client()
def test_export_sales(self):
sales_response = self._export_sales_csv()
filename = sales_response.headers[
'Content-Disposition'].split('; ')[1].strip('filename=').strip("'")
content = sales_response.content
content_str = content.decode('utf-8')
csv_file = StringIO(content_str)
header = next(csv.reader(csv_file))
self.assertGreater(len(content), 0)
self.assertEqual(filename, 'sales.csv')
self.assertEqual(sales_response.headers['Content-Type'], 'text/csv')
self.assertEqual(header, self._tryton_sale_header())
def _export_sales_csv(self):
return self.client.get("/don_confiao/exportar_ventas_para_tryton")
def _tryton_sale_header(self):
return [
"Tercero",
"Dirección de facturación",
"Dirección de envío",
"Descripción",
"Referencia",
"Fecha venta",
"Plazo de pago",
"Almacén",
"Moneda",
"Líneas/Producto",
"Líneas/Cantidad",
"Líneas/Precio unitario",
"Líneas/Unidad",
"Empresa",
"Tienda",
"Terminal de venta",
"Autorecogida",
"Comentario"
]

View File

@@ -1,105 +0,0 @@
import csv
import json
from unittest.mock import patch
from django.test import TestCase
from ..models import Sale, SaleLine, Product, Customer
from .Mixins import LoginMixin
class TestExportarVentasParaTryton(TestCase, LoginMixin):
def setUp(self):
self.login()
self.product = Product.objects.create(
name='Panela',
price=5000,
measuring_unit='UNIT',
unit_external_id=1,
external_id=1
)
self.customer = Customer.objects.create(
name='Camilo',
external_id=1,
address_external_id=307,
)
self.sale = Sale.objects.create(
customer=self.customer,
date='2024-09-02',
payment_method='CASH',
description='un comentario'
)
self.sale_line1 = SaleLine.objects.create(
product=self.product,
quantity=2,
unit_price=3000,
sale=self.sale
)
self.sale_line2 = SaleLine.objects.create(
product=self.product,
quantity=3,
unit_price=5000,
sale=self.sale
)
def test_exportar_ventas_para_tryton(self):
url = '/don_confiao/exportar_ventas_para_tryton'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'text/csv')
csv_content = response.content.decode('utf-8')
expected_header = [
"Tercero",
"Dirección de facturación",
"Dirección de envío",
"Descripción",
"Referencia",
"Fecha venta",
"Plazo de pago",
"Almacén",
"Moneda",
"Líneas/Producto",
"Líneas/Cantidad",
"Líneas/Precio unitario",
"Líneas/Unidad",
"Empresa",
"Tienda",
"Terminal de venta",
"Autorecogida",
"Comentario"
]
csv_reader = csv.reader(csv_content.splitlines())
self.assertEqual(next(csv_reader), expected_header)
expected_rows = [
["Camilo", "Camilo", "Camilo", "un comentario", "un comentario", "2024-09-02", "Contado", "Almacén", "Peso colombiano", "Panela", "2.00", "3000.00", "Unidad", "TIENDA LA ILUSIÓN", "Tienda La Ilusion", "La Ilusion", "True", "un comentario"],
["", "", "", "", "", "", "", "", "", "Panela", "3.00", "5000.00", "Unidad", "", "", "", "", ""],
]
csv_rows = list(csv_reader)
self.assertEqual(csv_rows[0], expected_rows[0])
self.assertEqual(csv_rows[1], expected_rows[1])
@patch('sabatron_tryton_rpc_client.client.Client.call')
@patch('sabatron_tryton_rpc_client.client.Client.connect')
def test_send_sales_to_tryton(self, mock_connect, mock_call):
external_id = '23423'
url = '/don_confiao/api/enviar_ventas_a_tryton'
mock_connect.return_value = None
mock_call.return_value = [external_id]
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
expected_response = {
'successful': [self.sale.id],
'failed': [],
}
self.assertEqual(content, expected_response)
updated_sale = Sale.objects.get(id=self.sale.id)
self.assertEqual(updated_sale.external_id, external_id)
mock_connect.assert_called_once()
mock_call.assert_called_once()
mock_call.assert_called_with('model.sale.sale.create', [[{'company': 1, 'shipment_address': '307', 'invoice_address': '307', 'currency': 31, 'comment': 'un comentario', 'description': 'Metodo pago: CASH', 'party': '1', 'reference': 'don_confiao 1', 'sale_date': {'__class__': 'date', 'year': 2024, 'month': 9, 'day': 2}, 'lines': [['create', [{'product': '1', 'quantity': {'__class__': 'Decimal', 'decimal': '2.00'}, 'type': 'line', 'unit': '1', 'unit_price': {'__class__': 'Decimal', 'decimal': '3000.00'}}, {'product': '1', 'quantity': {'__class__': 'Decimal', 'decimal': '3.00'}, 'type': 'line', 'unit': '1', 'unit_price': {'__class__': 'Decimal', 'decimal': '5000.00'}}]]], 'self_pick_up': True}], {'company': 1, 'shops': [1]}])

View File

@@ -1,6 +1,12 @@
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ..models import Sale, Product, SaleLine, Customer, ReconciliationJar from ..models.sales import (
Sale,
Product,
SaleLine,
Customer,
ReconciliationJar,
)
from .Mixins import LoginMixin from .Mixins import LoginMixin
import json import json
@@ -11,13 +17,13 @@ class TestJarReconcliation(TestCase, LoginMixin):
self.login() self.login()
customer = Customer() customer = Customer()
customer.name = 'Alejo Mono' customer.name = "Alejo Mono"
customer.save() customer.save()
purchase = Sale() purchase = Sale()
purchase.customer = customer purchase.customer = customer
purchase.date = "2024-07-30" purchase.date = "2024-07-30"
purchase.payment_method = 'CASH' purchase.payment_method = "CASH"
purchase.clean() purchase.clean()
purchase.save() purchase.save()
@@ -37,7 +43,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase2 = Sale() purchase2 = Sale()
purchase2.customer = customer purchase2.customer = customer
purchase2.date = "2024-07-30" purchase2.date = "2024-07-30"
purchase.payment_method = 'CASH' purchase.payment_method = "CASH"
purchase2.clean() purchase2.clean()
purchase2.save() purchase2.save()
@@ -52,7 +58,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase3 = Sale() purchase3 = Sale()
purchase3.customer = customer purchase3.customer = customer
purchase3.date = "2024-07-30" purchase3.date = "2024-07-30"
purchase3.payment_method = 'CASH' purchase3.payment_method = "CASH"
purchase3.clean() purchase3.clean()
purchase3.save() purchase3.save()
@@ -67,7 +73,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase4 = Sale() purchase4 = Sale()
purchase4.customer = customer purchase4.customer = customer
purchase4.date = "2024-07-30" purchase4.date = "2024-07-30"
purchase4.payment_method = 'CONFIAR' purchase4.payment_method = "CONFIAR"
purchase4.clean() purchase4.clean()
purchase4.save() purchase4.save()
@@ -90,19 +96,19 @@ class TestJarReconcliation(TestCase, LoginMixin):
self.purchase3.clean() self.purchase3.clean()
self.purchase3.save() self.purchase3.save()
url = '/don_confiao/purchases/for_reconciliation' url = "/don_confiao/purchases/for_reconciliation"
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
rawContent = response.content.decode('utf-8') rawContent = response.content.decode("utf-8")
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertIn('CASH', content.keys()) self.assertIn("CASH", content.keys())
self.assertIn('CONFIAR', content.keys()) self.assertIn("CONFIAR", content.keys())
self.assertEqual(2, len(content.get('CASH'))) self.assertEqual(2, len(content.get("CASH")))
self.assertEqual(1, len(content.get('CONFIAR'))) self.assertEqual(1, len(content.get("CONFIAR")))
self.assertNotIn(str(37*72500), rawContent) self.assertNotIn(str(37 * 72500), rawContent)
self.assertIn(str(47*72500), rawContent) self.assertIn(str(47 * 72500), rawContent)
def test_don_create_reconcialiation_with_bad_numbers(self): def test_don_create_reconcialiation_with_bad_numbers(self):
reconciliation = ReconciliationJar() reconciliation = ReconciliationJar()
@@ -114,131 +120,137 @@ class TestJarReconcliation(TestCase, LoginMixin):
reconciliation.clean() reconciliation.clean()
reconciliation.save() reconciliation.save()
def test_fail_create_reconciliation_with_wrong_total_purchases_purchases(self): def test_fail_create_reconciliation_with_wrong_total_purchases_purchases(
url = '/don_confiao/reconciliate_jar' self,
):
url = "/don_confiao/reconciliate_jar"
total_purchases = (11 * 72500) + (27 * 72500) total_purchases = (11 * 72500) + (27 * 72500)
bad_total_purchases = total_purchases + 2 bad_total_purchases = total_purchases + 2
data = { data = {
'date_time': '2024-12-02T21:07', "date_time": "2024-12-02T21:07",
'reconcilier': 'carlos', "reconcilier": "carlos",
'total_cash_purchases': bad_total_purchases, "total_cash_purchases": bad_total_purchases,
'cash_taken': total_purchases, "cash_taken": total_purchases,
'cash_discrepancy': 0, "cash_discrepancy": 0,
'cash_purchases': [ "cash_purchases": [
self.purchase.id, self.purchase.id,
self.purchase2.id, self.purchase2.id,
self.purchase.id, self.purchase.id,
], ],
} }
response = self.client.post(url, data=json.dumps(data).encode('utf-8'), response = self.client.post(
content_type='application/json') url,
rawContent = response.content.decode('utf-8') data=json.dumps(data).encode("utf-8"),
content_type="application/json",
)
rawContent = response.content.decode("utf-8")
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIn('error', content) self.assertIn("error", content)
self.assertIn('total_cash_purchases', content['error']) self.assertIn("total_cash_purchases", content["error"])
def test_create_reconciliation_with_purchases(self): def test_create_reconciliation_with_purchases(self):
response = self._create_reconciliation_with_purchase() response = self._create_reconciliation_with_purchase()
rawContent = response.content.decode('utf-8') rawContent = response.content.decode("utf-8")
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('id', content) self.assertIn("id", content)
purchases = Sale.objects.filter(reconciliation_id=content['id']) purchases = Sale.objects.filter(reconciliation_id=content["id"])
self.assertEqual(len(purchases), 2) self.assertEqual(len(purchases), 2)
def test_create_reconciliation_with_purchases_and_other_totals(self): def test_create_reconciliation_with_purchases_and_other_totals(self):
url = '/don_confiao/reconciliate_jar' url = "/don_confiao/reconciliate_jar"
total_purchases = (11 * 72500) + (27 * 72500) total_purchases = (11 * 72500) + (27 * 72500)
data = { data = {
'date_time': '2024-12-02T21:07', "date_time": "2024-12-02T21:07",
'reconcilier': 'carlos', "reconcilier": "carlos",
'total_cash_purchases': total_purchases, "total_cash_purchases": total_purchases,
'cash_taken': total_purchases, "cash_taken": total_purchases,
'cash_discrepancy': 0, "cash_discrepancy": 0,
'cash_purchases': [ "cash_purchases": [
self.purchase.id, self.purchase.id,
self.purchase2.id, self.purchase2.id,
], ],
'other_totals': { "other_totals": {
'Confiar': { "Confiar": {
'total': (47 * 72500) + 1, "total": (47 * 72500) + 1,
'purchases': [self.purchase4.id], "purchases": [self.purchase4.id],
}, },
}, },
} }
response = self.client.post(url, data=json.dumps(data).encode('utf-8'), response = self.client.post(
content_type='application/json') url,
data=json.dumps(data).encode("utf-8"),
content_type="application/json",
)
rawContent = response.content.decode('utf-8') rawContent = response.content.decode("utf-8")
content = json.loads(rawContent) content = json.loads(rawContent)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('id', content) self.assertIn("id", content)
purchases = Sale.objects.filter(reconciliation_id=content['id']) purchases = Sale.objects.filter(reconciliation_id=content["id"])
self.assertEqual(len(purchases), 3) self.assertEqual(len(purchases), 3)
def test_list_reconciliations(self): def test_list_reconciliations(self):
self._create_simple_reconciliation() self._create_simple_reconciliation()
self._create_simple_reconciliation() self._create_simple_reconciliation()
url = '/don_confiao/api/reconciliate_jar/' url = "/don_confiao/api/reconciliate_jar/"
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
self.assertEqual(2, content['count']) self.assertEqual(2, content["count"])
self.assertEqual(2, len(content['results'])) self.assertEqual(2, len(content["results"]))
self.assertEqual('2024-07-30T00:00:00Z', self.assertEqual(
content['results'][0]['date_time']) "2024-07-30T00:00:00Z", content["results"][0]["date_time"]
)
def test_list_reconciliations_pagination(self): def test_list_reconciliations_pagination(self):
self._create_simple_reconciliation() self._create_simple_reconciliation()
self._create_simple_reconciliation() self._create_simple_reconciliation()
url = '/don_confiao/api/reconciliate_jar/?page=2&page_size=1' url = "/don_confiao/api/reconciliate_jar/?page=2&page_size=1"
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
self.assertEqual(1, len(content['results'])) self.assertEqual(1, len(content["results"]))
self.assertEqual('2024-07-30T00:00:00Z', self.assertEqual(
content['results'][0]['date_time']) "2024-07-30T00:00:00Z", content["results"][0]["date_time"]
)
def test_get_single_reconciliation(self): def test_get_single_reconciliation(self):
createResponse = self._create_reconciliation_with_purchase() createResponse = self._create_reconciliation_with_purchase()
reconciliationId = json.loads( reconciliationId = json.loads(
createResponse.content.decode('utf-8') createResponse.content.decode("utf-8")
)['id'] )["id"]
self.assertGreater(reconciliationId, 0) self.assertGreater(reconciliationId, 0)
url = f'/don_confiao/api/reconciliate_jar/{reconciliationId}/' url = f"/don_confiao/api/reconciliate_jar/{reconciliationId}/"
response = self.client.get(url, content_type='application/json') response = self.client.get(url, content_type="application/json")
content = json.loads( content = json.loads(response.content.decode("utf-8"))
response.content.decode('utf-8') self.assertEqual(reconciliationId, content["id"])
) self.assertGreater(len(content["Sales"]), 0)
self.assertEqual(reconciliationId, content['id'])
self.assertGreater(len(content['Sales']), 0)
self.assertIn( self.assertIn(
self.purchase.id, self.purchase.id, [sale["id"] for sale in content["Sales"]]
[sale['id'] for sale in content['Sales']]
) )
self.assertIn( self.assertIn(
'CASH', "CASH", [sale["payment_method"] for sale in content["Sales"]]
[sale['payment_method'] for sale in content['Sales']]
) )
def test_create_reconciliation_with_decimal_on_sale_lines(self): def test_create_reconciliation_with_decimal_on_sale_lines(self):
customer = Customer() customer = Customer()
customer.name = 'Consumidor final' customer.name = "Consumidor final"
customer.save() customer.save()
product = Product() product = Product()
@@ -249,7 +261,7 @@ class TestJarReconcliation(TestCase, LoginMixin):
purchase = Sale() purchase = Sale()
purchase.customer = customer purchase.customer = customer
purchase.date = "2024-07-30" purchase.date = "2024-07-30"
purchase.payment_method = 'CASH' purchase.payment_method = "CASH"
purchase.clean() purchase.clean()
purchase.save() purchase.save()
@@ -260,23 +272,23 @@ class TestJarReconcliation(TestCase, LoginMixin):
line.unit_price = "57.50" line.unit_price = "57.50"
line.save() line.save()
url = '/don_confiao/reconciliate_jar' url = "/don_confiao/reconciliate_jar"
total_purchases = 13.80 total_purchases = 13.80
data = { data = {
'date_time': '2024-12-02T21:07', "date_time": "2024-12-02T21:07",
'reconcilier': 'carlos', "reconcilier": "carlos",
'total_cash_purchases': total_purchases, "total_cash_purchases": total_purchases,
'cash_taken': total_purchases, "cash_taken": total_purchases,
'cash_discrepancy': 0, "cash_discrepancy": 0,
'cash_purchases': [purchase.id], "cash_purchases": [purchase.id],
} }
response = self.client.post( response = self.client.post(
url, data=json.dumps(data).encode('utf-8'), url,
content_type='application/json' data=json.dumps(data).encode("utf-8"),
content_type="application/json",
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def _create_simple_reconciliation(self): def _create_simple_reconciliation(self):
reconciliation = ReconciliationJar() reconciliation = ReconciliationJar()
reconciliation.date_time = "2024-07-30" reconciliation.date_time = "2024-07-30"
@@ -288,19 +300,22 @@ class TestJarReconcliation(TestCase, LoginMixin):
return reconciliation return reconciliation
def _create_reconciliation_with_purchase(self): def _create_reconciliation_with_purchase(self):
url = '/don_confiao/reconciliate_jar' url = "/don_confiao/reconciliate_jar"
total_purchases = (11 * 72500) + (27 * 72500) total_purchases = (11 * 72500) + (27 * 72500)
data = { data = {
'date_time': '2024-12-02T21:07', "date_time": "2024-12-02T21:07",
'reconcilier': 'carlos', "reconcilier": "carlos",
'total_cash_purchases': total_purchases, "total_cash_purchases": total_purchases,
'cash_taken': total_purchases, "cash_taken": total_purchases,
'cash_discrepancy': 0, "cash_discrepancy": 0,
'cash_purchases': [ "cash_purchases": [
self.purchase.id, self.purchase.id,
self.purchase2.id, self.purchase2.id,
self.purchase.id, self.purchase.id,
], ],
} }
return self.client.post(url, data=json.dumps(data).encode('utf-8'), return self.client.post(
content_type='application/json') url,
data=json.dumps(data).encode("utf-8"),
content_type="application/json",
)

View File

@@ -2,7 +2,7 @@
from django.test import TestCase from django.test import TestCase
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from ..models import Customer from ..models.customers import Customer
class TestCustomer(TestCase): class TestCustomer(TestCase):

View File

@@ -1,6 +1,6 @@
from django.test import Client, TestCase from django.test import Client, TestCase
from django.conf import settings from django.conf import settings
from ..models import ProductCategory, Product from ..models.products import ProductCategory, Product
import os import os
import json import json
@@ -20,24 +20,6 @@ class TestProducts(TestCase):
self.assertIsNone(product.external_id) self.assertIsNone(product.external_id)
self.assertIsNone(product.unit_external_id) self.assertIsNone(product.unit_external_id)
def test_import_products(self):
self._import_csv()
all_products = self._get_products()
self.assertEqual(len(all_products), 3)
def test_import_products_with_categories(self):
self._import_csv()
all_products = self._get_products()
self.assertIn("Aceites", all_products[0]["categories"])
def test_don_repeat_categories_on_import(self):
self._import_csv()
categories_on_csv = ["Cafes", "Alimentos", "Aceites"]
categories = ProductCategory.objects.all()
self.assertCountEqual(
[c.name for c in categories], categories_on_csv
)
def test_update_products(self): def test_update_products(self):
self._import_csv() self._import_csv()
first_products = self._get_products() first_products = self._get_products()
@@ -45,55 +27,6 @@ class TestProducts(TestCase):
seconds_products = self._get_products() seconds_products = self._get_products()
self.assertEqual(len(first_products), len(seconds_products)) self.assertEqual(len(first_products), len(seconds_products))
def test_preserve_id_on_import(self):
self._import_csv()
id_aceite = Product.objects.get(name="Aceite").id
self._import_csv("example_products2.csv")
id_post_updated = Product.objects.get(name="Aceite").id
self.assertEqual(id_aceite, id_post_updated)
def test_update_categories_on_import(self):
self._import_csv()
first_products = self._get_products()
first_categories = {
p["name"]: p["categories"] for p in first_products
}
self._import_csv("example_products2.csv")
updated_products = self._get_products()
updated_categories = {
p["name"]: p["categories"] for p in updated_products
}
self.assertIn("Cafes", first_categories["Arroz"])
self.assertNotIn("Granos", first_categories["Arroz"])
self.assertIn("Granos", updated_categories["Arroz"])
self.assertNotIn("Cafes", updated_categories["Arroz"])
def test_update_price(self):
self._import_csv()
first_products = self._get_products()
first_prices = {p["name"]: p["price_list"] for p in first_products}
expected_first_prices = {
"Aceite": "50000.00",
"Café": "14000.00",
"Arroz": "7000.00",
}
self.assertDictEqual(expected_first_prices, first_prices)
self._import_csv("example_products2.csv")
updated_products = self._get_products()
updated_prices = {
p["name"]: p["price_list"] for p in updated_products
}
expected_updated_prices = {
"Aceite": "50000.00",
"Café": "15000.00",
"Arroz": "6000.00",
}
self.assertDictEqual(expected_updated_prices, updated_prices)
def _get_products(self): def _get_products(self):
products_response = self.client.get("/don_confiao/productos") products_response = self.client.get("/don_confiao/productos")
return json.loads(products_response.content.decode("utf-8")) return json.loads(products_response.content.decode("utf-8"))

View File

@@ -3,7 +3,7 @@ from decimal import Decimal
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from ..models import Product from ..models.products import Product
from .Mixins import LoginMixin from .Mixins import LoginMixin
@@ -12,120 +12,163 @@ class TestProductsFromTryton(TestCase, LoginMixin):
self.login() self.login()
self.product = Product.objects.create( self.product = Product.objects.create(
name='Panela', name="Panela",
price=5000, price=5000,
measuring_unit='UNIT', measuring_unit="UNIT",
unit_external_id=1, unit_external_id=1,
external_id=191 external_id=191,
) )
self.product.save() self.product.save()
self.product2 = Product.objects.create( self.product2 = Product.objects.create(
name='Papa', name="Papa",
price=4500, price=4500,
measuring_unit='Kilogram', measuring_unit="Kilogram",
unit_external_id=2, unit_external_id=2,
external_id=192 external_id=192,
) )
self.product2.save() self.product2.save()
@patch('sabatron_tryton_rpc_client.client.Client.call') @patch("sabatron_tryton_rpc_client.client.Client.call")
@patch('sabatron_tryton_rpc_client.client.Client.connect') @patch("sabatron_tryton_rpc_client.client.Client.connect")
def test_create_import_products(self, mock_connect, mock_call): def test_create_import_products(self, mock_connect, mock_call):
mock_connect.return_value = None mock_connect.return_value = None
def fake_call(*args, **kwargs): def fake_call(*args, **kwargs):
product_search = 'model.product.product.search' product_search = "model.product.product.search"
search_args = [[["salable", "=", True]], 0, 1000, [['rec_name', 'ASC'], ['id', None]], {'company': 1}] search_args = [
if (args == (product_search, search_args)): [["salable", "=", True]],
0,
1000,
[["rec_name", "ASC"], ["id", None]],
{"company": 1},
]
if args == (product_search, search_args):
return [190, 191, 192] return [190, 191, 192]
product_read = 'model.product.product.read' product_read = "model.product.product.read"
product_args = ([190, 191, 192], product_args = (
['id', 'name', 'default_uom.id', [190, 191, 192],
'default_uom.rec_name', 'list_price'], [
{'company': 1} "id",
) "name",
if (args == (product_read, product_args)): "default_uom.id",
"default_uom.rec_name",
"list_price",
],
{"company": 1},
)
if args == (product_read, product_args):
return [ return [
{'id': 190, 'list_price': Decimal('25000'), {
'name': 'Producto 1', "id": 190,
'default_uom.': {'id': 1, 'rec_name': 'Unit'}}, "list_price": Decimal("25000"),
{'id': 191, 'list_price': Decimal('6000'), "name": "Producto 1",
'name': 'Panela2', "default_uom.": {"id": 1, "rec_name": "Unit"},
'default_uom.': {'id': 1, 'rec_name': 'Unit'}}, },
{'id': 192, 'list_price': Decimal('4500'), {
'name': 'Papa', "id": 191,
'default_uom.': {'id': 2, 'rec_name': 'Kilogram'}}, "list_price": Decimal("6000"),
"name": "Panela2",
"default_uom.": {"id": 1, "rec_name": "Unit"},
},
{
"id": 192,
"list_price": Decimal("4500"),
"name": "Papa",
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
},
] ]
raise Exception(f"Sorry, args non expected on this test: {args}") raise Exception(
f"Sorry, args non expected on this test: {args}"
)
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
url = '/don_confiao/api/importar_productos_de_tryton' url = "/don_confiao/api/importar_productos_de_tryton"
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
expected_response = { expected_response = {
'checked_tryton_products': [190, 191, 192], "checked_tryton_products": [190, 191, 192],
'created_products': [3], "created_products": [3],
'untouched_products': [2], "untouched_products": [2],
'failed_products': [], "failed_products": [],
'updated_products': [1] "updated_products": [1],
} }
self.assertEqual(content, expected_response) self.assertEqual(content, expected_response)
created_product = Product.objects.get(id=3) created_product = Product.objects.get(id=3)
self.assertEqual(created_product.external_id, str(190)) self.assertEqual(created_product.external_id, str(190))
self.assertEqual(created_product.name, 'Producto 1') self.assertEqual(created_product.name, "Producto 1")
self.assertEqual(created_product.price, Decimal('25000')) self.assertEqual(created_product.price, Decimal("25000"))
self.assertEqual(created_product.measuring_unit, 'Unit') self.assertEqual(created_product.measuring_unit, "Unit")
updated_product = Product.objects.get(id=1) updated_product = Product.objects.get(id=1)
self.assertEqual(updated_product.external_id, str(191)) self.assertEqual(updated_product.external_id, str(191))
self.assertEqual(updated_product.name, 'Panela2') self.assertEqual(updated_product.name, "Panela2")
self.assertEqual(updated_product.price, Decimal('6000')) self.assertEqual(updated_product.price, Decimal("6000"))
self.assertEqual(updated_product.measuring_unit, 'Unit') self.assertEqual(updated_product.measuring_unit, "Unit")
@patch('sabatron_tryton_rpc_client.client.Client.call') @patch("sabatron_tryton_rpc_client.client.Client.call")
@patch('sabatron_tryton_rpc_client.client.Client.connect') @patch("sabatron_tryton_rpc_client.client.Client.connect")
def test_import_duplicated_name_products(self, mock_connect, mock_call): def test_import_duplicated_name_products(
self, mock_connect, mock_call
):
mock_connect.return_value = None mock_connect.return_value = None
def fake_call(*args, **kwargs): def fake_call(*args, **kwargs):
product_search = 'model.product.product.search' product_search = "model.product.product.search"
search_args = [[["salable", "=", True]], 0, 1000, [['rec_name', 'ASC'], ['id', None]], {'company': 1}] search_args = [
if (args == (product_search, search_args)): [["salable", "=", True]],
0,
1000,
[["rec_name", "ASC"], ["id", None]],
{"company": 1},
]
if args == (product_search, search_args):
return [200] return [200]
product_read = 'model.product.product.read' product_read = "model.product.product.read"
product_args = ([200], product_args = (
['id', 'name', 'default_uom.id', [200],
'default_uom.rec_name', 'list_price'], [
{'company': 1} "id",
) "name",
if (args == (product_read, product_args)): "default_uom.id",
"default_uom.rec_name",
"list_price",
],
{"company": 1},
)
if args == (product_read, product_args):
return [ return [
{'id': 200, 'list_price': Decimal('25000'), {
'name': self.product.name, "id": 200,
'default_uom.': {'id': 1, 'rec_name': 'Unit'}}, "list_price": Decimal("25000"),
"name": self.product.name,
"default_uom.": {"id": 1, "rec_name": "Unit"},
},
] ]
raise Exception(f"Sorry, args non expected on this test: {args}") raise Exception(
f"Sorry, args non expected on this test: {args}"
)
mock_call.side_effect = fake_call mock_call.side_effect = fake_call
url = '/don_confiao/api/importar_productos_de_tryton' url = "/don_confiao/api/importar_productos_de_tryton"
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8')) content = json.loads(response.content.decode("utf-8"))
expected_response = { expected_response = {
'checked_tryton_products': [200], "checked_tryton_products": [200],
'created_products': [], "created_products": [],
'untouched_products': [], "untouched_products": [],
'failed_products': [200], "failed_products": [200],
'updated_products': [], "updated_products": [],
} }
self.assertEqual(content, expected_response) self.assertEqual(content, expected_response)

View File

@@ -1,51 +0,0 @@
from django.test import Client, TestCase
from ..models import Payment, Sale, Product, Customer
class TestPurchaseWithPayment(TestCase):
def setUp(self):
self.client = Client()
self.product = Product()
self.product.name = "Arroz"
self.product.price = 5000
self.product.save()
customer = Customer()
customer.name = "Noelba Lopez"
customer.save()
self.customer = customer
def test_generate_payment_when_it_has_payment(self):
quantity = 2
unit_price = 2500
total = 5000
self.client.post(
'/don_confiao/comprar',
{
"customer": str(self.customer.id),
"date": "2024-07-27",
"phone": "3010101000",
"description": "Venta de contado",
"saleline_set-TOTAL_FORMS": "1",
"saleline_set-INITIAL_FORMS": "0",
"saleline_set-MIN_NUM_FORMS": "0",
"saleline_set-MAX_NUM_FORMS": "1000",
"saleline_set-0-product": str(self.product.id),
"saleline_set-0-quantity": str(quantity),
"saleline_set-0-unit_price": str(unit_price),
"saleline_set-0-description": "Linea de Venta",
"saleline_set-0-sale": "",
"saleline_set-0-id": "",
"quantity_lines": "1",
"quantity_products": str(quantity),
"ammount": str(quantity * unit_price),
"payment_method": "CASH",
}
)
purchases = Sale.objects.all()
self.assertEqual(1, len(purchases))
payments = Payment.objects.all()
self.assertEqual(1, len(payments))
self.assertEqual(total, payments[0].amount)
self.assertEqual('CASH', payments[0].type_payment)

View File

@@ -1,55 +0,0 @@
from django.test import TestCase
from ..models import Sale, Product, SaleLine, Customer
from .Mixins import LoginMixin
class TestSummaryViewPurchase(TestCase, LoginMixin):
def setUp(self):
self.login()
customer = Customer()
customer.name = 'Alejo Mono'
customer.save()
purchase = Sale()
purchase.customer = customer
purchase.date = "2024-07-30"
purchase.clean()
purchase.save()
product = Product()
product.name = "cafe"
product.price = "72500"
product.save()
line = SaleLine()
line.sale = purchase
line.product = product
line.quantity = "11"
line.unit_price = "72500"
line.save()
self.purchase = purchase
def test_summary_has_customer(self):
url = "/don_confiao/resumen_compra/" + str(self.purchase.id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.context["purchase"].customer,
self.purchase.customer
)
self.assertIn('Alejo Mono', response.content.decode('utf-8'))
def test_json_summary(self):
url = f"/don_confiao/resumen_compra_json/{self.purchase.id}"
response = self.client.get(url)
content = response.content.decode('utf-8')
self.assertEqual(response.status_code, 200)
self.assertIn('Alejo Mono', content)
self.assertIn('cafe', content)
self.assertIn('72500', content)
self.assertIn('quantity', content)
self.assertIn('11', content)
self.assertIn('date', content)
self.assertIn(self.purchase.date, content)
self.assertIn('lines', content)

View File

@@ -1,7 +1,9 @@
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ..models import Customer, Product, Sale, SaleLine from ..models.customers import Customer
from ..models.products import Product
from ..models.sales import Sale, SaleLine
class ConfiaoTest(TestCase): class ConfiaoTest(TestCase):
@@ -20,7 +22,7 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22 12:05:00" sale.date = "2024-06-22 12:05:00"
sale.phone = '666666666' sale.phone = "666666666"
sale.description = "Description" sale.description = "Description"
sale.save() sale.save()
@@ -30,9 +32,9 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22 12:05:00" sale.date = "2024-06-22 12:05:00"
sale.phone = '666666666' sale.phone = "666666666"
sale.description = "Description" sale.description = "Description"
sale.payment_method = '' sale.payment_method = ""
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
sale.full_clean() sale.full_clean()
@@ -41,7 +43,7 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22"
sale.phone = '666666666' sale.phone = "666666666"
sale.description = "Description" sale.description = "Description"
line = SaleLine() line = SaleLine()
@@ -58,7 +60,7 @@ class ConfiaoTest(TestCase):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22"
sale.phone = '666666666' sale.phone = "666666666"
sale.description = "Description" sale.description = "Description"
line1 = SaleLine() line1 = SaleLine()
@@ -81,15 +83,14 @@ class ConfiaoTest(TestCase):
self.assertEqual(len(SaleLine.objects.all()), 2) self.assertEqual(len(SaleLine.objects.all()), 2)
self.assertEqual( self.assertEqual(
Sale.objects.all()[0].saleline_set.all()[0].quantity, Sale.objects.all()[0].saleline_set.all()[0].quantity, 2
2
) )
def test_allow_sale_without_description(self): def test_allow_sale_without_description(self):
sale = Sale() sale = Sale()
sale.customer = self.customer sale.customer = self.customer
sale.date = "2024-06-22" sale.date = "2024-06-22"
sale.phone = '666666666' sale.phone = "666666666"
sale.description = None sale.description = None
sale.save() sale.save()

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env python3
from django.test import TestCase
from ..forms import PurchaseForm
from ..models import Customer
_csrf_token = \
"bVjBevJRavxRPFOlVgAWiyh9ceuiwPlyEcmbPZprNuCGHjFZRKZrBeunJvKTRgOx"
class PurchaseFormTest(TestCase):
def setUp(self):
self.customer = Customer()
self.customer.name = "Don Confiao Gonzalez"
self.customer.address = "Patio Bonito"
self.customer.save()
def test_add_purchase(self):
form_data = {
"csrfmiddlewaretoken": _csrf_token,
"customer": self.customer.id,
"date": "2024-08-03",
"payment_method": "CASH",
"phone": "sfasfd",
"description": "dasdadad",
"saleline_set-TOTAL_FORMS": "1",
"saleline_set-INITIAL_FORMS": "0",
"saleline_set-MIN_NUM_FORMS": "0",
"saleline_set-MAX_NUM_FORMS": "1000",
"saleline_set-0-product": "5",
"saleline_set-0-quantity": "1",
"saleline_set-0-unit_price": "500",
"saleline_set-0-description": "afasdfasdf",
"saleline_set-0-sale": "",
"saleline_set-0-id": "",
"quantity_lines": "1",
"quantity_products": "1",
"ammount": "500",
"form": ""
}
purchase_form = PurchaseForm(data=form_data)
purchase_form.is_valid()
# raise Exception(purchase_form)
self.assertTrue(purchase_form.is_valid())

View File

@@ -4,39 +4,55 @@ from rest_framework.routers import DefaultRouter
from . import views from . import views
from . import api_views from . import api_views
app_name = 'don_confiao' app_name = "don_confiao"
router = DefaultRouter() router = DefaultRouter()
router.register(r'sales', api_views.SaleView, basename='sale') router.register(r"sales", api_views.SaleView, basename="sale")
router.register(r'customers', api_views.CustomerView, basename='customer') router.register(r"customers", api_views.CustomerView, basename="customer")
router.register(r'products', api_views.ProductView, basename='product') router.register(r"products", api_views.ProductView, basename="product")
router.register(r'reconciliate_jar', api_views.ReconciliateJarModelView, router.register(
basename='reconciliate_jar') r"reconciliate_jar",
api_views.ReconciliateJarModelView,
basename="reconciliate_jar",
)
urlpatterns = [ urlpatterns = [
path("", views.index, name="wellcome"),
path("comprar", views.buy, name="buy"),
path("compras", views.purchases, name="purchases"),
path("productos", views.products, name="products"), path("productos", views.products, name="products"),
path("lista_productos", views.ProductListView.as_view(), name='product_list'), path(
path("importar_productos", views.import_products, name="import_products"), "resumen_compra_json/<int:id>",
path('api/importar_productos_de_tryton', api_views.SaleSummary.as_view(),
api_views.ProductsFromTrytonView.as_view(), name="purchase_json_summary",
name="products_from_tryton"), ),
path("importar_terceros", views.import_customers, name="import_customers"), path("api/", include(router.urls)),
path('api/importar_clientes_de_tryton', path(
api_views.CustomersFromTrytonView.as_view(), "api/importar_productos_de_tryton",
name="customers_from_tryton"), api_views.ProductsFromTrytonView.as_view(),
path("exportar_ventas_para_tryton", name="products_from_tryton",
views.exportar_ventas_para_tryton, ),
name="exportar_ventas_para_tryton"), path(
path('api/enviar_ventas_a_tryton', api_views.SalesToTrytonView.as_view(), name="send_tryton"), "api/importar_clientes_de_tryton",
path("resumen_compra/<int:id>", views.purchase_summary, name="purchase_summary"), api_views.CustomersFromTrytonView.as_view(),
path("resumen_compra_json/<int:id>", api_views.SaleSummary.as_view(), name="purchase_json_summary"), name="customers_from_tryton",
path("payment_methods/all/select_format", api_views.PaymentMethodView.as_view(), name="payment_methods_to_select"), ),
path('purchases/for_reconciliation', api_views.SalesForReconciliationView.as_view(), name='sales_for_reconciliation'), path(
path('reconciliate_jar', api_views.ReconciliateJarView.as_view()), "api/enviar_ventas_a_tryton",
path('api/admin_code/validate/<code>', api_views.AdminCodeValidateView.as_view()), api_views.SalesToTrytonView.as_view(),
path('api/sales/for_tryton', api_views.SalesForTrytonView.as_view()), name="send_tryton",
path('api/', include(router.urls)), ),
path(
"payment_methods/all/select_format",
api_views.PaymentMethodView.as_view(),
name="payment_methods_to_select",
),
path(
"purchases/for_reconciliation",
api_views.SalesForReconciliationView.as_view(),
name="sales_for_reconciliation",
),
path("reconciliate_jar", api_views.ReconciliateJarView.as_view()),
path(
"api/admin_code/validate/<code>",
api_views.AdminCodeValidateView.as_view(),
),
path("api/sales/for_tryton", api_views.SalesForTrytonView.as_view()),
] ]

View File

@@ -3,14 +3,11 @@ from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.views.generic import ListView from django.views.generic import ListView
from django.db import transaction from django.db import transaction
from .models import ( from .models.sales import Sale, SaleLine
Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods, ReconciliationJar) from .models.customers import Customer
from .forms import ( from .models.sales import Sale, SaleLine, Payment
ImportProductsForm, from .models.products import Product, ProductCategory
ImportCustomersForm, from .models.payments import PaymentMethods, ReconciliationJar
PurchaseForm,
SaleLineFormSet,
PurchaseSummaryForm)
import csv import csv
import io import io
@@ -26,105 +23,13 @@ class DecimalEncoder(json.JSONEncoder):
def index(request): def index(request):
return render(request, 'don_confiao/index.html') return render(request, "don_confiao/index.html")
def buy(request):
if request.method == "POST":
sale_form = PurchaseForm(request.POST)
line_formset = SaleLineFormSet(request.POST)
sale_summary_form = PurchaseSummaryForm(request.POST)
forms_are_valid = all([
sale_form.is_valid(),
line_formset.is_valid(),
sale_summary_form.is_valid()
])
payment_method = request.POST.get('payment_method')
valid_payment_methods = [PaymentMethods.CASH]
valid_payment_method = payment_method in valid_payment_methods
if forms_are_valid:
with transaction.atomic():
sale = sale_form.save()
line_formset.instance = sale
line_formset.save()
Payment.total_payment_from_sale(
payment_method,
sale
)
return HttpResponseRedirect("compras")
else:
sale_form = PurchaseForm()
line_formset = SaleLineFormSet()
sale_summary_form = PurchaseSummaryForm()
return render(
request,
'don_confiao/purchase.html',
{
'sale_form': sale_form,
'linea_formset': line_formset,
'summary_form': sale_summary_form,
'list_products': json.dumps(Product.to_list(), cls=DecimalEncoder),
}
)
def purchases(request):
purchases = Sale.objects.all()
context = {
"purchases": purchases,
}
return render(request, 'don_confiao/purchases.html', context)
def products(request): def products(request):
return JsonResponse(Product.to_list(), safe=False) return JsonResponse(Product.to_list(), safe=False)
def import_products(request):
if request.method == "POST":
form = ImportProductsForm(request.POST, request.FILES)
if form.is_valid():
handle_import_products_file(request.FILES["csv_file"])
return HttpResponseRedirect("productos")
else:
form = ImportProductsForm()
return render(
request,
"don_confiao/import_products.html",
{'form': form}
)
def import_customers(request):
if request.method == "POST":
form = ImportCustomersForm(request.POST, request.FILES)
if form.is_valid():
handle_import_customers_file(request.FILES["csv_file"])
return HttpResponseRedirect("productos")
else:
form = ImportCustomersForm()
return render(
request,
"don_confiao/import_customers.html",
{'form': form}
)
def reconciliations(request):
return HttpResponse('<h1>Reconciliaciones</h1>')
def purchase_summary(request, id):
purchase = Sale.objects.get(pk=id)
return render(
request,
"don_confiao/purchase_summary.html",
{
"purchase": purchase
}
)
def _categories_from_csv_string(categories_string, separator="&"): def _categories_from_csv_string(categories_string, separator="&"):
categories = categories_string.split(separator) categories = categories_string.split(separator)
clean_categories = [c.strip() for c in categories] clean_categories = [c.strip() for c in categories]
@@ -136,33 +41,32 @@ def _category_from_name(name):
def handle_import_products_file(csv_file): def handle_import_products_file(csv_file):
data = io.StringIO(csv_file.read().decode('utf-8')) data = io.StringIO(csv_file.read().decode("utf-8"))
reader = csv.DictReader(data, quotechar='"') reader = csv.DictReader(data, quotechar='"')
for row in reader: for row in reader:
product, created = Product.objects.update_or_create( product, created = Product.objects.update_or_create(
name=row['producto'], name=row["producto"],
defaults={ defaults={
'price': row['precio'], "price": row["precio"],
'measuring_unit': row['unidad'] "measuring_unit": row["unidad"],
} },
) )
categories = _categories_from_csv_string(row["categorias"]) categories = _categories_from_csv_string(row["categorias"])
product.categories.clear() product.categories.clear()
for category in categories: for category in categories:
product.categories.add(category) product.categories.add(category)
def handle_import_customers_file(csv_file): def handle_import_customers_file(csv_file):
data = io.StringIO(csv_file.read().decode('utf-8')) data = io.StringIO(csv_file.read().decode("utf-8"))
reader = csv.DictReader(data, quotechar='"') reader = csv.DictReader(data, quotechar='"')
for row in reader: for row in reader:
customer, created = Customer.objects.update_or_create( customer, created = Customer.objects.update_or_create(
name=row['nombre'], name=row["nombre"],
defaults={ defaults={"email": row["correo"], "phone": row["telefono"]},
'email': row['correo'],
'phone': row['telefono']
}
) )
def sales_to_tryton_csv(sales): def sales_to_tryton_csv(sales):
tryton_sales_header = [ tryton_sales_header = [
"Tercero", "Tercero",
@@ -182,7 +86,7 @@ def sales_to_tryton_csv(sales):
"Tienda", "Tienda",
"Terminal de venta", "Terminal de venta",
"Autorecogida", "Autorecogida",
"Comentario" "Comentario",
] ]
csv_data = [tryton_sales_header] csv_data = [tryton_sales_header]
@@ -194,7 +98,7 @@ def sales_to_tryton_csv(sales):
first_sale_line = sale_lines[0] first_sale_line = sale_lines[0]
customer_info = [sale.customer.name] * 3 + [sale.description] * 2 customer_info = [sale.customer.name] * 3 + [sale.description] * 2
first_line = customer_info + [ first_line = customer_info + [
sale.date.strftime('%Y-%m-%d'), sale.date.strftime("%Y-%m-%d"),
"Contado", "Contado",
"Almacén", "Almacén",
"Peso colombiano", "Peso colombiano",
@@ -206,39 +110,21 @@ def sales_to_tryton_csv(sales):
"Tienda La Ilusion", "Tienda La Ilusion",
"La Ilusion", "La Ilusion",
True, True,
sale.description] sale.description,
]
lines.append(first_line) lines.append(first_line)
for line in sale_lines[1:]: for line in sale_lines[1:]:
lines.append([""]*9+[ lines.append(
line.product.name, [""] * 9
line.quantity, + [
line.unit_price, line.product.name,
"Unidad"]+[""]*5) line.quantity,
line.unit_price,
"Unidad",
]
+ [""] * 5
)
for row in lines: for row in lines:
csv_data.append(row) csv_data.append(row)
return csv_data return csv_data
def exportar_ventas_para_tryton(request):
if request.method == "GET":
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = "attachment; filename=sales.csv"
sales = Sale.objects.all()
csv_data = sales_to_tryton_csv(sales)
writer = csv.writer(response)
for row in csv_data:
writer.writerow(row)
return response
class ProductListView(ListView):
model = Product
template_model = 'don_confiao/product_list.html'
def get_queryset(self):
name = self.request.GET.get('name')
if name:
return Product.objects.filter(name__icontains=name)
return Product.objects.all()