feat(#49): add product provenance (suppliers, organizations, geography)

- Country, Department, Municipality models (municipality unique per department)
- Organization and Supplier models, Supplier links to organization and municipality
- Product.suppliers M2M to link products to one or more suppliers
- Authenticated CRUD APIs for organizations, suppliers, countries, departments and municipalities (write only for administrators)
- product_provenance domain data embedded in sale/catalog summaries (public and authenticated)
- seed_geography endpoint (admin, idempotent) and scripts/seed_geography.py using pycountry + DANE municipalities CSV
- TDD tests for models, API permissions/CRUD and summary provenance data
This commit is contained in:
2026-08-15 16:32:20 -05:00
parent b088794716
commit 29d154e140
18 changed files with 1321 additions and 5 deletions

View File

@@ -20,6 +20,14 @@ from .payments import (
)
from .admin import AdminCodeValidateView
from .store_settings import StoreSettingsView
from .provenance import (
OrganizationView,
SupplierView,
CountryView,
DepartmentView,
MunicipalityView,
SeedGeographyView,
)
__all__ = [
# Catalogue Images
@@ -49,4 +57,11 @@ __all__ = [
"AdminCodeValidateView",
# Store Settings
"StoreSettingsView",
# Provenance
"OrganizationView",
"SupplierView",
"CountryView",
"DepartmentView",
"MunicipalityView",
"SeedGeographyView",
]

View File

@@ -0,0 +1,112 @@
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
_, created = Municipality.objects.get_or_create(
name=municipality_data["name"],
department=department,
defaults={"country": country},
)
if created:
municipalities_created += 1
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,
)