42 Commits
0.1 ... 0.1.7

Author SHA1 Message Date
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
477405a094 Merge pull request 'agregada direccion de envio y facturación al enviar las compras a tryton #14' (#15) from add_shipment_address_to_tryton_sale_#14 into main
Reviewed-on: #15
2025-08-16 16:37:46 -05:00
4dae669397 #14 fix(Tryton): add address on updated customers from tryton. 2025-08-16 16:36:06 -05:00
937fe06de4 #14 feat(Tryton): add address on update customers from tryton. 2025-08-16 16:09:20 -05:00
69185f2460 fix(Tryton Shop): add shops. 2025-08-16 12:24:05 -05:00
7ac28154eb Merge branch 'handle_duplicate_product_name_from_tryton_#11' 2025-08-16 10:32:32 -05:00
e7eda79c69 Merge branch 'main' into handle_duplicate_product_name_from_tryton_#11 2025-08-16 10:28:28 -05:00
5f40b4098c feat(Tryton): handle duplicate named products from tryton. 2025-08-16 10:27:17 -05:00
rodia
80864137b6 Merge branch 'main' of https://gitea.onecluster.org/OneTeam/don_confiao_backend 2025-08-16 12:10:27 -03:00
rodia
2e8e956b69 feat: Add env_example 2025-08-16 12:09:41 -03:00
2e4c6592a3 fix test. 2025-08-16 09:55:20 -05:00
6b149b0134 Merge pull request 'Exportando ventas directamente al tryton' (#12) from export_sales_to_tryton_#9 into main
Reviewed-on: #12
2025-08-09 15:39:40 -05:00
1b30076876 environment: add .env to docker-compose. 2025-08-09 15:27:39 -05:00
271f9b2942 fix(Tryton): fix import customers from tryton. 2025-08-09 15:27:17 -05:00
4734636b4f feat (Tryton): get customers from tryton. 2025-08-09 14:12:31 -05:00
59fbc8872a #9 feat(Tryton): get products salables from tryton only. 2025-07-27 23:34:07 -05:00
d1e137d387 #9 feat(Tryton): untouched products on sync from tryton. 2025-07-27 23:22:58 -05:00
c33c6f630a #9 feat(Tryton): update products from tryton. 2025-07-27 23:11:34 -05:00
76e525735d #9 feat(Tryton): create products from tryton. 2025-07-27 22:57:44 -05:00
3a5e13624f #9 config(Tryton): read tryton params from environment. 2025-07-20 22:01:34 -05:00
3d9feeac43 #9 feat(Tryton): add address_external_id to tryton. 2025-07-19 19:09:53 -05:00
81e4c0bc0d #9 feat(Customer): add address_external_id field. 2025-07-19 19:04:58 -05:00
cf0f6dc4b5 #9 feat(Tryton): enviar_ventas_a_tryton 2025-07-19 19:00:33 -05:00
1d3160ae92 #9 feat(Product): add unit_external_id field. 2025-07-19 18:17:18 -05:00
ba9ef039f4 #9 feat(Product): add external_id field. 2025-07-19 17:32:35 -05:00
46e7181653 #9 feat(Sale): add external_id field. 2025-07-19 17:28:47 -05:00
62d39c97c0 #9 feat(Sale): add external_id field. 2025-07-19 10:57:28 -05:00
f8ff6b7905 Merge pull request 'Se arregla fecha en formato csv para tryton para que se pueda exportar #7' (#8) from fix_csv_to_tryton_#7 into main
Reviewed-on: #8
2025-04-12 12:14:07 -05:00
c7e1af9c81 fix(CSV): report tryton csv. 2025-04-12 12:06:07 -05:00
32149b10e2 Merge pull request 'Permitiendo números decimales para la cantidad de productos en las compras #5' (#6) from #5_sale_with_decima_throught_api into main
Reviewed-on: #6
2025-04-06 16:11:00 -05:00
8791c5fa2d fix(Sale): allow decimal on quantity products. 2025-04-06 16:02:44 -05:00
3c318f2751 Merge pull request 'Exportando ventas para tryton desde api' (#4) from api_export_sales_for_tryton_#3 into main
Reviewed-on: #4
2025-03-08 11:21:24 -05:00
5ecf0f4bf5 feat(Api): export sales for tryton from api. 2025-03-02 23:33:26 -05:00
0e38255a9d refactor(View): public function. 2025-03-02 23:15:52 -05:00
2364583952 refactor(views): extract to method. 2025-03-02 23:14:55 -05:00
585d92c64c fix(ExportSales): add test and fix. 2025-03-02 23:04:19 -05:00
99e1198819 allowed settings from environment. 2025-02-08 18:54:54 -05:00
21 changed files with 896 additions and 46 deletions

4
.env_example Normal file
View File

@@ -0,0 +1,4 @@
TRYTON_HOST=localhost
TRYTON_DATABASE=tryton
TRYTON_USERNAME=admin
TRYTON_PASSWORD=admin

View File

@@ -3,6 +3,8 @@ services:
build: build:
context: ./ context: ./
dockerfile: django.Dockerfile dockerfile: django.Dockerfile
env_file:
- .env
volumes: volumes:
- ./tienda_ilusion:/app/ - ./tienda_ilusion:/app/
ports: ports:

View File

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

View File

@@ -6,9 +6,21 @@ from rest_framework.pagination import PageNumberPagination
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer
from .views import sales_to_tryton_csv
from decimal import Decimal from decimal import Decimal
import json from sabatron_tryton_rpc_client.client import Client
import io
import csv
import os
TRYTON_HOST = os.environ.get('TRYTON_HOST', 'localhost')
TRYTON_DATABASE = os.environ.get('TRYTON_DATABASE', 'tryton')
TRYTON_USERNAME = os.environ.get('TRYTON_USERNAME', 'admin')
TRYTON_PASSWORD = os.environ.get('TRYTON_PASSWORD', 'admin')
TRYTON_COP_CURRENCY = 31
TRYTON_COMPANY_ID = 1
TRYTON_SHOPS = [1]
class Pagination(PageNumberPagination): class Pagination(PageNumberPagination):
@@ -128,12 +140,14 @@ class SalesForReconciliationView(APIView):
return Response(grouped_sales) return Response(grouped_sales)
class SaleSummary(APIView): class SaleSummary(APIView):
def get(self, request, id): def get(self, request, id):
sale = Sale.objects.get(pk=id) sale = Sale.objects.get(pk=id)
serializer = SaleSummarySerializer(sale) serializer = SaleSummarySerializer(sale)
return Response(serializer.data) return Response(serializer.data)
class AdminCodeValidateView(APIView): 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)
@@ -144,3 +158,276 @@ 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
class SalesForTrytonView(APIView):
def get(self, request):
sales = Sale.objects.all()
csv = self._generate_sales_CSV(sales)
return Response({'csv': csv})
def _generate_sales_CSV(self, sales):
output = io.StringIO()
writer = csv.writer(output)
csv_data = sales_to_tryton_csv(sales)
for row in csv_data:
writer.writerow(row)
return output.getvalue()
class SalesToTrytonView(APIView):
def post(self, request):
tryton_client = Client(
hostname=TRYTON_HOST,
database=TRYTON_DATABASE,
username=TRYTON_USERNAME,
password=TRYTON_PASSWORD
)
tryton_client.connect()
method = 'model.sale.sale.create'
tryton_context = {'company': TRYTON_COMPANY_ID,
'shops': TRYTON_SHOPS}
successful = []
failed = []
sales = Sale.objects.filter(external_id=None)
for sale in sales:
try:
lines = SaleLine.objects.filter(sale=sale.id)
tryton_params = self.__to_tryton_params(sale, lines, tryton_context)
external_ids = tryton_client.call(method, tryton_params)
sale.external_id = external_ids[0]
sale.save()
successful.append(sale.id)
except Exception as e:
print(f"Error al enviar la venta: {e}"
f"venta_id: {sale.id}")
failed.append(sale.id)
continue
return Response(
{'successful': successful, 'failed': failed},
status=200
)
def __to_tryton_params(self, sale, lines, tryton_context):
sale_tryton = TrytonSale(sale, lines)
return [[sale_tryton.to_tryton()], tryton_context]
class TrytonSale:
def __init__(self, sale, lines):
self.sale = sale
self.lines = lines
def _format_date(self, _date):
return {"__class__": "date", "year": _date.year, "month": _date.month,
"day": _date.day}
def to_tryton(self):
return {
"company": TRYTON_COMPANY_ID,
"shipment_address": self.sale.customer.address_external_id,
"invoice_address": self.sale.customer.address_external_id,
"currency": TRYTON_COP_CURRENCY,
"description": self.sale.description 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]
]]
}
class TrytonLineSale:
def __init__(self, sale_line):
self.sale_line = sale_line
def _format_decimal(self, number):
return {"__class__": "Decimal", "decimal": str(number)}
def to_tryton(self):
return {
"product": self.sale_line.product.external_id,
"quantity": self._format_decimal(self.sale_line.quantity),
"type": "line",
"unit": self.sale_line.product.unit_external_id,
"unit_price": self._format_decimal(self.sale_line.unit_price)
}
class ProductsFromTrytonView(APIView):
def post(self, request):
tryton_client = Client(
hostname=TRYTON_HOST,
database=TRYTON_DATABASE,
username=TRYTON_USERNAME,
password=TRYTON_PASSWORD
)
tryton_client.connect()
method = 'model.product.product.search'
context = {'company': 1}
params = [[["salable", "=", True]], 0, 1000, [["rec_name", "ASC"], ["id", None]], context]
product_ids = tryton_client.call(method, params)
tryton_products = self.__get_product_datails_from_tryton(
product_ids, tryton_client, context
)
checked_tryton_products = product_ids
failed_products = []
updated_products = []
created_products = []
untouched_products = []
for tryton_product in tryton_products:
try:
product = Product.objects.get(
external_id=tryton_product.get('id')
)
except Product.DoesNotExist:
try:
product = self.__create_product(tryton_product)
created_products.append(product.id)
continue
except Exception as e:
print(f"Error al importar productos: {e}"
f"El producto: {tryton_product}")
failed_products.append(tryton_product.get('id'))
continue
if self.__need_update(product, tryton_product):
self.__update_product(product, tryton_product)
updated_products.append(product.id)
else:
untouched_products.append(product.id)
return Response(
{
'checked_tryton_products': checked_tryton_products,
'failed_products': failed_products,
'updated_products': updated_products,
'created_products': created_products,
'untouched_products': untouched_products,
},
status=200
)
def __get_product_datails_from_tryton(self, product_ids, tryton_client, context):
tryton_fields = ['id', 'name', 'default_uom.id',
'default_uom.rec_name', 'list_price']
method = 'model.product.product.read'
params = (product_ids, tryton_fields, context)
response = tryton_client.call(method, params)
return response
def __need_update(self, product, tryton_product):
if not product.name == tryton_product.get('name'):
return True
if not product.price == tryton_product.get('list_price'):
return True
unit = tryton_product.get('default_uom.')
if not product.measuring_unit == unit.get('rec_name'):
return True
def __create_product(self, tryton_product):
product = Product()
product.name = tryton_product.get('name')
product.price = tryton_product.get('list_price')
product.external_id = tryton_product.get('id')
unit = tryton_product.get('default_uom.')
product.measuring_unit = unit.get('rec_name')
product.unit_external_id = unit.get('id')
product.save()
return product
def __update_product(self, product, tryton_product):
product.name = tryton_product.get('name')
product.price = tryton_product.get('list_price')
product.external_id = tryton_product.get('id')
unit = tryton_product.get('default_uom.')
product.measuring_unit = unit.get('rec_name')
product.unit_external_id = unit.get('id')
product.save()
class CustomersFromTrytonView(APIView):
def post(self, request):
tryton_client = Client(
hostname=TRYTON_HOST,
database=TRYTON_DATABASE,
username=TRYTON_USERNAME,
password=TRYTON_PASSWORD
)
tryton_client.connect()
method = 'model.party.party.search'
context = {'company': 1}
params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context]
party_ids = tryton_client.call(method, params)
tryton_parties = self.__get_party_datails(
party_ids, tryton_client, context
)
checked_tryton_parties = party_ids
failed_parties = []
updated_customers = []
created_customers = []
untouched_customers = []
for tryton_party in tryton_parties:
try:
customer = Customer.objects.get(
external_id=tryton_party.get('id')
)
except Customer.DoesNotExist:
customer = self.__create_customer(tryton_party)
created_customers.append(customer.id)
continue
if self.__need_update(customer, tryton_party):
self.__update_customer(customer, tryton_party)
updated_customers.append(customer.id)
else:
untouched_customers.append(customer.id)
return Response(
{
'checked_tryton_parties': checked_tryton_parties,
'failed_parties': failed_parties,
'updated_customers': updated_customers,
'created_customers': created_customers,
'untouched_customers': untouched_customers,
},
status=200
)
def __get_party_datails(self, party_ids, tryton_client, context):
tryton_fields = ['id', 'name', 'addresses']
method = 'model.party.party.read'
params = (party_ids, tryton_fields, context)
response = tryton_client.call(method, params)
return response
def __need_update(self, customer, tryton_party):
if not customer.name == tryton_party.get('name'):
return True
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
if not customer.address_external_id == str(tryton_party.get('addresses')[0]):
return True
def __create_customer(self, tryton_party):
customer = Customer()
customer.name = tryton_party.get('name')
customer.external_id = tryton_party.get('id')
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
customer.address_external_id = tryton_party.get('addresses')[0]
customer.save()
return customer
def __update_customer(self, customer, tryton_party):
customer.name = tryton_party.get('name')
customer.external_id = tryton_party.get('id')
if tryton_party.get('addresses') and tryton_party.get('addresses')[0]:
customer.address_external_id = tryton_party.get('addresses')[0]
customer.save()

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-04-06 20:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0037_admincode'),
]
operations = [
migrations.AlterField(
model_name='saleline',
name='quantity',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-07-19 15:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0038_alter_saleline_quantity'),
]
operations = [
migrations.AddField(
model_name='sale',
name='external_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-07-19 22:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0039_sale_external_id'),
]
operations = [
migrations.AddField(
model_name='customer',
name='external_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-07-19 22:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0040_customer_external_id'),
]
operations = [
migrations.AddField(
model_name='product',
name='external_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-07-19 23:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0041_product_external_id'),
]
operations = [
migrations.AddField(
model_name='product',
name='unit_external_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-07-20 00:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0042_product_unit_external_id'),
]
operations = [
migrations.AddField(
model_name='customer',
name='address_external_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -17,6 +17,8 @@ class Customer(models.Model):
address = models.CharField(max_length=100, null=True, blank=True) address = models.CharField(max_length=100, null=True, blank=True)
email = 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) 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): def __str__(self):
return self.name return self.name
@@ -41,7 +43,9 @@ class Product(models.Model):
choices=MeasuringUnits.choices, choices=MeasuringUnits.choices,
default=MeasuringUnits.UNIT default=MeasuringUnits.UNIT
) )
unit_external_id = models.CharField(max_length=100, null=True, blank=True)
categories = models.ManyToManyField(ProductCategory) categories = models.ManyToManyField(ProductCategory)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -105,6 +109,7 @@ class Sale(models.Model):
related_name='Sales', related_name='Sales',
null=True null=True
) )
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self): def __str__(self):
return f"{self.date} {self.customer}" return f"{self.date} {self.customer}"
@@ -128,7 +133,7 @@ class SaleLine(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE) sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
product = models.ForeignKey(Product, null=False, blank=False, on_delete=models.CASCADE) product = models.ForeignKey(Product, null=False, blank=False, on_delete=models.CASCADE)
quantity = models.IntegerField(null=True) quantity = models.DecimalField(max_digits=10, decimal_places=2, null=True)
unit_price = models.DecimalField(max_digits=9, decimal_places=2) unit_price = models.DecimalField(max_digits=9, decimal_places=2)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(max_length=255, null=True, blank=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

@@ -1,4 +1,7 @@
import json import json
import csv
import io
from django.urls import reverse 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
@@ -13,7 +16,8 @@ class TestAPI(APITestCase):
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):
@@ -22,6 +26,22 @@ class TestAPI(APITestCase):
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(
sale.customer.name,
self.customer.name,
)
self.assertEqual(
sale.id,
content['id']
)
self.assertIsNone(sale.external_id)
def test_create_sale_with_decimal(self):
response = self._create_sale_with_decimal()
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Sale.objects.count(), 1)
sale = Sale.objects.all()[0]
self.assertEqual( self.assertEqual(
sale.customer.name, sale.customer.name,
self.customer.name self.customer.name
@@ -30,6 +50,10 @@ class TestAPI(APITestCase):
sale.id, sale.id,
content['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/'
@@ -44,6 +68,75 @@ 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):
url = '/don_confiao/api/sales/for_tryton'
self._create_sale()
response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('csv', json_response)
self.assertGreater(len(json_response['csv']), 0)
def test_csv_structure_in_sales_for_tryton(self):
url = '/don_confiao/api/sales/for_tryton'
self._create_sale()
response = self.client.get(url)
json_response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
csv_reader = csv.reader(io.StringIO(json_response['csv']))
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"
]
self.assertEqual(next(csv_reader), expected_header)
expected_rows = [
[self.customer.name, self.customer.name, self.customer.name, "",
"", "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)
self.assertEqual(rows, expected_rows)
def _create_sale(self): def _create_sale(self):
url = '/don_confiao/api/sales/' url = '/don_confiao/api/sales/'
@@ -57,3 +150,24 @@ class TestAPI(APITestCase):
], ],
} }
return self.client.post(url, data, format='json') 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

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

