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

@@ -1,3 +1,12 @@
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",
]

View File

@@ -0,0 +1,54 @@
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"
)
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

View File

@@ -26,6 +26,9 @@ class Product(models.Model):
max_length=100, null=True, blank=True
)
categories = models.ManyToManyField(ProductCategory)
suppliers = models.ManyToManyField(
"Supplier", blank=True, related_name="products"
)
external_id = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):

View 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