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

@@ -17,6 +17,8 @@ from .payments import (
PaymentMethodSerializer,
)
from .store_settings import StoreSettingsSerializer
from .geography import CountrySerializer, DepartmentSerializer, MunicipalitySerializer
from .provenance import OrganizationSerializer, SupplierSerializer
__all__ = [
# Catalogue Images
@@ -42,4 +44,11 @@ __all__ = [
"PaymentMethodSerializer",
# Store Settings
"StoreSettingsSerializer",
# Geography
"CountrySerializer",
"DepartmentSerializer",
"MunicipalitySerializer",
# Provenance
"OrganizationSerializer",
"SupplierSerializer",
]

View File

@@ -0,0 +1,21 @@
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 DepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = ["id", "name", "country"]
class MunicipalitySerializer(serializers.ModelSerializer):
class Meta:
model = Municipality
fields = ["id", "name", "department", "country"]

View File

@@ -1,10 +1,14 @@
from rest_framework import serializers
from ..models.products import Product, ProductCategory
from ..models.provenance import Supplier
class ProductSerializer(serializers.ModelSerializer):
catalogue_images = serializers.SerializerMethodField()
suppliers = serializers.PrimaryKeyRelatedField(
many=True, queryset=Supplier.objects.all(), required=False
)
class Meta:
model = Product
@@ -15,6 +19,7 @@ class ProductSerializer(serializers.ModelSerializer):
"price",
"measuring_unit",
"categories",
"suppliers",
"external_id",
"catalogue_images",
]

View File

@@ -0,0 +1,93 @@
from rest_framework import serializers
from ..models.geography import Country, Department, Municipality
from ..models.provenance import Organization, Supplier
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"]
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",
]

View File

@@ -1,6 +1,7 @@
from rest_framework import serializers
from django.urls import reverse
from ..models.products import Product
from ..models.sales import (
Sale,
SaleLine,
@@ -10,6 +11,7 @@ from ..models.sales import (
)
from .products import ListProductSerializer
from .customers import ListCustomerSerializer
from ..services.provenance import build_product_provenance
class PublicSummaryLinkMixin:
@@ -23,6 +25,16 @@ class PublicSummaryLinkMixin:
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 Meta:
model = SaleLine
@@ -101,14 +113,26 @@ class SummarySaleLineSerializer(serializers.ModelSerializer):
fields = ["product", "quantity", "unit_price", "description"]
class SaleSummarySerializer(PublicSummaryLinkMixin, serializers.ModelSerializer):
class SaleSummarySerializer(
PublicSummaryLinkMixin, ProvenanceSummaryMixin, serializers.ModelSerializer
):
customer = ListCustomerSerializer()
lines = SummarySaleLineSerializer(many=True, source="saleline_set")
link = serializers.SerializerMethodField()
product_provenance = serializers.SerializerMethodField()
class Meta:
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):
@@ -119,18 +143,31 @@ class CatalogSummarySaleLineSerializer(serializers.ModelSerializer):
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(
PublicSummaryLinkMixin, serializers.ModelSerializer
PublicSummaryLinkMixin,
CatalogProvenanceSummaryMixin,
serializers.ModelSerializer,
):
customer = ListCustomerSerializer()
lines = CatalogSummarySaleLineSerializer(
many=True, source="catalogsaleline_set"
)
link = serializers.SerializerMethodField()
product_provenance = serializers.SerializerMethodField()
class Meta:
model = CatalogSale
fields = ["id", "code", "date", "customer", "lines", "link"]
fields = ["id", "code", "date", "customer", "lines", "link", "product_provenance"]
class SaleForRenconciliationSerializer(serializers.Serializer):