21 Commits

Author SHA1 Message Date
1e16e6e983 feat: Add exernal_id to serializer issue #34 2026-03-14 18:07:44 -05:00
648207192c doc: products endpoint 2026-03-14 18:06:38 -05:00
a05061c14e Merge pull request 'Cambiando autenticación de api a JWT #29' (#33) from add_jwt_authentication_#29 into main
Reviewed-on: #33
2026-02-14 15:57:03 -05:00
7c0047b4d3 fix: rm duplicate file. 2026-02-14 15:55:59 -05:00
c021104b62 doc(API): add sample requests. 2026-02-14 15:50:30 -05:00
7a9034943a feat(API): change to jwt authentication. 2026-02-14 15:12:32 -05:00
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
24 changed files with 316 additions and 39 deletions

78
doc/requests.org Normal file
View File

@@ -0,0 +1,78 @@
* Requests
Ejemplo de request contra la api usando [[https://github.com/federicotdn/verb][verb]]
** Autenticación :verb:
template http://localhost:7000/api
Content-Type: application/json;
*** Solicitar token
post /token/
{
"username": "admin",
"password": "123"
}
**** respuesta
#+begin_src json
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc"
}
#+end_src
*** Perfil de usuario
get /users/me/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc
**** Respuesta
#+begin_src json
{
"id": 2,
"username": "admin",
"email": "correo@example.com",
"first_name": "",
"last_name": ""
}
#+end_src
*** Renovar token
post /token/refresh/
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k"
}
**** response
#+begin_src json
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA"
}
#+end_src
** Don confiao :verb:
template http://localhost:7000/don_confiao/api/
Content-Type: application/json;
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA
*** todas las rutas
get
**** response
#+begin_src json
{
"sales": "http://localhost:7000/don_confiao/api/sales/",
"customers": "http://localhost:7000/don_confiao/api/customers/",
"products": "http://localhost:7000/don_confiao/api/products/",
"reconciliate_jar": "http://localhost:7000/don_confiao/api/reconciliate_jar/"
}
#+end_src
*** customers
get customers/
**** response
#+begin_src json
[
{
"id": 1,
"name": "Consumidor Final",
"address": "",
"email": "",
"phone": "",
"external_id": "2753"
},
...
]
#+end_src
*** products
get products/

View File

@@ -1,4 +1,5 @@
Django==5.0.6
djangorestframework
django-cors-headers
djangorestframework-simplejwt
sabatron-tryton-rpc-client==7.4.0

View File

@@ -38,10 +38,12 @@ class SaleView(viewsets.ModelViewSet):
date = data['date']
lines = data['saleline_set']
payment_method = data['payment_method']
description = data.get('notes', '')
sale = Sale.objects.create(
customer=customer,
date=date,
payment_method=payment_method
payment_method=payment_method,
description=description
)
for line in lines:
@@ -97,7 +99,8 @@ class ReconciliateJarView(APIView):
def _is_valid_total(self, purchases, total):
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):
if not other_totals:
@@ -233,14 +236,18 @@ class TrytonSale:
"shipment_address": self.sale.customer.address_external_id,
"invoice_address": self.sale.customer.address_external_id,
"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,
"reference": "don_confiao " + str(self.sale.id),
"sale_date": self._format_date(self.sale.date),
"lines": [[
"create",
[TrytonLineSale(line).to_tryton() for line in self.lines]
]]
]],
"self_pick_up": True,
}

View File

@@ -21,7 +21,7 @@ class SaleSerializer(serializers.ModelSerializer):
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'name', 'price', 'measuring_unit', 'categories']
fields = ['id', 'name', 'price', 'measuring_unit', 'categories', 'external_id']
class CustomerSerializer(serializers.ModelSerializer):

View File

@@ -0,0 +1,19 @@
from django.contrib.auth.models import User
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.test import APIClient
class LoginMixin:
def login(self):
self.user = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='adminpass'
)
refresh = RefreshToken.for_user(self.user)
self.access_token = str(refresh.access_token)
self.client = APIClient()
self.client.credentials(
HTTP_AUTHORIZATION=f'Bearer {self.access_token}')

View File

@@ -1,20 +1,21 @@
from django.test import TestCase, Client
from django.test import TestCase
from ..models import AdminCode
from .Mixins import LoginMixin
import json
class TestAdminCode(TestCase):
class TestAdminCode(TestCase, LoginMixin):
def setUp(self):
self.login()
self.valid_code = 'some valid code'
admin_code = AdminCode()
admin_code.value = self.valid_code
admin_code.clean()
admin_code.save()
self.client = Client()
def test_validate_code(self):
url = '/don_confiao/api/admin_code/validate/' + self.valid_code
response = self.client.get(url)

View File

