21 Commits

Author SHA1 Message Date
4812160ea2 Merge pull request 'Se adiciona autenticación a django' (#30) from add_authentication_#29 into main
Reviewed-on: #30
2025-12-13 17:35:34 -05:00
fb4c82a94c #29 refactor(Tests): extract to mixin class. 2025-12-13 17:35:20 -05:00
fb3124246c #29 refactor(Tests): extract to mixin class. 2025-12-13 17:34:40 -05:00
f3d3681bc4 #29 fix(Tests): add auth to tests. 2025-12-13 17:10:00 -05:00
e6d2160d2e #29 feat(Auth): add logout and profile. 2025-12-13 16:31:20 -05:00
f323873d80 #29 feat(Auth): add logout and profile. 2025-12-13 16:30:53 -05:00
b730d24855 #29 feat(Auth): add login. 2025-12-13 12:24:35 -05:00
6261d64206 Merge pull request '#27 fix: reconciliation jar.' (#28) from test_decimal_error_jar_#27 into main
Reviewed-on: #28
2025-11-15 17:51:31 -05:00
64f07a2ce2 #27 fix: reconciliation jar. 2025-11-15 17:50:06 -05:00
308e2d08c1 Merge pull request 'feat(tryton): add comment to tryton sale.' (#26) from add_payment_method_to_tryton_sale_on_notes_field_#21 into main
Reviewed-on: #26
2025-11-08 15:38:44 -05:00
e1ff427856 feat(tryton): add comment to tryton sale. 2025-11-08 15:34:45 -05:00
bf70c47551 Merge pull request 'Se adiciona el método de pago en la descripción de la venta de tryton #21' (#25) from add_payment_method_to_tryton_sale_on_notes_field_#21 into main
Reviewed-on: #25
2025-09-05 20:26:10 -05:00
f02f754ae7 feat(tryton): add payment method to tryton sale description. 2025-09-05 20:24:13 -05:00
1668a37091 Merge pull request 'Adicionando autorecogida a las ventas que se envian a tryton #23' (#24) from add_self_pick_up_to_sales_in_tryton_#23 into main
Reviewed-on: #24
2025-09-05 19:24:54 -05:00
b33937d4a5 feat(Tryton): add self_pick_up in send sale to tryton. 2025-09-05 19:15:26 -05:00
a265b94460 Merge pull request 'Enviando ventas a Tryton así alguna de las ventas falle #16' (#19) from send_sales_to_tryton_in_a_no_bloqueant_way_#16 into main
Reviewed-on: #19
2025-08-30 15:48:26 -05:00
253fcbae27 fix(Tryton): add try except at send sales to tryton. #16 2025-08-30 15:45:13 -05:00
d127609508 Merge pull request 'Adicionando external id en venta y customers en la API #17' (#18) from add_external_id_in_api_sale_fields_#17 into main
Reviewed-on: #18
2025-08-30 15:32:57 -05:00
604bbd3ab9 #17 feat(API): add external id to sales on api. 2025-08-30 15:28:50 -05:00
e17b8f6973 style. 2025-08-30 15:25:33 -05:00
e3f571afc5 #17 feat(API): add external id to customers on api. 2025-08-30 15:24:42 -05:00
17 changed files with 225 additions and 45 deletions

View File

@@ -38,10 +38,12 @@ class SaleView(viewsets.ModelViewSet):
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', '')
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
) )
for line in lines: for line in lines:
@@ -97,7 +99,8 @@ class ReconciliateJarView(APIView):
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 calculated_total == Decimal(total) return Decimal(calculated_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:
@@ -194,12 +197,18 @@ class SalesToTrytonView(APIView):
sales = Sale.objects.filter(external_id=None) sales = Sale.objects.filter(external_id=None)
for sale in sales: for sale in sales:
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:
print(f"Error al enviar la venta: {e}"
f"venta_id: {sale.id}")
failed.append(sale.id)
continue
return Response( return Response(
{'successful': successful, 'failed': failed}, {'successful': successful, 'failed': failed},
@@ -227,14 +236,18 @@ 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,
"description": self.sale.description or '', "comment": self.sale.description or '',
"description": "Metodo pago: " + 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", "create",
[TrytonLineSale(line).to_tryton() for line in self.lines] [TrytonLineSale(line).to_tryton() for line in self.lines]
]] ]],
"self_pick_up": True,
} }

View File

@@ -15,7 +15,7 @@ class SaleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Sale model = Sale
fields = ['id', 'customer', 'date', 'saleline_set', fields = ['id', 'customer', 'date', 'saleline_set',
'total', 'payment_method'] 'total', 'payment_method', 'external_id']
class ProductSerializer(serializers.ModelSerializer): class ProductSerializer(serializers.ModelSerializer):
@@ -27,7 +27,7 @@ class ProductSerializer(serializers.ModelSerializer):
class CustomerSerializer(serializers.ModelSerializer): class CustomerSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Customer model = Customer
fields = ['id', 'name', 'address', 'email', 'phone'] fields = ['id', 'name', 'address', 'email', 'phone', 'external_id']
class ReconciliationJarSerializer(serializers.ModelSerializer): class ReconciliationJarSerializer(serializers.ModelSerializer):

View File

@@ -0,0 +1,10 @@
from django.contrib.auth.models import User
class LoginMixin:
def login(self):
username = 'nombre_usuario'
password = 'contraseña'
email = 'correo@example.com'
self.user = User.objects.create_user(username, email, password)
self.client.login(username=username, password=password)

View File

@@ -1,20 +1,21 @@
from django.test import TestCase, Client from django.test import TestCase
from ..models import AdminCode from ..models import AdminCode
from .Mixins import LoginMixin
import json import json
class TestAdminCode(TestCase): class TestAdminCode(TestCase, LoginMixin):
def setUp(self): def setUp(self):
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()
self.client = Client()
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)

View File

@@ -2,21 +2,24 @@ import json
import csv import csv
import io import io
from django.urls import reverse
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 import Sale, Product, Customer
from .Mixins import LoginMixin
class TestAPI(APITestCase): class TestAPI(APITestCase, LoginMixin):
def setUp(self): def setUp(self):
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'
) )
self.customer = Customer.objects.create( self.customer = Customer.objects.create(
name='Camilo' name='Camilo',
external_id='18'
) )
def test_create_sale(self): def test_create_sale(self):
@@ -67,6 +70,23 @@ class TestAPI(APITestCase):
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.customer.external_id,
json_response[0]['external_id']
)
def test_get_sales(self):
url = '/don_confiao/api/sales/'
self._create_sale()
response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(self.customer.id, json_response[0]['customer'])
self.assertEqual(
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'

View File

@@ -1,12 +1,15 @@
import json import json
from unittest.mock import patch from unittest.mock import patch
from django.test import Client, TestCase from django.test import TestCase
from ..models import Customer from ..models import Customer
from .Mixins import LoginMixin
class TestCustomersFromTryton(TestCase): class TestCustomersFromTryton(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.login()
self.customer = Customer.objects.create( self.customer = Customer.objects.create(
name='Calos', name='Calos',
external_id=5 external_id=5

View File

@@ -2,13 +2,16 @@ import csv
import json import json
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase, Client from django.test import TestCase
from django.urls import reverse
from ..models import Sale, SaleLine, Product, Customer from ..models import Sale, SaleLine, Product, Customer
from .Mixins import LoginMixin
class TestExportarVentasParaTryton(TestCase):
class TestExportarVentasParaTryton(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.login()
self.product = Product.objects.create( self.product = Product.objects.create(
name='Panela', name='Panela',
price=5000, price=5000,
@@ -25,6 +28,7 @@ class TestExportarVentasParaTryton(TestCase):
customer=self.customer, customer=self.customer,
date='2024-09-02', date='2024-09-02',
payment_method='CASH', payment_method='CASH',
description='un comentario'
) )
self.sale_line1 = SaleLine.objects.create( self.sale_line1 = SaleLine.objects.create(
product=self.product, product=self.product,
@@ -40,9 +44,8 @@ class TestExportarVentasParaTryton(TestCase):
) )
def test_exportar_ventas_para_tryton(self): def test_exportar_ventas_para_tryton(self):
client = Client()
url = '/don_confiao/exportar_ventas_para_tryton' url = '/don_confiao/exportar_ventas_para_tryton'
response = client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'text/csv') self.assertEqual(response['Content-Type'], 'text/csv')
csv_content = response.content.decode('utf-8') csv_content = response.content.decode('utf-8')
@@ -71,7 +74,7 @@ class TestExportarVentasParaTryton(TestCase):
self.assertEqual(next(csv_reader), expected_header) self.assertEqual(next(csv_reader), expected_header)
expected_rows = [ expected_rows = [
["Camilo", "Camilo", "Camilo", "", "", "2024-09-02", "Contado", "Almacén", "Peso colombiano", "Panela", "2.00", "3000.00", "Unidad", "TIENDA LA ILUSIÓN", "Tienda La Ilusion", "La Ilusion", "True", ""], ["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", "", "", "", "", ""], ["", "", "", "", "", "", "", "", "", "Panela", "3.00", "5000.00", "Unidad", "", "", "", "", ""],
] ]
csv_rows = list(csv_reader) csv_rows = list(csv_reader)
@@ -81,12 +84,11 @@ class TestExportarVentasParaTryton(TestCase):
@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_send_sales_to_tryton(self, mock_connect, mock_call): def test_send_sales_to_tryton(self, mock_connect, mock_call):
client = Client()
external_id = '23423' external_id = '23423'
url = '/don_confiao/api/enviar_ventas_a_tryton' url = '/don_confiao/api/enviar_ventas_a_tryton'
mock_connect.return_value = None mock_connect.return_value = None
mock_call.return_value = [external_id] mock_call.return_value = [external_id]
response = 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'))
@@ -100,4 +102,4 @@ class TestExportarVentasParaTryton(TestCase):
self.assertEqual(updated_sale.external_id, external_id) self.assertEqual(updated_sale.external_id, external_id)
mock_connect.assert_called_once() mock_connect.assert_called_once()
mock_call.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, 'description': '', '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'}}]]]}], {'company': 1, 'shops': [1]}]) 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,18 +1,19 @@
from django.test import TestCase, Client 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 import Sale, Product, SaleLine, Customer, ReconciliationJar
from .Mixins import LoginMixin
import json import json
class TestJarReconcliation(TestCase): class TestJarReconcliation(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.login()
customer = Customer() customer = Customer()
customer.name = 'Alejo Mono' customer.name = 'Alejo Mono'
customer.save() customer.save()
self.client = Client()
purchase = Sale() purchase = Sale()
purchase.customer = customer purchase.customer = customer
purchase.date = "2024-07-30" purchase.date = "2024-07-30"
@@ -235,6 +236,47 @@ class TestJarReconcliation(TestCase):
[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):
customer = Customer()
customer.name = 'Consumidor final'
customer.save()
product = Product()
product.name = "Mantequilla natural gramos"
product.price = "57.50"
product.save()
purchase = Sale()
purchase.customer = customer
purchase.date = "2024-07-30"
purchase.payment_method = 'CASH'
purchase.clean()
purchase.save()
line = SaleLine()
line.sale = purchase
line.product = product
line.quantity = "0.24"
line.unit_price = "57.50"
line.save()
url = '/don_confiao/reconciliate_jar'
total_purchases = 13.80
data = {
'date_time': '2024-12-02T21:07',
'reconcilier': 'carlos',
'total_cash_purchases': total_purchases,
'cash_taken': total_purchases,
'cash_discrepancy': 0,
'cash_purchases': [purchase.id],
}
response = self.client.post(
url, data=json.dumps(data).encode('utf-8'),
content_type='application/json'
)
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"

View File

@@ -1,10 +1,10 @@
from django.test import Client, TestCase from django.test import TestCase
from .Mixins import LoginMixin
# from ..models import PaymentMethods
class TestPaymentMethods(TestCase): class TestPaymentMethods(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.client = Client() self.login()
def test_keys_in_payment_methods_to_select(self): def test_keys_in_payment_methods_to_select(self):
response = self.client.get( response = self.client.get(

View File

@@ -2,12 +2,15 @@ import json
from decimal import Decimal from decimal import Decimal
from unittest.mock import patch from unittest.mock import patch
from django.test import Client, TestCase from django.test import TestCase
from ..models import ProductCategory, Product from ..models import Product
from .Mixins import LoginMixin
class TestProductsFromTryton(TestCase): class TestProductsFromTryton(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.login()
self.product = Product.objects.create( self.product = Product.objects.create(
name='Panela', name='Panela',
price=5000, price=5000,

View File

@@ -1,14 +1,16 @@
from django.test import TestCase, Client from django.test import TestCase
from ..models import Sale, Product, SaleLine, Customer from ..models import Sale, Product, SaleLine, Customer
from .Mixins import LoginMixin
class TestSummaryViewPurchase(TestCase): class TestSummaryViewPurchase(TestCase, LoginMixin):
def setUp(self): def setUp(self):
self.login()
customer = Customer() customer = Customer()
customer.name = 'Alejo Mono' customer.name = 'Alejo Mono'
customer.save() customer.save()
self.client = Client()
purchase = Sale() purchase = Sale()
purchase.customer = customer purchase.customer = customer
purchase.date = "2024-07-30" purchase.date = "2024-07-30"

View File

@@ -65,7 +65,7 @@ ROOT_URLCONF = 'tienda_ilusion.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [os.path.join(BASE_DIR, 'tienda_ilusion/templates')],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@@ -134,3 +134,14 @@ STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
FIXTURE_DIRS = ['don_confiao/tests/Fixtures'] FIXTURE_DIRS = ['don_confiao/tests/Fixtures']
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
LOGOUT_REDIRECT_URL = '/start/'

View File

@@ -0,0 +1,5 @@
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Iniciar sesión</button>
</form>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Perfil de usuario</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<style>
body {
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1>Perfil de usuario</h1>
<p>Nombre de usuario: {{ user.username }}</p>
<p>Email: {{ user.email }}</p>
<form action="{% url 'logout' %}" method="post" style="display: inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Cerrar sesión</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Bienvenido a la tienda la ilusión</title>
<style>
body {
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1>Bienvenido a la tienda la ilusion</h1>
<a href="{% url 'login' %}">Login</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -16,11 +16,15 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from . import views
app_name = "don_confiao" app_name = "don_confiao"
urlpatterns = [ urlpatterns = [
path("don_confiao/", include("don_confiao.urls")), path("don_confiao/", include("don_confiao.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path('accounts/profile/', views.ProfileView.as_view(), name='profile'),
path('start/', views.StartView.as_view(), name='start'),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]

View File

@@ -0,0 +1,15 @@
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'registration/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.request.user
return context
class StartView(TemplateView):
template_name = 'start.html'