View File

@@ -0,0 +1,103 @@
import csv
import json
from unittest.mock import patch
from django.test import TestCase, Client
from django.urls import reverse
from ..models import Sale, SaleLine, Product, Customer
class TestExportarVentasParaTryton(TestCase):
def setUp(self):
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',
)
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):
client = Client()
url = '/don_confiao/exportar_ventas_para_tryton'
response = 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", "", "", "2024-09-02", "Contado", "Almacén", "Peso colombiano", "Panela", "2.00", "3000.00", "Unidad", "TIENDA LA ILUSIÓN", "Tienda La Ilusion", "La Ilusion", "True", ""],
["", "", "", "", "", "", "", "", "", "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):
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)
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, '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]}])

View File

@@ -14,6 +14,8 @@ class TestCustomer(TestCase):
customer.save() customer.save()
self.assertIsInstance(customer, Customer) self.assertIsInstance(customer, Customer)
self.assertIsNone(customer.external_id)
self.assertIsNone(customer.address_external_id)
def test_don_create_customer_without_name(self): def test_don_create_customer_without_name(self):
customer = Customer() customer = Customer()

View File

@@ -10,6 +10,16 @@ class TestProducts(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
def test_create_product(self):
product = Product()
product.name = "Un producto"
product.price = 1000
product.save()
self.assertIsInstance(product, Product)
self.assertIsNone(product.external_id)
self.assertIsNone(product.unit_external_id)
def test_import_products(self): def test_import_products(self):
self._import_csv() self._import_csv()
all_products = self._get_products() all_products = self._get_products()

View File

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

View File

@@ -20,15 +20,23 @@ urlpatterns = [
path("productos", views.products, name="products"), path("productos", views.products, name="products"),
path("lista_productos", views.ProductListView.as_view(), name='product_list'), path("lista_productos", views.ProductListView.as_view(), name='product_list'),
path("importar_productos", views.import_products, name="import_products"), path("importar_productos", views.import_products, name="import_products"),
path('api/importar_productos_de_tryton',
api_views.ProductsFromTrytonView.as_view(),
name="products_from_tryton"),
path("importar_terceros", views.import_customers, name="import_customers"), path("importar_terceros", views.import_customers, name="import_customers"),
path('api/importar_clientes_de_tryton',
api_views.CustomersFromTrytonView.as_view(),
name="customers_from_tryton"),
path("exportar_ventas_para_tryton", path("exportar_ventas_para_tryton",
views.exportar_ventas_para_tryton, views.exportar_ventas_para_tryton,
name="exportar_ventas_para_tryton"), name="exportar_ventas_para_tryton"),
path('api/enviar_ventas_a_tryton', api_views.SalesToTrytonView.as_view(), name="send_tryton"),
path("resumen_compra/<int:id>", views.purchase_summary, name="purchase_summary"), path("resumen_compra/<int:id>", views.purchase_summary, name="purchase_summary"),
path("resumen_compra_json/<int:id>", api_views.SaleSummary.as_view(), name="purchase_json_summary"), path("resumen_compra_json/<int:id>", api_views.SaleSummary.as_view(), name="purchase_json_summary"),
path("payment_methods/all/select_format", api_views.PaymentMethodView.as_view(), name="payment_methods_to_select"), 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('purchases/for_reconciliation', api_views.SalesForReconciliationView.as_view(), name='sales_for_reconciliation'),
path('reconciliate_jar', api_views.ReconciliateJarView.as_view()), path('reconciliate_jar', api_views.ReconciliateJarView.as_view()),
path('api/admin_code/validate/<code>', api_views.AdminCodeValidateView.as_view()), path('api/admin_code/validate/<code>', api_views.AdminCodeValidateView.as_view()),
path('api/sales/for_tryton', api_views.SalesForTrytonView.as_view()),
path('api/', include(router.urls)), path('api/', include(router.urls)),
] ]