@@ -2,14 +2,16 @@ import json
import csv
import io
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from ..models import Sale, Product, Customer
from .Mixins import LoginMixin
class TestAPI(APITestCase):
class TestAPI(APITestCase, LoginMixin):
def setUp(self):
self.login()
self.product = Product.objects.create(
name='Panela',
price=5000,

View File

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

View File

@@ -2,13 +2,16 @@ import csv
import json
from unittest.mock import patch
from django.test import TestCase, Client
from django.urls import reverse
from django.test import TestCase
from ..models import Sale, SaleLine, Product, Customer
from .Mixins import LoginMixin
class TestExportarVentasParaTryton(TestCase):
class TestExportarVentasParaTryton(TestCase, LoginMixin):
def setUp(self):
self.login()
self.product = Product.objects.create(
name='Panela',
price=5000,
@@ -25,6 +28,7 @@ class TestExportarVentasParaTryton(TestCase):
customer=self.customer,
date='2024-09-02',
payment_method='CASH',
description='un comentario'
)
self.sale_line1 = SaleLine.objects.create(
product=self.product,
@@ -40,9 +44,8 @@ class TestExportarVentasParaTryton(TestCase):
)
def test_exportar_ventas_para_tryton(self):
client = Client()
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['Content-Type'], 'text/csv')
csv_content = response.content.decode('utf-8')
@@ -71,7 +74,7 @@ class TestExportarVentasParaTryton(TestCase):
self.assertEqual(next(csv_reader), expected_header)
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", "", "", "", "", ""],
]
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.connect')
def test_send_sales_to_tryton(self, mock_connect, mock_call):
client = Client()
external_id = '23423'
url = '/don_confiao/api/enviar_ventas_a_tryton'
mock_connect.return_value = None
mock_call.return_value = [external_id]
response = client.post(url)
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
@@ -100,4 +102,4 @@ class TestExportarVentasParaTryton(TestCase):
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, '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 ..models import Sale, Product, SaleLine, Customer, ReconciliationJar
from .Mixins import LoginMixin
import json
class TestJarReconcliation(TestCase):
class TestJarReconcliation(TestCase, LoginMixin):
def setUp(self):
self.login()
customer = Customer()
customer.name = 'Alejo Mono'
customer.save()
self.client = Client()
purchase = Sale()
purchase.customer = customer
purchase.date = "2024-07-30"
@@ -235,6 +236,47 @@ class TestJarReconcliation(TestCase):
[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):
reconciliation = ReconciliationJar()
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):
self.client = Client()
self.login()
def test_keys_in_payment_methods_to_select(self):
response = self.client.get(

View File

@@ -2,12 +2,15 @@ import json
from decimal import Decimal
from unittest.mock import patch
from django.test import Client, TestCase
from ..models import ProductCategory, Product
from django.test import TestCase
from ..models import Product
from .Mixins import LoginMixin
class TestProductsFromTryton(TestCase):
class TestProductsFromTryton(TestCase, LoginMixin):
def setUp(self):
self.login()
self.product = Product.objects.create(
name='Panela',
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 .Mixins import LoginMixin
class TestSummaryViewPurchase(TestCase):
class TestSummaryViewPurchase(TestCase, LoginMixin):
def setUp(self):
self.login()
customer = Customer()
customer.name = 'Alejo Mono'
customer.save()
self.client = Client()
purchase = Sale()
purchase.customer = customer
purchase.date = "2024-07-30"

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -44,7 +45,9 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
'users',
# 'don_confiao'
]
@@ -57,7 +60,6 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
]
ROOT_URLCONF = 'tienda_ilusion.urls'
@@ -65,7 +67,7 @@ ROOT_URLCONF = 'tienda_ilusion.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [os.path.join(BASE_DIR, 'tienda_ilusion/templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -134,3 +136,22 @@ STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
FIXTURE_DIRS = ['don_confiao/tests/Fixtures']
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"AUTH_HEADER_TYPES": ("Bearer",),
}
# CORS_ALLOWED_ORIGINS = [
# "http://localhost:5173",
# ]

View File

@@ -16,11 +16,19 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import include, path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
app_name = "don_confiao"
urlpatterns = [
path("don_confiao/", include("don_confiao.urls")),
path('admin/', admin.site.urls),
path('api/token/', TokenObtainPairView.as_view(),
name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(),
name='token_refresh'),
path('api/users/', include('users.urls')),
]

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,8 @@
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name')

View File

@@ -0,0 +1,51 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
class MeEndpointTests(TestCase):
def setUp(self):
self.user = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='adminpass'
)
refresh = RefreshToken.for_user(self.user)
self.access_token = str(refresh.access_token)
self.client = APIClient()
self.client.credentials(
HTTP_AUTHORIZATION=f'Bearer {self.access_token}')
def test_me_endpoint_returns_correct_user_data(self):
"""
Verifica que GET /api/users/me/ devuelve los datos del usuario
autenticado.
"""
url = reverse('current-user')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected_fields = {'id', 'username', 'email',
'first_name', 'last_name'}
self.assertTrue(expected_fields.issubset(response.json().keys()))
data = response.json()
self.assertEqual(data['username'], self.user.username)
self.assertEqual(data['email'], self.user.email)
self.assertEqual(data['first_name'], self.user.first_name)
self.assertEqual(data['last_name'], self.user.last_name)
def test_me_endpoint_requires_authentication(self):
"""
Sin token el endpoint debe devolver 401 Unauthorized.
"""
client_no_auth = APIClient()
url = reverse('current-user')
response = client_no_auth.get(url)
self.assertEqual(response.status_code, 401)

View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import CurrentUserView
urlpatterns = [
path('me/', CurrentUserView.as_view(), name='current-user'),
]

View File

@@ -0,0 +1,12 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .serializers import UserSerializer
class CurrentUserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)