Compare commits
10 Commits
feat/seria
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 53cc333c19 | |||
| 0fd79a2ba7 | |||
| b5d057bd1a | |||
| 792ac4367c | |||
| 3a0c76ce69 | |||
| 699983931e | |||
| 29d154e140 | |||
| ce556918f2 | |||
| 965332e0b4 | |||
| d7132733fc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -321,7 +321,7 @@ pyrightconfig.json
|
|||||||
[Ll]ib
|
[Ll]ib
|
||||||
[Ll]ib64
|
[Ll]ib64
|
||||||
[Ll]ocal
|
[Ll]ocal
|
||||||
[Ss]cripts
|
Scripts/
|
||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
pip-selfcheck.json
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
|||||||
1121
scripts/data/colombia_municipios.csv
Normal file
1121
scripts/data/colombia_municipios.csv
Normal file
File diff suppressed because it is too large
Load Diff
188
scripts/seed_geography.py
Normal file
188
scripts/seed_geography.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Seed geografía de Colombia (países, departamentos y municipios).
|
||||||
|
|
||||||
|
Requisitos (instalar ad-hoc, NO son dependencia del proyecto):
|
||||||
|
pip install pycountry requests
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python scripts/seed_geography.py http://localhost:7000
|
||||||
|
|
||||||
|
El script obtiene el país y los departamentos de Colombia desde
|
||||||
|
`pycountry` (ISO 3166-1/3166-2) y los municipios desde el dataset DANE
|
||||||
|
incluido en `scripts/data/colombia_municipios.csv`. No consulta internet
|
||||||
|
en tiempo de ejecución.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import getpass
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pycountry
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
print(
|
||||||
|
"pycountry no está instalado. Ejecuta: pip install pycountry",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
TOKEN_URL = "/api/token/"
|
||||||
|
SEED_GEOGRAPHY_URL = "/don_confiao/api/seed_geography"
|
||||||
|
|
||||||
|
MUNICIPALITIES_CSV = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"data",
|
||||||
|
"colombia_municipios.csv",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nombres DANE que difieren de los nombres oficiales de pycountry.
|
||||||
|
DEPARTMENT_ALIASES = {
|
||||||
|
"BOGOTA": "Distrito Capital de Bogotá",
|
||||||
|
"SAN ANDRES": "San Andrés, Providencia y Santa Catalina",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_credentials():
|
||||||
|
username = input("Usuario: ")
|
||||||
|
password = getpass.getpass("Contraseña: ")
|
||||||
|
return username, password
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(domain, username, password):
|
||||||
|
url = domain.rstrip("/") + TOKEN_URL
|
||||||
|
response = requests.post(
|
||||||
|
url, json={"username": username, "password": password}
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(
|
||||||
|
f"Error al obtener token: {response.status_code} {response.text}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
return response.json()["access"]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(name):
|
||||||
|
return "".join(
|
||||||
|
char
|
||||||
|
for char in unicodedata.normalize("NFD", name)
|
||||||
|
if unicodedata.category(char) != "Mn"
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def get_department_mapping(subdivisions):
|
||||||
|
by_normalized = {
|
||||||
|
_normalize(subdivision.name): subdivision.name
|
||||||
|
for subdivision in subdivisions
|
||||||
|
}
|
||||||
|
|
||||||
|
csv_department_names = set()
|
||||||
|
with open(MUNICIPALITIES_CSV, newline="", encoding="utf-8") as f:
|
||||||
|
for row in csv.DictReader(f):
|
||||||
|
csv_department_names.add(row["department"])
|
||||||
|
|
||||||
|
mapping = dict(DEPARTMENT_ALIASES)
|
||||||
|
for name in csv_department_names:
|
||||||
|
if name in mapping:
|
||||||
|
continue
|
||||||
|
mapped = by_normalized.get(_normalize(name))
|
||||||
|
if mapped is None:
|
||||||
|
print(
|
||||||
|
f"Departamento '{name}' no encontrado en pycountry",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
mapping[name] = mapped
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def read_municipalities(mapping):
|
||||||
|
municipalities = []
|
||||||
|
with open(MUNICIPALITIES_CSV, newline="", encoding="utf-8") as f:
|
||||||
|
for row in csv.DictReader(f):
|
||||||
|
municipality = {
|
||||||
|
"name": row["municipality"],
|
||||||
|
"department": mapping[row["department"]],
|
||||||
|
}
|
||||||
|
if row.get("latitude"):
|
||||||
|
municipality["latitude"] = float(row["latitude"])
|
||||||
|
if row.get("longitude"):
|
||||||
|
municipality["longitude"] = float(row["longitude"])
|
||||||
|
municipalities.append(municipality)
|
||||||
|
return municipalities
|
||||||
|
|
||||||
|
|
||||||
|
def build_payload():
|
||||||
|
country = pycountry.countries.get(alpha_2="CO")
|
||||||
|
subdivisions = pycountry.subdivisions.get(country_code="CO")
|
||||||
|
|
||||||
|
departments = [
|
||||||
|
{"name": subdivision.name, "code": subdivision.code}
|
||||||
|
for subdivision in subdivisions
|
||||||
|
]
|
||||||
|
mapping = get_department_mapping(subdivisions)
|
||||||
|
municipalities = read_municipalities(mapping)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"country": {"name": country.name, "code": country.alpha_2},
|
||||||
|
"departments": departments,
|
||||||
|
"municipalities": municipalities,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def seed_geography(domain, token, payload):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
full_url = domain.rstrip("/") + SEED_GEOGRAPHY_URL
|
||||||
|
response = requests.post(full_url, headers=headers, json=payload)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(
|
||||||
|
f"Error al sembrar geografía: {response.status_code} {response.text}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Alimenta países, departamentos y municipios de Colombia "
|
||||||
|
"usando pycountry y el dataset DANE incluido en scripts/data/."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"domain", help="Dominio del backend (ej: http://localhost:7000)"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
username, password = get_credentials()
|
||||||
|
token = get_token(args.domain, username, password)
|
||||||
|
print("Token obtenido correctamente.")
|
||||||
|
|
||||||
|
payload = build_payload()
|
||||||
|
print(
|
||||||
|
"Preparando siembra: 1 país, "
|
||||||
|
f"{len(payload['departments'])} departamentos, "
|
||||||
|
f"{len(payload['municipalities'])} municipios."
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Enviando geografía...")
|
||||||
|
result = seed_geography(args.domain, token, payload)
|
||||||
|
print(
|
||||||
|
f" [OK] {result['country']}: "
|
||||||
|
f"{result['total_departments']} departamentos, "
|
||||||
|
f"{result['total_municipalities']} municipios."
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"\nResumen: {result['departments_created']} departamentos y "
|
||||||
|
f"{result['municipalities_created']} municipios creados "
|
||||||
|
f"(re-ejecutar es idempotente)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -21,6 +21,7 @@ class StoreSettingsAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(Customer)
|
@admin.register(Customer)
|
||||||
class CustomerAdmin(admin.ModelAdmin):
|
class CustomerAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
"phone",
|
"phone",
|
||||||
@@ -72,7 +73,7 @@ class CatalogSaleLineAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Product)
|
@admin.register(Product)
|
||||||
class ProductAdmin(admin.ModelAdmin):
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "price", "measuring_unit", "external_id")
|
list_display = ("id", "name", "price", "measuring_unit", "external_id")
|
||||||
search_fields = ("id", "name",)
|
search_fields = ("id", "name",)
|
||||||
list_filter = ("name", "id")
|
list_filter = ("name", "id")
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ from .payments import (
|
|||||||
)
|
)
|
||||||
from .admin import AdminCodeValidateView
|
from .admin import AdminCodeValidateView
|
||||||
from .store_settings import StoreSettingsView
|
from .store_settings import StoreSettingsView
|
||||||
|
from .provenance import (
|
||||||
|
OrganizationView,
|
||||||
|
SupplierView,
|
||||||
|
CountryView,
|
||||||
|
DepartmentView,
|
||||||
|
MunicipalityView,
|
||||||
|
SeedGeographyView,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Catalogue Images
|
# Catalogue Images
|
||||||
@@ -49,4 +57,11 @@ __all__ = [
|
|||||||
"AdminCodeValidateView",
|
"AdminCodeValidateView",
|
||||||
# Store Settings
|
# Store Settings
|
||||||
"StoreSettingsView",
|
"StoreSettingsView",
|
||||||
|
# Provenance
|
||||||
|
"OrganizationView",
|
||||||
|
"SupplierView",
|
||||||
|
"CountryView",
|
||||||
|
"DepartmentView",
|
||||||
|
"MunicipalityView",
|
||||||
|
"SeedGeographyView",
|
||||||
]
|
]
|
||||||
|
|||||||
122
tienda_ilusion/don_confiao/api/provenance.py
Normal file
122
tienda_ilusion/don_confiao/api/provenance.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from ..models.geography import Country, Department, Municipality
|
||||||
|
from ..models.provenance import Organization, Supplier
|
||||||
|
from ..serializers import (
|
||||||
|
CountrySerializer,
|
||||||
|
DepartmentSerializer,
|
||||||
|
MunicipalitySerializer,
|
||||||
|
OrganizationSerializer,
|
||||||
|
SupplierSerializer,
|
||||||
|
)
|
||||||
|
from ..permissions import IsAdministrator, IsNotPublico
|
||||||
|
|
||||||
|
|
||||||
|
class _AdminWriteMixin:
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action in ("list", "retrieve"):
|
||||||
|
return [IsNotPublico()]
|
||||||
|
return [IsAuthenticated(), IsAdministrator()]
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationView(_AdminWriteMixin, viewsets.ModelViewSet):
|
||||||
|
queryset = Organization.objects.all()
|
||||||
|
serializer_class = OrganizationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierView(_AdminWriteMixin, viewsets.ModelViewSet):
|
||||||
|
queryset = Supplier.objects.select_related(
|
||||||
|
"organization", "municipality"
|
||||||
|
)
|
||||||
|
serializer_class = SupplierSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CountryView(_AdminWriteMixin, viewsets.ModelViewSet):
|
||||||
|
queryset = Country.objects.all()
|
||||||
|
serializer_class = CountrySerializer
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentView(_AdminWriteMixin, viewsets.ModelViewSet):
|
||||||
|
queryset = Department.objects.select_related("country")
|
||||||
|
serializer_class = DepartmentSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class MunicipalityView(_AdminWriteMixin, viewsets.ModelViewSet):
|
||||||
|
queryset = Municipality.objects.select_related(
|
||||||
|
"department", "country"
|
||||||
|
)
|
||||||
|
serializer_class = MunicipalitySerializer
|
||||||
|
|
||||||
|
|
||||||
|
class SeedGeographyView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated, IsAdministrator]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
country_data = request.data.get("country") or {}
|
||||||
|
country_name = country_data.get("name")
|
||||||
|
if not country_name:
|
||||||
|
return Response(
|
||||||
|
{"detail": "country.name is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
country, _ = Country.objects.get_or_create(
|
||||||
|
name=country_name,
|
||||||
|
defaults={"code": country_data.get("code", "")},
|
||||||
|
)
|
||||||
|
|
||||||
|
departments_created = 0
|
||||||
|
departments = {}
|
||||||
|
for department_data in request.data.get("departments", []):
|
||||||
|
department, created = Department.objects.get_or_create(
|
||||||
|
name=department_data["name"],
|
||||||
|
defaults={"country": country},
|
||||||
|
)
|
||||||
|
departments[department.name] = department
|
||||||
|
if created:
|
||||||
|
departments_created += 1
|
||||||
|
|
||||||
|
municipalities_created = 0
|
||||||
|
for municipality_data in request.data.get(
|
||||||
|
"municipalities", []
|
||||||
|
):
|
||||||
|
department_name = municipality_data["department"]
|
||||||
|
department = departments.get(department_name)
|
||||||
|
if department is None:
|
||||||
|
department, created = Department.objects.get_or_create(
|
||||||
|
name=department_name, defaults={"country": country}
|
||||||
|
)
|
||||||
|
departments[department.name] = department
|
||||||
|
if created:
|
||||||
|
departments_created += 1
|
||||||
|
|
||||||
|
municipality, created = Municipality.objects.get_or_create(
|
||||||
|
name=municipality_data["name"],
|
||||||
|
department=department,
|
||||||
|
defaults={"country": country},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
municipalities_created += 1
|
||||||
|
|
||||||
|
latitude = municipality_data.get("latitude")
|
||||||
|
longitude = municipality_data.get("longitude")
|
||||||
|
if latitude is not None or longitude is not None:
|
||||||
|
municipality.country = country
|
||||||
|
if latitude is not None:
|
||||||
|
municipality.latitude = latitude
|
||||||
|
if longitude is not None:
|
||||||
|
municipality.longitude = longitude
|
||||||
|
municipality.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"country": country.name,
|
||||||
|
"departments_created": departments_created,
|
||||||
|
"municipalities_created": municipalities_created,
|
||||||
|
"total_departments": Department.objects.count(),
|
||||||
|
"total_municipalities": Municipality.objects.count(),
|
||||||
|
},
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-15 21:17
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0052_merge_20260810_0351'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Country',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('code', models.CharField(blank=True, max_length=3, null=True, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Country',
|
||||||
|
'verbose_name_plural': 'Countries',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Organization',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('website', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('contact_email', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('contact_phone', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Organization',
|
||||||
|
'verbose_name_plural': 'Organizations',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Department',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='departments', to='don_confiao.country')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Department',
|
||||||
|
'verbose_name_plural': 'Departments',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Municipality',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='municipalities', to='don_confiao.country')),
|
||||||
|
('department', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='municipalities', to='don_confiao.department')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Municipality',
|
||||||
|
'verbose_name_plural': 'Municipalities',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Supplier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('website', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('contact_email', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('contact_phone', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('municipality', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='suppliers', to='don_confiao.municipality')),
|
||||||
|
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='suppliers', to='don_confiao.organization')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Supplier',
|
||||||
|
'verbose_name_plural': 'Suppliers',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='suppliers',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='products', to='don_confiao.supplier'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='municipality',
|
||||||
|
constraint=models.UniqueConstraint(fields=('name', 'department'), name='unique_municipality_name_per_department'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-16 08:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0053_country_organization_department_municipality_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='municipality',
|
||||||
|
name='latitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='municipality',
|
||||||
|
name='longitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-22 20:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0054_municipality_latitude_municipality_longitude'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productcategory',
|
||||||
|
name='external_id',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
from .store_settings import StoreSettings
|
from .store_settings import StoreSettings
|
||||||
|
from .geography import Country, Department, Municipality
|
||||||
|
from .provenance import Organization, Supplier
|
||||||
|
|
||||||
__all__ = ["StoreSettings"]
|
__all__ = [
|
||||||
|
"StoreSettings",
|
||||||
|
"Country",
|
||||||
|
"Department",
|
||||||
|
"Municipality",
|
||||||
|
"Organization",
|
||||||
|
"Supplier",
|
||||||
|
]
|
||||||
|
|||||||
60
tienda_ilusion/don_confiao/models/geography.py
Normal file
60
tienda_ilusion/don_confiao/models/geography.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Country(models.Model):
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
code = models.CharField(
|
||||||
|
max_length=3, unique=True, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Country"
|
||||||
|
verbose_name_plural = "Countries"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Department(models.Model):
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
country = models.ForeignKey(
|
||||||
|
Country, on_delete=models.PROTECT, related_name="departments"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Department"
|
||||||
|
verbose_name_plural = "Departments"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Municipality(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
department = models.ForeignKey(
|
||||||
|
Department, on_delete=models.PROTECT, related_name="municipalities"
|
||||||
|
)
|
||||||
|
country = models.ForeignKey(
|
||||||
|
Country, on_delete=models.PROTECT, related_name="municipalities"
|
||||||
|
)
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=7, null=True, blank=True
|
||||||
|
)
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=7, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Municipality"
|
||||||
|
verbose_name_plural = "Municipalities"
|
||||||
|
ordering = ["name"]
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["name", "department"], name="unique_municipality_name_per_department"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
@@ -8,6 +8,7 @@ class MeasuringUnits(models.TextChoices):
|
|||||||
|
|
||||||
class ProductCategory(models.Model):
|
class ProductCategory(models.Model):
|
||||||
name = models.CharField(max_length=100, unique=True)
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
external_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -26,6 +27,9 @@ class Product(models.Model):
|
|||||||
max_length=100, null=True, blank=True
|
max_length=100, null=True, blank=True
|
||||||
)
|
)
|
||||||
categories = models.ManyToManyField(ProductCategory)
|
categories = models.ManyToManyField(ProductCategory)
|
||||||
|
suppliers = models.ManyToManyField(
|
||||||
|
"Supplier", blank=True, related_name="products"
|
||||||
|
)
|
||||||
external_id = models.CharField(max_length=100, null=True, blank=True)
|
external_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
53
tienda_ilusion/don_confiao/models/provenance.py
Normal file
53
tienda_ilusion/don_confiao/models/provenance.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .geography import Municipality
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(models.Model):
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
website = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
contact_email = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
contact_phone = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Organization"
|
||||||
|
verbose_name_plural = "Organizations"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Supplier(models.Model):
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
organization = models.ForeignKey(
|
||||||
|
Organization,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="suppliers",
|
||||||
|
)
|
||||||
|
municipality = models.ForeignKey(
|
||||||
|
Municipality,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="suppliers",
|
||||||
|
)
|
||||||
|
website = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
contact_email = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
contact_phone = models.CharField(max_length=100, null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Supplier"
|
||||||
|
verbose_name_plural = "Suppliers"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
@@ -17,6 +17,8 @@ from .payments import (
|
|||||||
PaymentMethodSerializer,
|
PaymentMethodSerializer,
|
||||||
)
|
)
|
||||||
from .store_settings import StoreSettingsSerializer
|
from .store_settings import StoreSettingsSerializer
|
||||||
|
from .geography import CountrySerializer, DepartmentSerializer, MunicipalitySerializer
|
||||||
|
from .provenance import OrganizationSerializer, SupplierSerializer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Catalogue Images
|
# Catalogue Images
|
||||||
@@ -42,4 +44,11 @@ __all__ = [
|
|||||||
"PaymentMethodSerializer",
|
"PaymentMethodSerializer",
|
||||||
# Store Settings
|
# Store Settings
|
||||||
"StoreSettingsSerializer",
|
"StoreSettingsSerializer",
|
||||||
|
# Geography
|
||||||
|
"CountrySerializer",
|
||||||
|
"DepartmentSerializer",
|
||||||
|
"MunicipalitySerializer",
|
||||||
|
# Provenance
|
||||||
|
"OrganizationSerializer",
|
||||||
|
"SupplierSerializer",
|
||||||
]
|
]
|
||||||
|
|||||||
55
tienda_ilusion/don_confiao/serializers/geography.py
Normal file
55
tienda_ilusion/don_confiao/serializers/geography.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from ..models.geography import Country, Department, Municipality
|
||||||
|
|
||||||
|
|
||||||
|
class CountrySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Country
|
||||||
|
fields = ["id", "name", "code"]
|
||||||
|
|
||||||
|
|
||||||
|
class CountryBriefSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Country
|
||||||
|
fields = ["id", "name", "code"]
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentBriefSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Department
|
||||||
|
fields = ["id", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
class MunicipalityBriefSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Municipality
|
||||||
|
fields = ["id", "name", "latitude", "longitude"]
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentSerializer(serializers.ModelSerializer):
|
||||||
|
country_detail = CountryBriefSerializer(source="country", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Department
|
||||||
|
fields = ["id", "name", "country", "country_detail"]
|
||||||
|
|
||||||
|
|
||||||
|
class MunicipalitySerializer(serializers.ModelSerializer):
|
||||||
|
department_detail = DepartmentBriefSerializer(
|
||||||
|
source="department", read_only=True
|
||||||
|
)
|
||||||
|
country_detail = CountryBriefSerializer(source="country", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Municipality
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"department",
|
||||||
|
"department_detail",
|
||||||
|
"country",
|
||||||
|
"country_detail",
|
||||||
|
"latitude",
|
||||||
|
"longitude",
|
||||||
|
]
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..models.products import Product, ProductCategory
|
from ..models.products import Product, ProductCategory
|
||||||
|
from ..models.provenance import Supplier
|
||||||
|
|
||||||
|
|
||||||
class ProductSerializer(serializers.ModelSerializer):
|
class ProductSerializer(serializers.ModelSerializer):
|
||||||
catalogue_images = serializers.SerializerMethodField()
|
catalogue_images = serializers.SerializerMethodField()
|
||||||
|
suppliers = serializers.PrimaryKeyRelatedField(
|
||||||
|
many=True, queryset=Supplier.objects.all(), required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
@@ -15,6 +19,7 @@ class ProductSerializer(serializers.ModelSerializer):
|
|||||||
"price",
|
"price",
|
||||||
"measuring_unit",
|
"measuring_unit",
|
||||||
"categories",
|
"categories",
|
||||||
|
"suppliers",
|
||||||
"external_id",
|
"external_id",
|
||||||
"catalogue_images",
|
"catalogue_images",
|
||||||
]
|
]
|
||||||
|
|||||||
79
tienda_ilusion/don_confiao/serializers/provenance.py
Normal file
79
tienda_ilusion/don_confiao/serializers/provenance.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from ..models.provenance import Organization, Supplier
|
||||||
|
from .geography import (
|
||||||
|
CountryBriefSerializer,
|
||||||
|
DepartmentBriefSerializer,
|
||||||
|
MunicipalityBriefSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Organization
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"website",
|
||||||
|
"contact_email",
|
||||||
|
"contact_phone",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationBriefSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Organization
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"website",
|
||||||
|
"contact_email",
|
||||||
|
"contact_phone",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierSerializer(serializers.ModelSerializer):
|
||||||
|
organization_detail = OrganizationBriefSerializer(
|
||||||
|
source="organization", read_only=True
|
||||||
|
)
|
||||||
|
municipality_detail = MunicipalityBriefSerializer(
|
||||||
|
source="municipality", read_only=True
|
||||||
|
)
|
||||||
|
products = serializers.PrimaryKeyRelatedField(
|
||||||
|
many=True, read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Supplier
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"organization",
|
||||||
|
"organization_detail",
|
||||||
|
"municipality",
|
||||||
|
"municipality_detail",
|
||||||
|
"website",
|
||||||
|
"contact_email",
|
||||||
|
"contact_phone",
|
||||||
|
"products",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierBriefSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Supplier
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"website",
|
||||||
|
"contact_email",
|
||||||
|
"contact_phone",
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from ..models.products import Product
|
||||||
from ..models.sales import (
|
from ..models.sales import (
|
||||||
Sale,
|
Sale,
|
||||||
SaleLine,
|
SaleLine,
|
||||||
@@ -10,6 +11,7 @@ from ..models.sales import (
|
|||||||
)
|
)
|
||||||
from .products import ListProductSerializer
|
from .products import ListProductSerializer
|
||||||
from .customers import ListCustomerSerializer
|
from .customers import ListCustomerSerializer
|
||||||
|
from ..services.provenance import build_product_provenance
|
||||||
|
|
||||||
|
|
||||||
class PublicSummaryLinkMixin:
|
class PublicSummaryLinkMixin:
|
||||||
@@ -23,6 +25,16 @@ class PublicSummaryLinkMixin:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class ProvenanceSummaryMixin:
|
||||||
|
def get_product_provenance(self, obj):
|
||||||
|
products = Product.objects.filter(
|
||||||
|
saleline__sale=obj
|
||||||
|
).distinct()
|
||||||
|
return build_product_provenance(
|
||||||
|
products, self.context.get("request")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SaleLineSerializer(serializers.ModelSerializer):
|
class SaleLineSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SaleLine
|
model = SaleLine
|
||||||
@@ -101,14 +113,26 @@ class SummarySaleLineSerializer(serializers.ModelSerializer):
|
|||||||
fields = ["product", "quantity", "unit_price", "description"]
|
fields = ["product", "quantity", "unit_price", "description"]
|
||||||
|
|
||||||
|
|
||||||
class SaleSummarySerializer(PublicSummaryLinkMixin, serializers.ModelSerializer):
|
class SaleSummarySerializer(
|
||||||
|
PublicSummaryLinkMixin, ProvenanceSummaryMixin, serializers.ModelSerializer
|
||||||
|
):
|
||||||
customer = ListCustomerSerializer()
|
customer = ListCustomerSerializer()
|
||||||
lines = SummarySaleLineSerializer(many=True, source="saleline_set")
|
lines = SummarySaleLineSerializer(many=True, source="saleline_set")
|
||||||
link = serializers.SerializerMethodField()
|
link = serializers.SerializerMethodField()
|
||||||
|
product_provenance = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Sale
|
model = Sale
|
||||||
fields = ["id", "code", "date", "customer", "payment_method", "lines", "link"]
|
fields = [
|
||||||
|
"id",
|
||||||
|
"code",
|
||||||
|
"date",
|
||||||
|
"customer",
|
||||||
|
"payment_method",
|
||||||
|
"lines",
|
||||||
|
"link",
|
||||||
|
"product_provenance",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
|
class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
|
||||||
@@ -119,18 +143,31 @@ class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
|
|||||||
fields = ["product", "quantity", "unit_price", "description"]
|
fields = ["product", "quantity", "unit_price", "description"]
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogProvenanceSummaryMixin(ProvenanceSummaryMixin):
|
||||||
|
def get_product_provenance(self, obj):
|
||||||
|
products = Product.objects.filter(
|
||||||
|
catalogsaleline__catalog_sale=obj
|
||||||
|
).distinct()
|
||||||
|
return build_product_provenance(
|
||||||
|
products, self.context.get("request")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CatalogSaleSummarySerializer(
|
class CatalogSaleSummarySerializer(
|
||||||
PublicSummaryLinkMixin, serializers.ModelSerializer
|
PublicSummaryLinkMixin,
|
||||||
|
CatalogProvenanceSummaryMixin,
|
||||||
|
serializers.ModelSerializer,
|
||||||
):
|
):
|
||||||
customer = ListCustomerSerializer()
|
customer = ListCustomerSerializer()
|
||||||
lines = CatalogSummarySaleLineSerializer(
|
lines = CatalogSummarySaleLineSerializer(
|
||||||
many=True, source="catalogsaleline_set"
|
many=True, source="catalogsaleline_set"
|
||||||
)
|
)
|
||||||
link = serializers.SerializerMethodField()
|
link = serializers.SerializerMethodField()
|
||||||
|
product_provenance = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CatalogSale
|
model = CatalogSale
|
||||||
fields = ["id", "code", "date", "customer", "lines", "link"]
|
fields = ["id", "code", "date", "customer", "lines", "link", "product_provenance"]
|
||||||
|
|
||||||
|
|
||||||
class SaleForRenconciliationSerializer(serializers.Serializer):
|
class SaleForRenconciliationSerializer(serializers.Serializer):
|
||||||
|
|||||||
66
tienda_ilusion/don_confiao/services/provenance.py
Normal file
66
tienda_ilusion/don_confiao/services/provenance.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from ..serializers.geography import (
|
||||||
|
CountryBriefSerializer,
|
||||||
|
DepartmentBriefSerializer,
|
||||||
|
MunicipalityBriefSerializer,
|
||||||
|
)
|
||||||
|
from ..serializers.provenance import (
|
||||||
|
OrganizationBriefSerializer,
|
||||||
|
SupplierBriefSerializer,
|
||||||
|
)
|
||||||
|
from ..serializers.products import ListProductSerializer
|
||||||
|
|
||||||
|
|
||||||
|
def build_product_provenance(products, request=None):
|
||||||
|
"""Domain data for provenance charts.
|
||||||
|
|
||||||
|
Returns a list of products, each with its suppliers. Each supplier
|
||||||
|
entry includes the linked organization and the territorial hierarchy
|
||||||
|
(municipality -> department -> country) derived from the supplier's
|
||||||
|
municipality.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for product in products:
|
||||||
|
suppliers = []
|
||||||
|
supplier_qs = product.suppliers.all().select_related(
|
||||||
|
"organization", "municipality__department__country"
|
||||||
|
)
|
||||||
|
for supplier in supplier_qs:
|
||||||
|
municipality = supplier.municipality
|
||||||
|
entry = {
|
||||||
|
"supplier": SupplierBriefSerializer(supplier).data,
|
||||||
|
"organization": (
|
||||||
|
OrganizationBriefSerializer(supplier.organization).data
|
||||||
|
if supplier.organization
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"municipality": (
|
||||||
|
MunicipalityBriefSerializer(municipality).data
|
||||||
|
if municipality
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"department": (
|
||||||
|
DepartmentBriefSerializer(
|
||||||
|
municipality.department
|
||||||
|
).data
|
||||||
|
if municipality
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"country": (
|
||||||
|
CountryBriefSerializer(
|
||||||
|
municipality.department.country
|
||||||
|
).data
|
||||||
|
if municipality
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
suppliers.append(entry)
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"product": ListProductSerializer(
|
||||||
|
product, context={"request": request}
|
||||||
|
).data,
|
||||||
|
"suppliers": suppliers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
@@ -15,7 +15,10 @@ class CustomerTrytonService:
|
|||||||
party_ids = self.client.call(method, params)
|
party_ids = self.client.call(method, params)
|
||||||
tryton_parties = self._get_party_details(party_ids, context)
|
tryton_parties = self._get_party_details(party_ids, context)
|
||||||
|
|
||||||
checked_tryton_parties = party_ids
|
checked_tryton_parties = [
|
||||||
|
{"id": p.get("id"), "name": p.get("name")}
|
||||||
|
for p in tryton_parties
|
||||||
|
]
|
||||||
failed_parties = []
|
failed_parties = []
|
||||||
updated_customers = []
|
updated_customers = []
|
||||||
created_customers = []
|
created_customers = []
|
||||||
@@ -25,15 +28,35 @@ class CustomerTrytonService:
|
|||||||
try:
|
try:
|
||||||
customer = Customer.objects.get(external_id=tryton_party.get("id"))
|
customer = Customer.objects.get(external_id=tryton_party.get("id"))
|
||||||
except Customer.DoesNotExist:
|
except Customer.DoesNotExist:
|
||||||
customer = self._create_customer(tryton_party)
|
try:
|
||||||
created_customers.append(customer.id)
|
customer = self._create_customer(tryton_party)
|
||||||
continue
|
created_customers.append(self._build_item(customer))
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Error al importar clientes: {e}El cliente: {tryton_party}"
|
||||||
|
)
|
||||||
|
failed_parties.append(
|
||||||
|
self._build_failure(tryton_party, "crear", e)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
if self._need_update(customer, tryton_party):
|
try:
|
||||||
self._update_customer(customer, tryton_party)
|
if self._need_update(customer, tryton_party):
|
||||||
updated_customers.append(customer.id)
|
changes = self._describe_changes(customer, tryton_party)
|
||||||
else:
|
self._update_customer(customer, tryton_party)
|
||||||
untouched_customers.append(customer.id)
|
updated_customers.append(
|
||||||
|
{**self._build_item(customer), "detail": changes}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
untouched_customers.append(self._build_item(customer))
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Error al importar clientes: {e}El cliente: {tryton_party}"
|
||||||
|
)
|
||||||
|
failed_parties.append(
|
||||||
|
self._build_failure(tryton_party, "actualizar", e)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"checked_tryton_parties": checked_tryton_parties,
|
"checked_tryton_parties": checked_tryton_parties,
|
||||||
@@ -43,6 +66,41 @@ class CustomerTrytonService:
|
|||||||
"untouched_customers": untouched_customers,
|
"untouched_customers": untouched_customers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _build_item(self, customer):
|
||||||
|
"""Construye el objeto de un cliente sincronizado"""
|
||||||
|
return {"id": customer.id, "name": customer.name}
|
||||||
|
|
||||||
|
def _describe_changes(self, customer, tryton_party):
|
||||||
|
"""Describe los cambios detectados entre el cliente local y Tryton"""
|
||||||
|
changes = []
|
||||||
|
name = tryton_party.get("name")
|
||||||
|
if customer.name != name:
|
||||||
|
changes.append(f"Nombre: {customer.name} → {name}")
|
||||||
|
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
||||||
|
new_address = str(tryton_party.get("addresses")[0])
|
||||||
|
if customer.address_external_id != new_address:
|
||||||
|
changes.append(
|
||||||
|
f"Dirección: {customer.address_external_id} → {new_address}"
|
||||||
|
)
|
||||||
|
return ", ".join(changes)
|
||||||
|
|
||||||
|
def _build_failure(self, tryton_party, operation, exc):
|
||||||
|
"""Construye el detalle de un cliente que falló al sincronizar"""
|
||||||
|
error = self._friendly_error(tryton_party, str(exc))
|
||||||
|
return {
|
||||||
|
"external_id": tryton_party.get("id"),
|
||||||
|
"name": tryton_party.get("name"),
|
||||||
|
"error": f"Error al {operation} cliente: {error}",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _friendly_error(self, tryton_party, message):
|
||||||
|
"""Traduce causas conocidas de error a mensajes legibles"""
|
||||||
|
if "name" in message and (
|
||||||
|
"not-null constraint" in message or "NOT NULL constraint" in message
|
||||||
|
):
|
||||||
|
return "El cliente no tiene nombre en Tryton"
|
||||||
|
return message
|
||||||
|
|
||||||
def _get_party_details(self, party_ids, context):
|
def _get_party_details(self, party_ids, context):
|
||||||
"""Obtiene detalles de clientes desde Tryton"""
|
"""Obtiene detalles de clientes desde Tryton"""
|
||||||
tryton_fields = ["id", "name", "addresses"]
|
tryton_fields = ["id", "name", "addresses"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from ...models.products import Product
|
from ...models.products import Product, ProductCategory
|
||||||
|
|
||||||
|
|
||||||
class ProductTrytonService:
|
class ProductTrytonService:
|
||||||
@@ -21,7 +21,23 @@ class ProductTrytonService:
|
|||||||
product_ids = self.client.call(method, params)
|
product_ids = self.client.call(method, params)
|
||||||
tryton_products = self._get_product_details(product_ids, context)
|
tryton_products = self._get_product_details(product_ids, context)
|
||||||
|
|
||||||
checked_tryton_products = product_ids
|
try:
|
||||||
|
(
|
||||||
|
categories_by_tryton_id,
|
||||||
|
category_report,
|
||||||
|
) = self._import_categories(tryton_products, context)
|
||||||
|
created_categories = category_report["created"]
|
||||||
|
updated_categories = category_report["updated"]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al importar categorías: {e}")
|
||||||
|
categories_by_tryton_id = {}
|
||||||
|
created_categories = []
|
||||||
|
updated_categories = []
|
||||||
|
|
||||||
|
checked_tryton_products = [
|
||||||
|
{"id": p.get("id"), "name": p.get("name")}
|
||||||
|
for p in tryton_products
|
||||||
|
]
|
||||||
failed_products = []
|
failed_products = []
|
||||||
updated_products = []
|
updated_products = []
|
||||||
created_products = []
|
created_products = []
|
||||||
@@ -29,24 +45,50 @@ class ProductTrytonService:
|
|||||||
|
|
||||||
for tryton_product in tryton_products:
|
for tryton_product in tryton_products:
|
||||||
try:
|
try:
|
||||||
product = Product.objects.get(external_id=tryton_product.get("id"))
|
product = Product.objects.get(
|
||||||
|
external_id=tryton_product.get("id")
|
||||||
|
)
|
||||||
except Product.DoesNotExist:
|
except Product.DoesNotExist:
|
||||||
try:
|
try:
|
||||||
product = self._create_product(tryton_product)
|
product = self._create_product(tryton_product)
|
||||||
created_products.append(product.id)
|
self._sync_product_categories(
|
||||||
|
product, tryton_product, categories_by_tryton_id
|
||||||
|
)
|
||||||
|
created_products.append(self._build_item(product))
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"Error al importar productos: {e}El producto: {tryton_product}"
|
f"Error al importar productos: {e}El producto: {tryton_product}"
|
||||||
)
|
)
|
||||||
failed_products.append(tryton_product.get("id"))
|
failed_products.append(
|
||||||
|
self._build_failure(tryton_product, "crear", e)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._need_update(product, tryton_product):
|
try:
|
||||||
self._update_product(product, tryton_product)
|
if self._need_update(product, tryton_product):
|
||||||
updated_products.append(product.id)
|
changes = self._describe_changes(
|
||||||
else:
|
product, tryton_product
|
||||||
untouched_products.append(product.id)
|
)
|
||||||
|
self._update_product(product, tryton_product)
|
||||||
|
self._sync_product_categories(
|
||||||
|
product, tryton_product, categories_by_tryton_id
|
||||||
|
)
|
||||||
|
updated_products.append(
|
||||||
|
{**self._build_item(product), "detail": changes}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._sync_product_categories(
|
||||||
|
product, tryton_product, categories_by_tryton_id
|
||||||
|
)
|
||||||
|
untouched_products.append(self._build_item(product))
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Error al importar productos: {e}El producto: {tryton_product}"
|
||||||
|
)
|
||||||
|
failed_products.append(
|
||||||
|
self._build_failure(tryton_product, "actualizar", e)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"checked_tryton_products": checked_tryton_products,
|
"checked_tryton_products": checked_tryton_products,
|
||||||
@@ -54,8 +96,155 @@ class ProductTrytonService:
|
|||||||
"updated_products": updated_products,
|
"updated_products": updated_products,
|
||||||
"created_products": created_products,
|
"created_products": created_products,
|
||||||
"untouched_products": untouched_products,
|
"untouched_products": untouched_products,
|
||||||
|
"created_categories": created_categories,
|
||||||
|
"updated_categories": updated_categories,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _import_categories(self, tryton_products, context):
|
||||||
|
"""Importa las categorías de Tryton usadas por los productos
|
||||||
|
sincronizados. Devuelve un mapa {id_tryton: ProductCategory} y un
|
||||||
|
reporte {"created": [...], "updated": [...]}
|
||||||
|
"""
|
||||||
|
empty_report = {"created": [], "updated": []}
|
||||||
|
category_ids = {
|
||||||
|
category_id
|
||||||
|
for tryton_product in tryton_products
|
||||||
|
for category_id in self._extract_category_ids(tryton_product)
|
||||||
|
}
|
||||||
|
if not category_ids:
|
||||||
|
return {}, empty_report
|
||||||
|
|
||||||
|
categories = {}
|
||||||
|
report = {"created": [], "updated": []}
|
||||||
|
tryton_categories = self._get_category_details(
|
||||||
|
sorted(category_ids), context
|
||||||
|
)
|
||||||
|
for tryton_category in tryton_categories:
|
||||||
|
try:
|
||||||
|
external_id = str(tryton_category.get("id"))
|
||||||
|
name = tryton_category.get("name")
|
||||||
|
category = ProductCategory.objects.filter(
|
||||||
|
external_id=external_id
|
||||||
|
).first()
|
||||||
|
if category is None and name:
|
||||||
|
category = ProductCategory.objects.filter(
|
||||||
|
name=name
|
||||||
|
).first()
|
||||||
|
is_new = category is None
|
||||||
|
if is_new:
|
||||||
|
category = ProductCategory()
|
||||||
|
old_name = category.name
|
||||||
|
old_external_id = category.external_id
|
||||||
|
category.name = name
|
||||||
|
category.external_id = external_id
|
||||||
|
category.save()
|
||||||
|
categories[tryton_category.get("id")] = category
|
||||||
|
|
||||||
|
item = {
|
||||||
|
"id": category.id,
|
||||||
|
"name": category.name,
|
||||||
|
"external_id": category.external_id,
|
||||||
|
}
|
||||||
|
if is_new:
|
||||||
|
report["created"].append(item)
|
||||||
|
continue
|
||||||
|
changes = []
|
||||||
|
if old_name != category.name:
|
||||||
|
changes.append(f"Nombre: {old_name} → {category.name}")
|
||||||
|
if old_external_id != category.external_id:
|
||||||
|
changes.append(
|
||||||
|
"External ID: "
|
||||||
|
f"{old_external_id} → {category.external_id}"
|
||||||
|
)
|
||||||
|
if changes:
|
||||||
|
report["updated"].append(
|
||||||
|
{**item, "detail": ", ".join(changes)}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Error al importar categoría: {e}"
|
||||||
|
f"La categoría: {tryton_category}"
|
||||||
|
)
|
||||||
|
return categories, report
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_category_ids(tryton_product):
|
||||||
|
"""Extrae los IDs de categorías de la respuesta RPC de Tryton
|
||||||
|
(los campos con puntos llegan agrupados bajo la clave 'template.')"""
|
||||||
|
template = tryton_product.get("template.") or {}
|
||||||
|
return template.get("categories") or []
|
||||||
|
|
||||||
|
def _get_category_details(self, category_ids, context):
|
||||||
|
"""Obtiene los nombres de categorías desde Tryton"""
|
||||||
|
method = "model.product.category.read"
|
||||||
|
params = (category_ids, ["id", "name"], context)
|
||||||
|
return self.client.call(method, params)
|
||||||
|
|
||||||
|
def _sync_product_categories(self, product, tryton_product, categories):
|
||||||
|
"""Vincula al producto las categorías importadas desde Tryton"""
|
||||||
|
category_ids = self._extract_category_ids(tryton_product)
|
||||||
|
product.categories.set(
|
||||||
|
[
|
||||||
|
categories[category_id]
|
||||||
|
for category_id in category_ids
|
||||||
|
if category_id in categories
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_item(self, product):
|
||||||
|
"""Construye el objeto de un producto sincronizado"""
|
||||||
|
return {"id": product.id, "name": product.name}
|
||||||
|
|
||||||
|
def _describe_changes(self, product, tryton_product):
|
||||||
|
"""Describe los cambios detectados entre el producto local y Tryton"""
|
||||||
|
changes = []
|
||||||
|
name = tryton_product.get("name")
|
||||||
|
if product.name != name:
|
||||||
|
changes.append(f"Nombre: {product.name} → {name}")
|
||||||
|
price = tryton_product.get("list_price")
|
||||||
|
if product.price != price:
|
||||||
|
changes.append(
|
||||||
|
f"Precio: {self._format_price(product.price)} → "
|
||||||
|
f"{self._format_price(price)}"
|
||||||
|
)
|
||||||
|
unit = tryton_product.get("default_uom.")
|
||||||
|
if unit:
|
||||||
|
unit_name = unit.get("rec_name")
|
||||||
|
if product.measuring_unit != unit_name:
|
||||||
|
changes.append(
|
||||||
|
f"Unidad: {product.measuring_unit} → {unit_name}"
|
||||||
|
)
|
||||||
|
return ", ".join(changes)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_price(value):
|
||||||
|
"""Formatea un precio sin ceros decimales innecesarios"""
|
||||||
|
return f"{value:.2f}".rstrip("0").rstrip(".")
|
||||||
|
|
||||||
|
def _build_failure(self, tryton_product, operation, exc):
|
||||||
|
"""Construye el detalle de un producto que falló al sincronizar"""
|
||||||
|
error = self._friendly_error(tryton_product, str(exc))
|
||||||
|
return {
|
||||||
|
"external_id": tryton_product.get("id"),
|
||||||
|
"name": tryton_product.get("name"),
|
||||||
|
"error": f"Error al {operation} producto: {error}",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _friendly_error(self, tryton_product, message):
|
||||||
|
"""Traduce causas conocidas de error a mensajes legibles"""
|
||||||
|
if "price" in message and (
|
||||||
|
"not-null constraint" in message
|
||||||
|
or "NOT NULL constraint" in message
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
"El producto no tiene precio en Tryton (list_price nulo)"
|
||||||
|
)
|
||||||
|
if "name" in message and (
|
||||||
|
"duplicate key" in message or "UNIQUE constraint" in message
|
||||||
|
):
|
||||||
|
return f"Ya existe un producto con el nombre '{tryton_product.get('name')}'"
|
||||||
|
return message
|
||||||
|
|
||||||
def _get_product_details(self, product_ids, context):
|
def _get_product_details(self, product_ids, context):
|
||||||
"""Obtiene detalles de productos desde Tryton"""
|
"""Obtiene detalles de productos desde Tryton"""
|
||||||
tryton_fields = [
|
tryton_fields = [
|
||||||
@@ -64,6 +253,7 @@ class ProductTrytonService:
|
|||||||
"default_uom.id",
|
"default_uom.id",
|
||||||
"default_uom.rec_name",
|
"default_uom.rec_name",
|
||||||
"list_price",
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
]
|
]
|
||||||
method = "model.product.product.read"
|
method = "model.product.product.read"
|
||||||
params = (product_ids, tryton_fields, context)
|
params = (product_ids, tryton_fields, context)
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ class SaleTrytonService:
|
|||||||
external_ids = self.client.call(method, tryton_params)
|
external_ids = self.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(self._build_item(sale))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al enviar la venta: {e}venta_id: {sale.id}")
|
print(f"Error al enviar la venta: {e}venta_id: {sale.id}")
|
||||||
failed.append(sale.id)
|
failed.append(self._build_failure(sale, e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return {"successful": successful, "failed": failed}
|
return {"successful": successful, "failed": failed}
|
||||||
@@ -61,16 +61,28 @@ class SaleTrytonService:
|
|||||||
external_ids = self.client.call(method, tryton_params)
|
external_ids = self.client.call(method, tryton_params)
|
||||||
catalog_sale.external_id = external_ids[0]
|
catalog_sale.external_id = external_ids[0]
|
||||||
catalog_sale.save()
|
catalog_sale.save()
|
||||||
successful.append(catalog_sale.id)
|
successful.append(self._build_item(catalog_sale))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"Error al enviar catalog sale: {e}, catalog_sale_id: {catalog_sale.id}"
|
f"Error al enviar catalog sale: {e}, catalog_sale_id: {catalog_sale.id}"
|
||||||
)
|
)
|
||||||
failed.append(catalog_sale.id)
|
failed.append(self._build_failure(catalog_sale, e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return {"successful": successful, "failed": failed}
|
return {"successful": successful, "failed": failed}
|
||||||
|
|
||||||
|
def _build_item(self, sale):
|
||||||
|
"""Construye el objeto de una venta sincronizada"""
|
||||||
|
return {"id": sale.id, "code": sale.code}
|
||||||
|
|
||||||
|
def _build_failure(self, sale, exc):
|
||||||
|
"""Construye el detalle de una venta que falló al sincronizar"""
|
||||||
|
return {
|
||||||
|
"id": sale.id,
|
||||||
|
"code": sale.code,
|
||||||
|
"error": f"Error al enviar la venta: {exc}",
|
||||||
|
}
|
||||||
|
|
||||||
def _catalog_sale_to_tryton_params(self, catalog_sale, lines, tryton_context):
|
def _catalog_sale_to_tryton_params(self, catalog_sale, lines, tryton_context):
|
||||||
"""Convierte catalog sale a parámetros para Tryton"""
|
"""Convierte catalog sale a parámetros para Tryton"""
|
||||||
sale_tryton = TrytonCatalogSale(catalog_sale, lines)
|
sale_tryton = TrytonCatalogSale(catalog_sale, lines)
|
||||||
|
|||||||
@@ -62,11 +62,25 @@ class TestCustomersFromTryton(TestCase, LoginMixin):
|
|||||||
|
|
||||||
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": [
|
||||||
"created_customers": [3, 4],
|
{"id": 5, "name": "Carlos"},
|
||||||
"untouched_customers": [2],
|
{"id": 6, "name": "Cristian"},
|
||||||
|
{"id": 7, "name": "Ana"},
|
||||||
|
{"id": 8, "name": "José"},
|
||||||
|
],
|
||||||
|
"created_customers": [
|
||||||
|
{"id": 3, "name": "Ana"},
|
||||||
|
{"id": 4, "name": "José"},
|
||||||
|
],
|
||||||
|
"untouched_customers": [{"id": 2, "name": "Cristian"}],
|
||||||
"failed_parties": [],
|
"failed_parties": [],
|
||||||
"updated_customers": [1],
|
"updated_customers": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Carlos",
|
||||||
|
"detail": "Nombre: Calos → Carlos, Dirección: None → 303",
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
self.assertEqual(content, expected_response)
|
self.assertEqual(content, expected_response)
|
||||||
|
|
||||||
@@ -79,3 +93,115 @@ class TestCustomersFromTryton(TestCase, LoginMixin):
|
|||||||
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))
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_create_failure_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, 9]
|
||||||
|
|
||||||
|
party_read = "model.party.party.read"
|
||||||
|
read_args = (
|
||||||
|
[5, 9],
|
||||||
|
["id", "name", "addresses"],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (party_read, read_args):
|
||||||
|
return [
|
||||||
|
{"id": 5, "name": "Carlos", "addresses": [303]},
|
||||||
|
{"id": 9, "name": None, "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"))
|
||||||
|
self.assertEqual(len(content["failed_parties"]), 1)
|
||||||
|
failure = content["failed_parties"][0]
|
||||||
|
self.assertEqual(failure["external_id"], 9)
|
||||||
|
self.assertEqual(failure["name"], None)
|
||||||
|
self.assertEqual(
|
||||||
|
failure["error"],
|
||||||
|
"Error al crear cliente: El cliente no tiene nombre en Tryton",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
content["updated_customers"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Carlos",
|
||||||
|
"detail": "Nombre: Calos → Carlos, Dirección: None → 303",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_update_failure_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]
|
||||||
|
|
||||||
|
party_read = "model.party.party.read"
|
||||||
|
read_args = (
|
||||||
|
[5, 6],
|
||||||
|
["id", "name", "addresses"],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (party_read, read_args):
|
||||||
|
return [
|
||||||
|
{"id": 5, "name": "Carlos", "addresses": [303]},
|
||||||
|
{"id": 6, "name": None, "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"))
|
||||||
|
self.assertEqual(len(content["failed_parties"]), 1)
|
||||||
|
failure = content["failed_parties"][0]
|
||||||
|
self.assertEqual(failure["external_id"], 6)
|
||||||
|
self.assertIn("Error al actualizar cliente", failure["error"])
|
||||||
|
self.assertEqual(
|
||||||
|
content["updated_customers"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Carlos",
|
||||||
|
"detail": "Nombre: Calos → Carlos, Dirección: None → 303",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|||||||
@@ -79,7 +79,13 @@ class TestSendSalesToTryton(TestCase, LoginMixin):
|
|||||||
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(
|
self.assertEqual(
|
||||||
content, {"successful": [self.sale.id], "failed": []}
|
content,
|
||||||
|
{
|
||||||
|
"successful": [
|
||||||
|
{"id": self.sale.id, "code": self.sale.code}
|
||||||
|
],
|
||||||
|
"failed": [],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_sale = Sale.objects.get(id=self.sale.id)
|
updated_sale = Sale.objects.get(id=self.sale.id)
|
||||||
@@ -165,7 +171,15 @@ class TestSendSalesToTryton(TestCase, LoginMixin):
|
|||||||
content = json.loads(response.content.decode("utf-8"))
|
content = json.loads(response.content.decode("utf-8"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
content,
|
content,
|
||||||
{"successful": [self.catalog_sale.id], "failed": []},
|
{
|
||||||
|
"successful": [
|
||||||
|
{
|
||||||
|
"id": self.catalog_sale.id,
|
||||||
|
"code": self.catalog_sale.code,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failed": [],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
updated = CatalogSale.objects.get(id=self.catalog_sale.id)
|
updated = CatalogSale.objects.get(id=self.catalog_sale.id)
|
||||||
@@ -247,3 +261,51 @@ class TestSendSalesToTryton(TestCase, LoginMixin):
|
|||||||
lines[1]["unit_price"],
|
lines[1]["unit_price"],
|
||||||
{"__class__": "Decimal", "decimal": "5000.00"},
|
{"__class__": "Decimal", "decimal": "5000.00"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch("don_confiao.api.sales.get_tryton_client")
|
||||||
|
def test_send_sales_to_tryton_failure(self, mock_get_tryton_client):
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.call.side_effect = Exception("error de tryton")
|
||||||
|
mock_get_tryton_client.return_value = mock_client
|
||||||
|
|
||||||
|
url = "/don_confiao/api/enviar_ventas_a_tryton"
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = json.loads(response.content.decode("utf-8"))
|
||||||
|
self.assertEqual(content["successful"], [])
|
||||||
|
self.assertEqual(len(content["failed"]), 1)
|
||||||
|
failure = content["failed"][0]
|
||||||
|
self.assertEqual(failure["id"], self.sale.id)
|
||||||
|
self.assertEqual(failure["code"], self.sale.code)
|
||||||
|
self.assertEqual(
|
||||||
|
failure["error"], "Error al enviar la venta: error de tryton"
|
||||||
|
)
|
||||||
|
|
||||||
|
sale = Sale.objects.get(id=self.sale.id)
|
||||||
|
self.assertIsNone(sale.external_id)
|
||||||
|
|
||||||
|
@patch("don_confiao.api.sales.get_tryton_client")
|
||||||
|
def test_send_catalog_sales_to_tryton_failure(
|
||||||
|
self, mock_get_tryton_client
|
||||||
|
):
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.call.side_effect = Exception("error de tryton")
|
||||||
|
mock_get_tryton_client.return_value = mock_client
|
||||||
|
|
||||||
|
url = "/don_confiao/api/enviar_catalog_sales_a_tryton"
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = json.loads(response.content.decode("utf-8"))
|
||||||
|
self.assertEqual(content["successful"], [])
|
||||||
|
self.assertEqual(len(content["failed"]), 1)
|
||||||
|
failure = content["failed"][0]
|
||||||
|
self.assertEqual(failure["id"], self.catalog_sale.id)
|
||||||
|
self.assertEqual(failure["code"], self.catalog_sale.code)
|
||||||
|
self.assertEqual(
|
||||||
|
failure["error"], "Error al enviar la venta: error de tryton"
|
||||||
|
)
|
||||||
|
|
||||||
|
catalog_sale = CatalogSale.objects.get(id=self.catalog_sale.id)
|
||||||
|
self.assertIsNone(catalog_sale.external_id)
|
||||||
|
|||||||
129
tienda_ilusion/don_confiao/tests/test_geography_models.py
Normal file
129
tienda_ilusion/don_confiao/tests/test_geography_models.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
from django.db import IntegrityError, transaction
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from ..models.geography import Country, Department, Municipality
|
||||||
|
|
||||||
|
|
||||||
|
class TestCountryModel(TestCase):
|
||||||
|
def test_create_country(self):
|
||||||
|
country = Country.objects.create(name="Colombia", code="CO")
|
||||||
|
self.assertEqual(country.name, "Colombia")
|
||||||
|
self.assertEqual(country.code, "CO")
|
||||||
|
self.assertEqual(str(country), "Colombia")
|
||||||
|
|
||||||
|
def test_country_name_is_unique(self):
|
||||||
|
Country.objects.create(name="Colombia", code="CO")
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Country.objects.create(name="Colombia", code="CO2")
|
||||||
|
|
||||||
|
def test_country_code_is_unique(self):
|
||||||
|
Country.objects.create(name="Colombia", code="CO")
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Country.objects.create(name="Colombia2", code="CO")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDepartmentModel(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.country = Country.objects.create(name="Colombia", code="CO")
|
||||||
|
|
||||||
|
def test_create_department(self):
|
||||||
|
department = Department.objects.create(
|
||||||
|
name="Cundinamarca", country=self.country
|
||||||
|
)
|
||||||
|
self.assertEqual(department.country, self.country)
|
||||||
|
|
||||||
|
def test_department_requires_country(self):
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Department.objects.create(name="Antioquia")
|
||||||
|
|
||||||
|
def test_department_name_is_unique(self):
|
||||||
|
Department.objects.create(
|
||||||
|
name="Cundinamarca", country=self.country
|
||||||
|
)
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Department.objects.create(
|
||||||
|
name="Cundinamarca", country=self.country
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMunicipalityModel(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.country = Country.objects.create(name="Colombia", code="CO")
|
||||||
|
self.department = Department.objects.create(
|
||||||
|
name="Cundinamarca", country=self.country
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_municipality(self):
|
||||||
|
municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
self.assertEqual(municipality.department, self.department)
|
||||||
|
self.assertEqual(municipality.country, self.country)
|
||||||
|
self.assertEqual(municipality.department.country.name, "Colombia")
|
||||||
|
self.assertEqual(str(municipality), "La Mesa")
|
||||||
|
|
||||||
|
def test_municipality_geolocation_fields(self):
|
||||||
|
municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
latitude=4.63092,
|
||||||
|
longitude=-74.39152,
|
||||||
|
)
|
||||||
|
self.assertEqual(municipality.latitude, 4.63092)
|
||||||
|
self.assertEqual(municipality.longitude, -74.39152)
|
||||||
|
|
||||||
|
def test_municipality_geolocation_optional(self):
|
||||||
|
municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
self.assertIsNone(municipality.latitude)
|
||||||
|
self.assertIsNone(municipality.longitude)
|
||||||
|
|
||||||
|
def test_municipality_requires_department_and_country(self):
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Municipality.objects.create(name="La Mesa")
|
||||||
|
|
||||||
|
def test_municipality_name_is_unique_per_department(self):
|
||||||
|
Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_municipality_same_name_allowed_in_different_department(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
department2 = Department.objects.create(
|
||||||
|
name="Antioquia", country=self.country
|
||||||
|
)
|
||||||
|
Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
municipality2 = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=department2,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
Municipality.objects.filter(name="La Mesa").count(), 2
|
||||||
|
)
|
||||||
|
self.assertEqual(municipality2.department, department2)
|
||||||
@@ -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.products import Product
|
from ..models.products import Product, ProductCategory
|
||||||
from .Mixins import LoginMixin
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
"default_uom.id",
|
"default_uom.id",
|
||||||
"default_uom.rec_name",
|
"default_uom.rec_name",
|
||||||
"list_price",
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
],
|
],
|
||||||
{"company": 1},
|
{"company": 1},
|
||||||
)
|
)
|
||||||
@@ -65,18 +66,21 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
"list_price": Decimal("25000"),
|
"list_price": Decimal("25000"),
|
||||||
"name": "Producto 1",
|
"name": "Producto 1",
|
||||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 191,
|
"id": 191,
|
||||||
"list_price": Decimal("6000"),
|
"list_price": Decimal("6000"),
|
||||||
"name": "Panela2",
|
"name": "Panela2",
|
||||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 192,
|
"id": 192,
|
||||||
"list_price": Decimal("4500"),
|
"list_price": Decimal("4500"),
|
||||||
"name": "Papa",
|
"name": "Papa",
|
||||||
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -92,11 +96,23 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
|
|
||||||
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": [
|
||||||
"created_products": [3],
|
{"id": 190, "name": "Producto 1"},
|
||||||
"untouched_products": [2],
|
{"id": 191, "name": "Panela2"},
|
||||||
|
{"id": 192, "name": "Papa"},
|
||||||
|
],
|
||||||
|
"created_products": [{"id": 3, "name": "Producto 1"}],
|
||||||
|
"untouched_products": [{"id": 2, "name": "Papa"}],
|
||||||
"failed_products": [],
|
"failed_products": [],
|
||||||
"updated_products": [1],
|
"updated_products": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Panela2",
|
||||||
|
"detail": "Nombre: Panela → Panela2, Precio: 5000 → 6000, Unidad: UNIT → Unit",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_categories": [],
|
||||||
|
"updated_categories": [],
|
||||||
}
|
}
|
||||||
self.assertEqual(content, expected_response)
|
self.assertEqual(content, expected_response)
|
||||||
|
|
||||||
@@ -140,6 +156,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
"default_uom.id",
|
"default_uom.id",
|
||||||
"default_uom.rec_name",
|
"default_uom.rec_name",
|
||||||
"list_price",
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
],
|
],
|
||||||
{"company": 1},
|
{"company": 1},
|
||||||
)
|
)
|
||||||
@@ -150,6 +167,7 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
"list_price": Decimal("25000"),
|
"list_price": Decimal("25000"),
|
||||||
"name": self.product.name,
|
"name": self.product.name,
|
||||||
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -165,10 +183,564 @@ class TestProductsFromTryton(TestCase, LoginMixin):
|
|||||||
|
|
||||||
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": [{"id": 200, "name": "Panela"}],
|
||||||
"created_products": [],
|
"created_products": [],
|
||||||
"untouched_products": [],
|
"untouched_products": [],
|
||||||
"failed_products": [200],
|
"failed_products": [
|
||||||
|
{
|
||||||
|
"external_id": 200,
|
||||||
|
"name": "Panela",
|
||||||
|
"error": "Error al crear producto: Ya existe un producto con el nombre 'Panela'",
|
||||||
|
}
|
||||||
|
],
|
||||||
"updated_products": [],
|
"updated_products": [],
|
||||||
|
"created_categories": [],
|
||||||
|
"updated_categories": [],
|
||||||
}
|
}
|
||||||
self.assertEqual(content, expected_response)
|
self.assertEqual(content, expected_response)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_import_null_price_product(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 [201]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[201],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (product_read, product_args):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": 201,
|
||||||
|
"list_price": None,
|
||||||
|
"name": "ENVASES Y EMPAQUES",
|
||||||
|
"default_uom.": {"id": 1, "rec_name": "Unit"},
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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"))
|
||||||
|
self.assertEqual(content["created_products"], [])
|
||||||
|
self.assertEqual(len(content["failed_products"]), 1)
|
||||||
|
failure = content["failed_products"][0]
|
||||||
|
self.assertEqual(failure["external_id"], 201)
|
||||||
|
self.assertEqual(failure["name"], "ENVASES Y EMPAQUES")
|
||||||
|
self.assertEqual(
|
||||||
|
failure["error"],
|
||||||
|
"Error al crear producto: El producto no tiene precio en Tryton (list_price nulo)",
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_update_failure_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 [191, 192]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[191, 192],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (product_read, product_args):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": 191,
|
||||||
|
"list_price": Decimal("6000"),
|
||||||
|
"name": "Panela",
|
||||||
|
"default_uom.": None,
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 192,
|
||||||
|
"list_price": Decimal("4500"),
|
||||||
|
"name": "Papa",
|
||||||
|
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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"))
|
||||||
|
self.assertEqual(len(content["failed_products"]), 1)
|
||||||
|
failure = content["failed_products"][0]
|
||||||
|
self.assertEqual(failure["external_id"], 191)
|
||||||
|
self.assertEqual(failure["name"], "Panela")
|
||||||
|
self.assertIn("Error al actualizar producto", failure["error"])
|
||||||
|
self.assertEqual(content["untouched_products"], [{"id": 2, "name": "Papa"}])
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_import_products_with_categories(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]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[190],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"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"},
|
||||||
|
"template.": {"id": 999, "categories": [5, 8]},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
category_read = "model.product.category.read"
|
||||||
|
category_args = (
|
||||||
|
[5, 8],
|
||||||
|
["id", "name"],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (category_read, category_args):
|
||||||
|
return [
|
||||||
|
{"id": 5, "name": "Abarrotes"},
|
||||||
|
{"id": 8, "name": "Bebidas"},
|
||||||
|
]
|
||||||
|
|
||||||
|
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"))
|
||||||
|
self.assertEqual(content["failed_products"], [])
|
||||||
|
self.assertEqual(
|
||||||
|
content["created_products"], [{"id": 3, "name": "Producto 1"}]
|
||||||
|
)
|
||||||
|
self.assertEqual(content["updated_categories"], [])
|
||||||
|
self.assertEqual(
|
||||||
|
content["created_categories"],
|
||||||
|
[
|
||||||
|
{"id": 1, "name": "Abarrotes", "external_id": "5"},
|
||||||
|
{"id": 2, "name": "Bebidas", "external_id": "8"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
created_product = Product.objects.get(name="Producto 1")
|
||||||
|
self.assertEqual(created_product.external_id, str(190))
|
||||||
|
categories = created_product.categories.order_by("external_id")
|
||||||
|
self.assertEqual(
|
||||||
|
[(c.external_id, c.name) for c in categories],
|
||||||
|
[("5", "Abarrotes"), ("8", "Bebidas")],
|
||||||
|
)
|
||||||
|
self.assertEqual(ProductCategory.objects.count(), 2)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_import_updates_category_name_from_tryton(
|
||||||
|
self, mock_connect, mock_call
|
||||||
|
):
|
||||||
|
mock_connect.return_value = None
|
||||||
|
local_category = ProductCategory.objects.create(
|
||||||
|
name="Granos", external_id="7"
|
||||||
|
)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[190],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"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"},
|
||||||
|
"template.": {"id": 999, "categories": [7]},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
category_read = "model.product.category.read"
|
||||||
|
category_args = (
|
||||||
|
[7],
|
||||||
|
["id", "name"],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (category_read, category_args):
|
||||||
|
return [
|
||||||
|
{"id": 7, "name": "Cereales"},
|
||||||
|
]
|
||||||
|
|
||||||
|
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"))
|
||||||
|
self.assertEqual(content["created_categories"], [])
|
||||||
|
self.assertEqual(
|
||||||
|
content["updated_categories"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": local_category.pk,
|
||||||
|
"name": "Cereales",
|
||||||
|
"external_id": "7",
|
||||||
|
"detail": "Nombre: Granos → Cereales",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
refreshed_category = ProductCategory.objects.get(
|
||||||
|
pk=local_category.pk
|
||||||
|
)
|
||||||
|
self.assertEqual(refreshed_category.name, "Cereales")
|
||||||
|
self.assertEqual(refreshed_category.external_id, "7")
|
||||||
|
self.assertEqual(ProductCategory.objects.count(), 1)
|
||||||
|
|
||||||
|
created_product = Product.objects.get(name="Producto 1")
|
||||||
|
self.assertEqual(
|
||||||
|
list(created_product.categories.all()), [refreshed_category]
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_import_links_existing_category_by_name(
|
||||||
|
self, mock_connect, mock_call
|
||||||
|
):
|
||||||
|
mock_connect.return_value = None
|
||||||
|
existing_category = ProductCategory.objects.create(name="Abarrotes")
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[190],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"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"},
|
||||||
|
"template.": {"id": 999, "categories": [5]},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
category_read = "model.product.category.read"
|
||||||
|
category_args = (
|
||||||
|
[5],
|
||||||
|
["id", "name"],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (category_read, category_args):
|
||||||
|
return [
|
||||||
|
{"id": 5, "name": "Abarrotes"},
|
||||||
|
]
|
||||||
|
|
||||||
|
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"))
|
||||||
|
self.assertEqual(content["created_categories"], [])
|
||||||
|
self.assertEqual(
|
||||||
|
content["updated_categories"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": existing_category.pk,
|
||||||
|
"name": "Abarrotes",
|
||||||
|
"external_id": "5",
|
||||||
|
"detail": "External ID: None → 5",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ProductCategory.objects.filter(name="Abarrotes").count(), 1
|
||||||
|
)
|
||||||
|
linked_category = ProductCategory.objects.get(
|
||||||
|
pk=existing_category.pk
|
||||||
|
)
|
||||||
|
self.assertEqual(linked_category.external_id, "5")
|
||||||
|
|
||||||
|
created_product = Product.objects.get(name="Producto 1")
|
||||||
|
self.assertEqual(
|
||||||
|
list(created_product.categories.all()), [linked_category]
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_import_product_without_categories(
|
||||||
|
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]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[190],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"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"},
|
||||||
|
"template.": {"id": 999, "categories": []},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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"))
|
||||||
|
self.assertEqual(content["failed_products"], [])
|
||||||
|
self.assertEqual(content["created_categories"], [])
|
||||||
|
self.assertEqual(content["updated_categories"], [])
|
||||||
|
created_product = Product.objects.get(name="Producto 1")
|
||||||
|
self.assertEqual(created_product.categories.count(), 0)
|
||||||
|
self.assertEqual(ProductCategory.objects.count(), 0)
|
||||||
|
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.call")
|
||||||
|
@patch("sabatron_tryton_rpc_client.client.Client.connect")
|
||||||
|
def test_import_assigns_categories_to_untouched_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 [192]
|
||||||
|
|
||||||
|
product_read = "model.product.product.read"
|
||||||
|
product_args = (
|
||||||
|
[192],
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"default_uom.id",
|
||||||
|
"default_uom.rec_name",
|
||||||
|
"list_price",
|
||||||
|
"template.categories",
|
||||||
|
],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (product_read, product_args):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": 192,
|
||||||
|
"list_price": Decimal("4500"),
|
||||||
|
"name": "Papa",
|
||||||
|
"default_uom.": {"id": 2, "rec_name": "Kilogram"},
|
||||||
|
"template.": {"id": 999, "categories": [9]},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
category_read = "model.product.category.read"
|
||||||
|
category_args = (
|
||||||
|
[9],
|
||||||
|
["id", "name"],
|
||||||
|
{"company": 1},
|
||||||
|
)
|
||||||
|
if args == (category_read, category_args):
|
||||||
|
return [
|
||||||
|
{"id": 9, "name": "Tubérculos"},
|
||||||
|
]
|
||||||
|
|
||||||
|
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"))
|
||||||
|
self.assertEqual(
|
||||||
|
content["untouched_products"], [{"id": 2, "name": "Papa"}]
|
||||||
|
)
|
||||||
|
|
||||||
|
untouched_product = Product.objects.get(id=2)
|
||||||
|
categories = untouched_product.categories.all()
|
||||||
|
self.assertEqual(len(categories), 1)
|
||||||
|
self.assertEqual(categories[0].name, "Tubérculos")
|
||||||
|
self.assertEqual(categories[0].external_id, "9")
|
||||||
|
self.assertEqual(content["updated_categories"], [])
|
||||||
|
self.assertEqual(
|
||||||
|
content["created_categories"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": categories[0].id,
|
||||||
|
"name": "Tubérculos",
|
||||||
|
"external_id": "9",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|||||||
399
tienda_ilusion/don_confiao/tests/test_provenance_api.py
Normal file
399
tienda_ilusion/don_confiao/tests/test_provenance_api.py
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient, APITestCase
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from ..models.geography import Country, Department, Municipality
|
||||||
|
from ..models.provenance import Organization, Supplier
|
||||||
|
from ..models.products import Product
|
||||||
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
|
|
||||||
|
def _create_user(username, user_type):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=username, password="password123"
|
||||||
|
)
|
||||||
|
user.profile.user_type = user_type
|
||||||
|
user.profile.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def _client_for(user):
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
client = APIClient()
|
||||||
|
client.credentials(
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||||
|
)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
class TestProvenanceAPIPermissions(APITestCase, LoginMixin):
|
||||||
|
resources = [
|
||||||
|
"/don_confiao/api/organizations/",
|
||||||
|
"/don_confiao/api/suppliers/",
|
||||||
|
"/don_confiao/api/countries/",
|
||||||
|
"/don_confiao/api/departments/",
|
||||||
|
"/don_confiao/api/municipalities/",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_anonymous_get_is_forbidden(self):
|
||||||
|
for url in self.resources:
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
status.HTTP_401_UNAUTHORIZED,
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_anonymous_write_is_forbidden(self):
|
||||||
|
for url in self.resources:
|
||||||
|
response = self.client.post(
|
||||||
|
url, {"name": "x"}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
status.HTTP_401_UNAUTHORIZED,
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_normal_user_can_read(self):
|
||||||
|
user = _create_user("normal", "user")
|
||||||
|
client = _client_for(user)
|
||||||
|
for url in self.resources:
|
||||||
|
response = client.get(url)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code, status.HTTP_200_OK, url
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_publico_user_cannot_read(self):
|
||||||
|
user = _create_user("publico", "publico")
|
||||||
|
client = _client_for(user)
|
||||||
|
for url in self.resources:
|
||||||
|
response = client.get(url)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code, status.HTTP_403_FORBIDDEN, url
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_normal_user_cannot_write(self):
|
||||||
|
user = _create_user("normal", "user")
|
||||||
|
client = _client_for(user)
|
||||||
|
for url in self.resources:
|
||||||
|
response = client.post(
|
||||||
|
url, {"name": "x"}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code, status.HTTP_403_FORBIDDEN, url
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_publico_user_cannot_write(self):
|
||||||
|
user = _create_user("publico", "publico")
|
||||||
|
client = _client_for(user)
|
||||||
|
for url in self.resources:
|
||||||
|
response = client.post(
|
||||||
|
url, {"name": "x"}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code, status.HTTP_403_FORBIDDEN, url
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProvenanceCRUD(APITestCase, LoginMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.login()
|
||||||
|
self.country = Country.objects.create(name="Colombia", code="CO")
|
||||||
|
self.department = Department.objects.create(
|
||||||
|
name="Cundinamarca", country=self.country
|
||||||
|
)
|
||||||
|
self.municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
self.organization = Organization.objects.create(name="Asociación")
|
||||||
|
self.supplier = Supplier.objects.create(
|
||||||
|
name="Proveedor 1",
|
||||||
|
organization=self.organization,
|
||||||
|
municipality=self.municipality,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_can_create_organization(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/don_confiao/api/organizations/",
|
||||||
|
{"name": "Org 2"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(
|
||||||
|
Organization.objects.filter(name="Org 2").count(), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_can_update_organization(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/don_confiao/api/organizations/{self.organization.id}/",
|
||||||
|
{"description": "Nueva descripción"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.organization.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
self.organization.description, "Nueva descripción"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_can_delete_organization(self):
|
||||||
|
response = self.client.delete(
|
||||||
|
f"/don_confiao/api/organizations/{self.organization.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
self.assertFalse(
|
||||||
|
Organization.objects.filter(pk=self.organization.id).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_can_create_supplier(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/don_confiao/api/suppliers/",
|
||||||
|
{
|
||||||
|
"name": "Proveedor 2",
|
||||||
|
"organization": self.organization.id,
|
||||||
|
"municipality": self.municipality.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
supplier = Supplier.objects.get(name="Proveedor 2")
|
||||||
|
self.assertEqual(supplier.organization, self.organization)
|
||||||
|
self.assertEqual(supplier.municipality, self.municipality)
|
||||||
|
|
||||||
|
def test_admin_can_unlink_supplier_organization(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/don_confiao/api/suppliers/{self.supplier.id}/",
|
||||||
|
{"organization": None},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.supplier.refresh_from_db()
|
||||||
|
self.assertIsNone(self.supplier.organization)
|
||||||
|
|
||||||
|
def test_admin_can_unlink_supplier_municipality(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/don_confiao/api/suppliers/{self.supplier.id}/",
|
||||||
|
{"municipality": None},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.supplier.refresh_from_db()
|
||||||
|
self.assertIsNone(self.supplier.municipality)
|
||||||
|
|
||||||
|
def test_admin_can_create_department(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/don_confiao/api/departments/",
|
||||||
|
{"name": "Antioquia", "country": self.country.id},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(
|
||||||
|
Department.objects.filter(name="Antioquia").count(), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_can_create_municipality(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/don_confiao/api/municipalities/",
|
||||||
|
{
|
||||||
|
"name": "El Colegio",
|
||||||
|
"department": self.department.id,
|
||||||
|
"country": self.country.id,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(
|
||||||
|
Municipality.objects.filter(name="El Colegio").count(), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_supplier_serializer_includes_details(self):
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/api/suppliers/{self.supplier.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["organization_detail"]["name"], "Asociación")
|
||||||
|
self.assertEqual(data["municipality_detail"]["name"], "La Mesa")
|
||||||
|
self.assertIn("products", data)
|
||||||
|
|
||||||
|
def test_department_serializer_includes_country_detail(self):
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/api/departments/{self.department.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["country"], self.country.id)
|
||||||
|
self.assertEqual(data["country_detail"]["name"], "Colombia")
|
||||||
|
self.assertEqual(data["country_detail"]["code"], "CO")
|
||||||
|
|
||||||
|
def test_municipality_serializer_includes_fk_details(self):
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/api/municipalities/{self.municipality.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["department"], self.department.id)
|
||||||
|
self.assertEqual(data["department_detail"]["name"], "Cundinamarca")
|
||||||
|
self.assertEqual(data["country"], self.country.id)
|
||||||
|
self.assertEqual(data["country_detail"]["name"], "Colombia")
|
||||||
|
self.assertEqual(data["country_detail"]["code"], "CO")
|
||||||
|
|
||||||
|
def test_municipality_serializer_includes_geolocation(self):
|
||||||
|
self.municipality.latitude = 4.63092
|
||||||
|
self.municipality.longitude = -74.39152
|
||||||
|
self.municipality.save()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/api/municipalities/{self.municipality.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(float(data["latitude"]), 4.63092)
|
||||||
|
self.assertEqual(float(data["longitude"]), -74.39152)
|
||||||
|
|
||||||
|
def test_municipality_serializer_geolocation_optional(self):
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/api/municipalities/{self.municipality.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIsNone(data["latitude"])
|
||||||
|
self.assertIsNone(data["longitude"])
|
||||||
|
|
||||||
|
def test_link_product_to_suppliers_via_product_api(self):
|
||||||
|
product = Product.objects.create(name="Panela", price=5000)
|
||||||
|
supplier2 = Supplier.objects.create(name="Proveedor 2")
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/don_confiao/api/products/{product.id}/",
|
||||||
|
{"suppliers": [self.supplier.id, supplier2.id]},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
product.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
set(product.suppliers.all()), {self.supplier, supplier2}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unlink_product_supplier_via_product_api(self):
|
||||||
|
product = Product.objects.create(name="Panela", price=5000)
|
||||||
|
product.suppliers.add(self.supplier)
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/don_confiao/api/products/{product.id}/",
|
||||||
|
{"suppliers": []},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
product.refresh_from_db()
|
||||||
|
self.assertEqual(product.suppliers.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSeedGeography(APITestCase, LoginMixin):
|
||||||
|
url = "/don_confiao/api/seed_geography"
|
||||||
|
|
||||||
|
def _payload(self):
|
||||||
|
return {
|
||||||
|
"country": {"name": "Colombia", "code": "CO"},
|
||||||
|
"departments": [{"name": "Cundinamarca", "code": "CO-CUN"}],
|
||||||
|
"municipalities": [
|
||||||
|
{"name": "La Mesa", "department": "Cundinamarca"},
|
||||||
|
{"name": "El Colegio", "department": "Cundinamarca"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_seed_requires_authentication(self):
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, self._payload(), format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_seed_requires_admin(self):
|
||||||
|
user = _create_user("normal", "user")
|
||||||
|
client = _client_for(user)
|
||||||
|
response = client.post(self.url, self._payload(), format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_seed_creates_geography(self):
|
||||||
|
self.login()
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, self._payload(), format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["country"], "Colombia")
|
||||||
|
self.assertEqual(data["total_departments"], 1)
|
||||||
|
self.assertEqual(data["total_municipalities"], 2)
|
||||||
|
|
||||||
|
def test_seed_is_idempotent(self):
|
||||||
|
self.login()
|
||||||
|
self.client.post(self.url, self._payload(), format="json")
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, self._payload(), format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(Country.objects.count(), 1)
|
||||||
|
self.assertEqual(Department.objects.count(), 1)
|
||||||
|
self.assertEqual(Municipality.objects.count(), 2)
|
||||||
|
|
||||||
|
def test_seed_creates_municipality_with_geolocation(self):
|
||||||
|
self.login()
|
||||||
|
payload = self._payload()
|
||||||
|
payload["municipalities"][0] = {
|
||||||
|
"name": "La Mesa",
|
||||||
|
"department": "Cundinamarca",
|
||||||
|
"latitude": 4.63092,
|
||||||
|
"longitude": -74.39152,
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, payload, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
municipality = Municipality.objects.get(name="La Mesa")
|
||||||
|
self.assertEqual(float(municipality.latitude), 4.63092)
|
||||||
|
self.assertEqual(float(municipality.longitude), -74.39152)
|
||||||
|
|
||||||
|
def test_seed_updates_geolocation_on_reseed(self):
|
||||||
|
self.login()
|
||||||
|
self.client.post(self.url, self._payload(), format="json")
|
||||||
|
payload = self._payload()
|
||||||
|
payload["municipalities"][0] = {
|
||||||
|
"name": "La Mesa",
|
||||||
|
"department": "Cundinamarca",
|
||||||
|
"latitude": 4.63092,
|
||||||
|
"longitude": -74.39152,
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, payload, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
municipality = Municipality.objects.get(name="La Mesa")
|
||||||
|
self.assertEqual(float(municipality.latitude), 4.63092)
|
||||||
|
self.assertEqual(float(municipality.longitude), -74.39152)
|
||||||
|
|
||||||
|
def test_seed_requires_country_name(self):
|
||||||
|
self.login()
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, {"departments": []}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_seed_creates_duplicate_municipality_names_in_different_departments(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
self.login()
|
||||||
|
payload = {
|
||||||
|
"country": {"name": "Colombia", "code": "CO"},
|
||||||
|
"departments": [
|
||||||
|
{"name": "Cundinamarca", "code": "CO-CUN"},
|
||||||
|
{"name": "Antioquia", "code": "CO-ANT"},
|
||||||
|
],
|
||||||
|
"municipalities": [
|
||||||
|
{"name": "Bolivar", "department": "Cundinamarca"},
|
||||||
|
{"name": "Bolivar", "department": "Antioquia"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, payload, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["total_departments"], 2)
|
||||||
|
self.assertEqual(data["total_municipalities"], 2)
|
||||||
|
self.assertEqual(
|
||||||
|
Municipality.objects.filter(name="Bolivar").count(), 2
|
||||||
|
)
|
||||||
180
tienda_ilusion/don_confiao/tests/test_provenance_graphs.py
Normal file
180
tienda_ilusion/don_confiao/tests/test_provenance_graphs.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from ..models.customers import Customer
|
||||||
|
from ..models.geography import Country, Department, Municipality
|
||||||
|
from ..models.provenance import Organization, Supplier
|
||||||
|
from ..models.products import Product
|
||||||
|
from ..models.sales import CatalogSale, Sale
|
||||||
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestProvenanceInSummaries(APITestCase, LoginMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.country = Country.objects.create(name="Colombia", code="CO")
|
||||||
|
self.department = Department.objects.create(
|
||||||
|
name="Cundinamarca", country=self.country
|
||||||
|
)
|
||||||
|
self.municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
self.organization = Organization.objects.create(name="Asociación")
|
||||||
|
self.supplier = Supplier.objects.create(
|
||||||
|
name="Proveedor 1",
|
||||||
|
organization=self.organization,
|
||||||
|
municipality=self.municipality,
|
||||||
|
)
|
||||||
|
self.product = Product.objects.create(name="Panela", price=5000)
|
||||||
|
self.product.suppliers.add(self.supplier)
|
||||||
|
self.customer = Customer.objects.create(
|
||||||
|
name="Camilo", external_id="18"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_sale(self):
|
||||||
|
sale = Sale.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
date=datetime(2024, 9, 2, tzinfo=timezone.utc),
|
||||||
|
payment_method="CASH",
|
||||||
|
)
|
||||||
|
sale.saleline_set.create(
|
||||||
|
product=self.product, quantity=2, unit_price=3000
|
||||||
|
)
|
||||||
|
return sale
|
||||||
|
|
||||||
|
def _create_catalog_sale(self):
|
||||||
|
catalog_sale = CatalogSale.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
date=datetime(2024, 9, 2, tzinfo=timezone.utc),
|
||||||
|
customer_name="Camilo",
|
||||||
|
)
|
||||||
|
catalog_sale.catalogsaleline_set.create(
|
||||||
|
product=self.product, quantity=2, unit_price=3000
|
||||||
|
)
|
||||||
|
return catalog_sale
|
||||||
|
|
||||||
|
def test_public_sale_summary_includes_product_provenance(self):
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("product_provenance", data)
|
||||||
|
self.assertEqual(len(data["product_provenance"]), 1)
|
||||||
|
entry = data["product_provenance"][0]
|
||||||
|
self.assertEqual(entry["product"]["name"], "Panela")
|
||||||
|
self.assertEqual(len(entry["suppliers"]), 1)
|
||||||
|
supplier = entry["suppliers"][0]
|
||||||
|
self.assertEqual(supplier["supplier"]["name"], "Proveedor 1")
|
||||||
|
self.assertEqual(supplier["organization"]["name"], "Asociación")
|
||||||
|
self.assertEqual(supplier["municipality"]["name"], "La Mesa")
|
||||||
|
self.assertEqual(supplier["department"]["name"], "Cundinamarca")
|
||||||
|
self.assertEqual(supplier["country"]["name"], "Colombia")
|
||||||
|
|
||||||
|
def test_public_summary_municipality_includes_geolocation(self):
|
||||||
|
self.municipality.latitude = 4.63092
|
||||||
|
self.municipality.longitude = -74.39152
|
||||||
|
self.municipality.save()
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
supplier = response.json()["product_provenance"][0]["suppliers"][0]
|
||||||
|
self.assertEqual(
|
||||||
|
float(supplier["municipality"]["latitude"]), 4.63092
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
float(supplier["municipality"]["longitude"]), -74.39152
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_public_catalog_summary_includes_product_provenance(self):
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{catalog_sale.code}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("product_provenance", data)
|
||||||
|
self.assertEqual(
|
||||||
|
len(data["product_provenance"][0]["suppliers"]), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_authenticated_summaries_include_product_provenance(self):
|
||||||
|
self.login()
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_compra_json/{sale.id}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("product_provenance", response.json())
|
||||||
|
|
||||||
|
catalog_sale = self._create_catalog_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_compra_catalogo_json/{catalog_sale.id}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("product_provenance", response.json())
|
||||||
|
|
||||||
|
def test_summary_existing_fields_are_preserved(self):
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
for field in [
|
||||||
|
"id",
|
||||||
|
"code",
|
||||||
|
"date",
|
||||||
|
"customer",
|
||||||
|
"payment_method",
|
||||||
|
"lines",
|
||||||
|
"link",
|
||||||
|
"type",
|
||||||
|
]:
|
||||||
|
self.assertIn(field, data)
|
||||||
|
|
||||||
|
def test_product_without_suppliers_returns_empty_list(self):
|
||||||
|
product2 = Product.objects.create(name="Café", price=6000)
|
||||||
|
sale = self._create_sale()
|
||||||
|
sale.saleline_set.create(
|
||||||
|
product=product2, quantity=1, unit_price=6000
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
entry = next(
|
||||||
|
e
|
||||||
|
for e in data["product_provenance"]
|
||||||
|
if e["product"]["name"] == "Café"
|
||||||
|
)
|
||||||
|
self.assertEqual(entry["suppliers"], [])
|
||||||
|
|
||||||
|
def test_supplier_without_municipality_returns_null_territory(self):
|
||||||
|
supplier2 = Supplier.objects.create(
|
||||||
|
name="Proveedor sin ubicación"
|
||||||
|
)
|
||||||
|
product2 = Product.objects.create(name="Café", price=6000)
|
||||||
|
product2.suppliers.add(supplier2)
|
||||||
|
sale = self._create_sale()
|
||||||
|
sale.saleline_set.create(
|
||||||
|
product=product2, quantity=1, unit_price=6000
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
entry = next(
|
||||||
|
e
|
||||||
|
for e in data["product_provenance"]
|
||||||
|
if e["product"]["name"] == "Café"
|
||||||
|
)
|
||||||
|
supplier = entry["suppliers"][0]
|
||||||
|
self.assertIsNone(supplier["municipality"])
|
||||||
|
self.assertIsNone(supplier["department"])
|
||||||
|
self.assertIsNone(supplier["country"])
|
||||||
126
tienda_ilusion/don_confiao/tests/test_provenance_models.py
Normal file
126
tienda_ilusion/don_confiao/tests/test_provenance_models.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
from django.db import IntegrityError, transaction
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from ..models.geography import Country, Department, Municipality
|
||||||
|
from ..models.provenance import Organization, Supplier
|
||||||
|
from ..models.products import Product
|
||||||
|
|
||||||
|
|
||||||
|
class TestOrganizationModel(TestCase):
|
||||||
|
def test_create_organization(self):
|
||||||
|
organization = Organization.objects.create(
|
||||||
|
name="Asociación La Mesa",
|
||||||
|
description="Cooperativa de campesinos",
|
||||||
|
)
|
||||||
|
self.assertEqual(organization.name, "Asociación La Mesa")
|
||||||
|
self.assertEqual(
|
||||||
|
organization.description, "Cooperativa de campesinos"
|
||||||
|
)
|
||||||
|
self.assertEqual(str(organization), "Asociación La Mesa")
|
||||||
|
|
||||||
|
def test_update_organization(self):
|
||||||
|
organization = Organization.objects.create(name="Asociación")
|
||||||
|
organization.description = "Nueva descripción"
|
||||||
|
organization.save()
|
||||||
|
organization.refresh_from_db()
|
||||||
|
self.assertEqual(organization.description, "Nueva descripción")
|
||||||
|
|
||||||
|
def test_delete_organization(self):
|
||||||
|
organization = Organization.objects.create(name="Asociación")
|
||||||
|
organization.delete()
|
||||||
|
self.assertFalse(
|
||||||
|
Organization.objects.filter(pk=organization.pk).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_organization_name_is_unique(self):
|
||||||
|
Organization.objects.create(name="Asociación")
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Organization.objects.create(name="Asociación")
|
||||||
|
|
||||||
|
|
||||||
|
class TestSupplierModel(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.country = Country.objects.create(name="Colombia", code="CO")
|
||||||
|
self.department = Department.objects.create(
|
||||||
|
name="Cundinamarca", country=self.country
|
||||||
|
)
|
||||||
|
self.municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
self.organization = Organization.objects.create(name="Asociación")
|
||||||
|
self.product = Product.objects.create(name="Panela", price=5000)
|
||||||
|
|
||||||
|
def test_create_supplier(self):
|
||||||
|
supplier = Supplier.objects.create(
|
||||||
|
name="Proveedor 1", municipality=self.municipality
|
||||||
|
)
|
||||||
|
self.assertEqual(supplier.municipality, self.municipality)
|
||||||
|
self.assertEqual(
|
||||||
|
supplier.municipality.department.country.name, "Colombia"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_supplier_organization_can_be_linked_and_unlinked(self):
|
||||||
|
supplier = Supplier.objects.create(name="Proveedor 1")
|
||||||
|
supplier.organization = self.organization
|
||||||
|
supplier.save()
|
||||||
|
supplier.refresh_from_db()
|
||||||
|
self.assertEqual(supplier.organization, self.organization)
|
||||||
|
|
||||||
|
supplier.organization = None
|
||||||
|
supplier.save()
|
||||||
|
supplier.refresh_from_db()
|
||||||
|
self.assertIsNone(supplier.organization)
|
||||||
|
|
||||||
|
def test_supplier_municipality_can_be_linked_and_unlinked(self):
|
||||||
|
supplier = Supplier.objects.create(name="Proveedor 1")
|
||||||
|
supplier.municipality = self.municipality
|
||||||
|
supplier.save()
|
||||||
|
supplier.refresh_from_db()
|
||||||
|
self.assertEqual(supplier.municipality, self.municipality)
|
||||||
|
|
||||||
|
supplier.municipality = None
|
||||||
|
supplier.save()
|
||||||
|
supplier.refresh_from_db()
|
||||||
|
self.assertIsNone(supplier.municipality)
|
||||||
|
|
||||||
|
def test_supplier_without_organization_or_municipality(self):
|
||||||
|
supplier = Supplier.objects.create(name="Proveedor 1")
|
||||||
|
self.assertIsNone(supplier.organization)
|
||||||
|
self.assertIsNone(supplier.municipality)
|
||||||
|
|
||||||
|
def test_supplier_name_is_unique(self):
|
||||||
|
Supplier.objects.create(name="Proveedor 1")
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
with transaction.atomic():
|
||||||
|
Supplier.objects.create(name="Proveedor 1")
|
||||||
|
|
||||||
|
def test_delete_organization_unlinks_supplier(self):
|
||||||
|
supplier = Supplier.objects.create(
|
||||||
|
name="Proveedor 1", organization=self.organization
|
||||||
|
)
|
||||||
|
self.organization.delete()
|
||||||
|
supplier.refresh_from_db()
|
||||||
|
self.assertIsNone(supplier.organization)
|
||||||
|
|
||||||
|
def test_delete_municipality_unlinks_supplier(self):
|
||||||
|
supplier = Supplier.objects.create(
|
||||||
|
name="Proveedor 1", municipality=self.municipality
|
||||||
|
)
|
||||||
|
self.municipality.delete()
|
||||||
|
supplier.refresh_from_db()
|
||||||
|
self.assertIsNone(supplier.municipality)
|
||||||
|
|
||||||
|
def test_product_can_be_linked_to_multiple_suppliers(self):
|
||||||
|
supplier1 = Supplier.objects.create(name="Proveedor 1")
|
||||||
|
supplier2 = Supplier.objects.create(name="Proveedor 2")
|
||||||
|
|
||||||
|
self.product.suppliers.add(supplier1, supplier2)
|
||||||
|
self.assertEqual(self.product.suppliers.count(), 2)
|
||||||
|
|
||||||
|
self.product.suppliers.remove(supplier1)
|
||||||
|
self.assertEqual(list(self.product.suppliers.all()), [supplier2])
|
||||||
|
|
||||||
|
self.assertEqual(set(supplier2.products.all()), {self.product})
|
||||||
@@ -29,6 +29,13 @@ from .api import (
|
|||||||
AdminCodeValidateView,
|
AdminCodeValidateView,
|
||||||
# Store Settings
|
# Store Settings
|
||||||
StoreSettingsView,
|
StoreSettingsView,
|
||||||
|
# Provenance
|
||||||
|
OrganizationView,
|
||||||
|
SupplierView,
|
||||||
|
CountryView,
|
||||||
|
DepartmentView,
|
||||||
|
MunicipalityView,
|
||||||
|
SeedGeographyView,
|
||||||
)
|
)
|
||||||
|
|
||||||
app_name = "don_confiao"
|
app_name = "don_confiao"
|
||||||
@@ -48,6 +55,11 @@ router.register(
|
|||||||
ReconciliateJarModelView,
|
ReconciliateJarModelView,
|
||||||
basename="reconciliate_jar",
|
basename="reconciliate_jar",
|
||||||
)
|
)
|
||||||
|
router.register(r"organizations", OrganizationView, basename="organization")
|
||||||
|
router.register(r"suppliers", SupplierView, basename="supplier")
|
||||||
|
router.register(r"countries", CountryView, basename="country")
|
||||||
|
router.register(r"departments", DepartmentView, basename="department")
|
||||||
|
router.register(r"municipalities", MunicipalityView, basename="municipality")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("productos", views.products, name="products"),
|
path("productos", views.products, name="products"),
|
||||||
@@ -109,4 +121,9 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("api/sales/for_tryton", SalesForTrytonView.as_view()),
|
path("api/sales/for_tryton", SalesForTrytonView.as_view()),
|
||||||
path("api/store_settings", StoreSettingsView.as_view()),
|
path("api/store_settings", StoreSettingsView.as_view()),
|
||||||
|
path(
|
||||||
|
"api/seed_geography",
|
||||||
|
SeedGeographyView.as_view(),
|
||||||
|
name="seed_geography",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user