View File

@@ -163,8 +163,7 @@ def handle_import_customers_file(csv_file):
} }
) )
def sales_to_tryton_csv(sales):
def exportar_ventas_para_tryton(request):
tryton_sales_header = [ tryton_sales_header = [
"Tercero", "Tercero",
"Dirección de facturación", "Dirección de facturación",
@@ -186,14 +185,7 @@ def exportar_ventas_para_tryton(request):
"Comentario" "Comentario"
] ]
if request.method == "GET": csv_data = [tryton_sales_header]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = "attachment; filename=sales.csv"
writer = csv.writer(response)
writer.writerow(tryton_sales_header)
sales = Sale.objects.all()
for sale in sales: for sale in sales:
sale_lines = SaleLine.objects.filter(sale=sale.id) sale_lines = SaleLine.objects.filter(sale=sale.id)
if not sale_lines: if not sale_lines:
@@ -202,14 +194,14 @@ def exportar_ventas_para_tryton(request):
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, sale.date.strftime('%Y-%m-%d'),
"Contado", "Contado",
"Almacén", "Almacén",
"Peso colombiano", "Peso colombiano",
first_sale_line.product.name, first_sale_line.product.name,
first_sale_line.quantity, first_sale_line.quantity,
"Unidad",
first_sale_line.unit_price, first_sale_line.unit_price,
"Unidad",
"TIENDA LA ILUSIÓN", "TIENDA LA ILUSIÓN",
"Tienda La Ilusion", "Tienda La Ilusion",
"La Ilusion", "La Ilusion",
@@ -223,6 +215,19 @@ def exportar_ventas_para_tryton(request):
line.unit_price, line.unit_price,
"Unidad"]+[""]*5) "Unidad"]+[""]*5)
for row in lines: for row in lines:
csv_data.append(row)
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) writer.writerow(row)
return response return response

View File

@@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/ https://docs.djangoproject.com/en/5.0/ref/settings/
""" """
import os
from pathlib import Path from pathlib import Path
@@ -20,14 +21,18 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v' SECRET_KEY = os.environ.get(
"SECRET_KEY",
"django-insecure-zh6rinl@8y7g(cf781snisx2j%p^c#d&b2@@9cqe!v@4yv8x=v"
)
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True if os.environ.get("DEBUG", 'False') in ['True', '1'] else False
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')
ALLOWED_HOSTS = ['localhost'] CORS_ALLOWED_ORIGINS = os.environ.get(
'CORS_ALLOWED_ORIGINS',
CORS_ALLOWED_ORIGINS = ['http://localhost:3000', 'http://localhost:7001'] 'http://localhost:3000,http://localhost:7001').split(',')
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [