Zones Models - Zone-Driven Inventory Architecture
File: api/nextango/apps/zones/models.py
Purpose: Physical store zones with inventory, compliance, and merchandising management
Database Tables: zone, zone_compliance, zone_merchandising, zone_inventory, par_level_item, planogram, planogram_product, junction tables
Overview
The Zones domain implements zone-driven inventory architecture where physical store spaces (zones) define product assignments, compliance requirements, and merchandising standards. Zones act as the product assignment layer between the catalog and stores.
Key Concepts:
- Zone: Physical area in a store (e.g., "Dairy Cooler", "Front Register Display")
- Par Level: Ideal quantity of each product variant that should exist in a zone
- Planogram: Visual merchandising guide showing product placement
- Compliance: Checklist requirements for zone maintenance
- Zone-Driven Inventory: Inventory tracking organized by physical zones
Architecture:
Store
│
├─ Zone (Dairy Cooler)
│ │
│ ├─ ZoneInventory (count frequency, par levels)
│ │ │
│ │ └─ ParLevelItem (Product A, SKU-123, Qty: 24)
│ │ └─ Creates/Ensures InventoryLevel exists
│ │
│ ├─ ZoneCompliance (daily checklist, photo required)
│ │
│ └─ ZoneMerchandising (display standards, required categories)
│
└─ Planogram (Holiday Display 2025)
│
├─ PlanogramProduct (Product A, SKU-123, Qty: 12, "Top shelf left")
│
└─ Target Zones (apply to specific zones)
Core Models
Zone
Purpose: Physical area within a store for product organization
Source Code:
# From models.py:9-64
class Zone(SanityIntegratedModel, AuditableMixin):
"""
Zone model that exactly mirrors Sanity's zone.js schema.
Zones act as the product assignment layer between catalog and stores.
"""
zone_name = models.CharField(
max_length=255,
help_text='Zone Name'
)
store_location = models.ForeignKey(
'stores.Store',
on_delete=models.CASCADE,
related_name='zones',
help_text='Store Location reference'
)
is_shown = models.BooleanField(
default=True,
help_text='Show in Compliance Dashboard'
)
display_order = models.IntegerField(
null=True,
blank=True,
help_text='Order zones appear in compliance checklists'
)
# Zone type configuration - stores the zone configuration data from Sanity
zone_type = models.JSONField(
default=list,
help_text='Zone type configuration from Sanity (array of zone type objects with compliance, merchandising, inventory settings)'
)
def get_par_levels(self):
"""Get all par level items for this zone if it has inventory configuration."""
if isinstance(self.zone_type, list):
has_inventory = any(
item.get('_type') == 'zoneInventory'
for item in self.zone_type
if isinstance(item, dict)
)
if has_inventory:
return self.zone_inventory.par_levels.all() if hasattr(self, 'zone_inventory') else []
return []
Fields
Identification:
zone_name(CharField, max_length=255) - Zone name- Examples: "Dairy Cooler", "Front Register Display", "Bakery Section"
store_location(ForeignKey → Store, CASCADE) - Store this zone belongs to- CASCADE: Delete zones when store deleted
- Related name:
zones(store.zones.all())
Display:
is_shown(BooleanField, default=True) - Visibility in compliance dashboard- True: Show in checklists and reports
- False: Hidden zone (inactive/deprecated)
display_order(IntegerField, nullable) - Sort order in lists- Lower numbers appear first
- Null values sorted last
Configuration:
zone_type(JSONField, default=list) - Polymorphic zone configuration- Array of zone type objects from Sanity
- Each object has
_typefield:zoneCompliance,zoneMerchandising,zoneInventory - Determines which related models are populated
Constraints:
unique_together = [['store_location', 'zone_name']]- Zone names unique per store
Indexes:
zone_store_idx- Fast lookup by storezone_type_idx- Fast filtering by configuration typezone_shown_idx- Fast filtering by visibility
Methods
get_par_levels():
def get_par_levels(self):
"""Get all par level items for this zone if it has inventory configuration."""
if isinstance(self.zone_type, list):
has_inventory = any(
item.get('_type') == 'zoneInventory'
for item in self.zone_type
if isinstance(item, dict)
)
if has_inventory:
return self.zone_inventory.par_levels.all() if hasattr(self, 'zone_inventory') else []
return []
- Returns: QuerySet of ParLevelItem objects
- Only returns items if zone has inventory configuration
- Used to fetch all product par levels for a zone
Zone Configuration Models
ZoneCompliance
Purpose: Compliance checklist requirements for zone maintenance
Source Code:
# From models.py:67-104
class ZoneCompliance(SanityIntegratedModel, AuditableMixin):
"""
ZoneCompliance model that mirrors Sanity's zoneCompliance.js schema.
Defines compliance checklist requirements for zones.
"""
zone = models.OneToOneField(
Zone,
on_delete=models.CASCADE,
related_name='zone_compliance',
help_text='Associated zone'
)
checklist_items = models.JSONField(
default=list,
help_text='Array of checklist item strings'
)
requires_photo = models.BooleanField(
default=True,
help_text='Photo Required'
)
frequency = models.CharField(
max_length=20,
choices=[
('Daily', 'Daily'),
('Weekly', 'Weekly'),
('Monthly', 'Monthly'),
],
null=True,
blank=True,
help_text='Check Frequency'
)
Fields
zone(OneToOneField → Zone, CASCADE) - Associated zone- One compliance config per zone
- Related name:
zone_compliance(zone.zone_compliance)
checklist_items(JSONField, default=list) - Checklist tasks- Array of strings:
["Clean shelves", "Check temperature", "Rotate stock"]
- Array of strings:
requires_photo(BooleanField, default=True) - Photo requirement- True: Must upload photo when completing checklist
- False: Photo optional
frequency(CharField, choices, nullable) - Check frequency- Choices:
Daily,Weekly,Monthly - Determines how often compliance checks required
- Choices:
Example Data:
{
"checklist_items": [
"Temperature check (35-38°F)",
"No expired products",
"Shelves clean and organized",
"Products properly rotated (FIFO)"
],
"requires_photo": true,
"frequency": "Daily"
}
ZoneMerchandising
Purpose: Merchandising standards and display requirements
Source Code:
# From models.py:107-129
class ZoneMerchandising(SanityIntegratedModel, AuditableMixin):
"""
ZoneMerchandising model that mirrors Sanity's zoneMerchandising.js schema.
Defines merchandising standards for zones.
"""
zone = models.OneToOneField(
Zone,
on_delete=models.CASCADE,
related_name='zone_merchandising',
help_text='Associated zone'
)
display_standards = models.JSONField(
default=list,
help_text='Array of display standard strings'
)
Fields
zone(OneToOneField → Zone, CASCADE) - Associated zone- Related name:
zone_merchandising(zone.zone_merchandising)
- Related name:
display_standards(JSONField, default=list) - Display requirements- Array of strings describing merchandising standards
Example Data:
{
"display_standards": [
"Products faced forward",
"Price tags visible and accurate",
"No gaps or holes in display",
"Promotional signage at eye level"
]
}
Related:
ZoneMerchandisingRequiredProductjunction table links required product types
ZoneInventory
Purpose: Inventory management configuration for zones
Source Code:
# From models.py:132-167
class ZoneInventory(SanityIntegratedModel, AuditableMixin):
"""
ZoneInventory model that mirrors Sanity's zoneInventory.js schema.
Defines inventory management configuration for zones.
"""
zone = models.OneToOneField(
Zone,
on_delete=models.CASCADE,
related_name='zone_inventory',
help_text='Associated zone'
)
count_frequency = models.CharField(
max_length=20,
choices=[
('Daily', 'Daily'),
('Weekly', 'Weekly'),
('Bi-Weekly', 'Bi-Weekly'),
('Monthly', 'Monthly'),
],
null=True,
blank=True,
help_text='How often should inventory in this zone be counted?'
)
last_count_date = models.DateField(
null=True,
blank=True,
help_text='Last Count Date'
)
Fields
zone(OneToOneField → Zone, CASCADE) - Associated zone- Related name:
zone_inventory(zone.zone_inventory)
- Related name:
count_frequency(CharField, choices, nullable) - Count schedule- Choices:
Daily,Weekly,Bi-Weekly,Monthly - Determines how often physical inventory counts required
- Choices:
last_count_date(DateField, nullable) - Last count timestamp- Updated when inventory count completed
- Used to calculate next count due date
Related:
par_levelsreverse relation to ParLevelItem (zone_inventory.par_levels.all())
Par Level System
ParLevelItem
Purpose: Ideal product quantities for specific zones (core of zone-driven inventory)
Source Code:
# From models.py:170-267
class ParLevelItem(SanityIntegratedModel, AuditableMixin):
"""
ParLevelItem model that mirrors Sanity's parLevelItem.js schema.
Defines which products belong in physical spaces with ideal quantities.
This is the core of the zone-driven inventory architecture.
"""
zone_inventory = models.ForeignKey(
ZoneInventory,
on_delete=models.CASCADE,
related_name='par_levels',
help_text='Associated zone inventory configuration'
)
product = models.ForeignKey(
'products.Product',
on_delete=models.CASCADE,
related_name='par_level_items',
help_text='Product reference'
)
variant_sku = models.CharField(
max_length=100,
db_index=True,
help_text='SKU of the specific product variant'
)
quantity = models.IntegerField(
help_text='Ideal Quantity for this variant in this zone'
)
def get_product_variant(self):
"""Get the ProductVariant record that matches this par level item's SKU."""
try:
from django.apps import apps
ProductVariant = apps.get_model('products', 'ProductVariant')
return ProductVariant.objects.filter(
product=self.product,
sku=self.variant_sku
).first()
except Exception as e:
logger.error(f"Failed to get ProductVariant for SKU {self.variant_sku}: {e}")
return None
def ensure_inventory_level_exists(self):
"""
Ensure an InventoryLevel record exists for this par level item.
This creates the inventory tracking infrastructure based on zone assignments.
"""
try:
from django.apps import apps
InventoryLevel = apps.get_model('products', 'InventoryLevel')
# Get the store from the zone
store = self.zone_inventory.zone.store_location
# Check if InventoryLevel already exists
inventory_level, created = InventoryLevel.objects.get_or_create(
product=self.product,
variant_sku=self.variant_sku,
store=store,
defaults={
'quantity_on_hand': 0,
'quantity_reserved': 0,
'cost_per_unit': None
}
)
if created:
logger.info(f"Created InventoryLevel for {self.variant_sku} at {store.name} based on par level")
return inventory_level
except Exception as e:
logger.error(f"Failed to ensure InventoryLevel exists for par level {self.pk}: {e}")
return None
def clean(self):
"""Validate that the variant_sku exists for the selected product."""
super().clean()
if self.product and self.variant_sku:
variant = self.get_product_variant()
if not variant:
raise ValidationError(
f"No ProductVariant with SKU '{self.variant_sku}' found for product '{self.product.name}'"
)
Fields
zone_inventory(ForeignKey → ZoneInventory, CASCADE) - Zone configuration- Related name:
par_levels(zone_inventory.par_levels.all())
- Related name:
product(ForeignKey → Product, CASCADE) - Product reference- Related name:
par_level_items(product.par_level_items.all())
- Related name:
variant_sku(CharField, max_length=100, indexed) - Specific variant SKU- Must match an existing ProductVariant.sku
- Indexed for fast SKU lookups
quantity(IntegerField) - Ideal/par quantity- How many units should ideally be in this zone
- Used for reordering triggers and compliance
Constraints:
unique_together = [['zone_inventory', 'product', 'variant_sku']]- Each product variant can have one par level per zone
Indexes:
par_level_sku_idx- Fast SKU lookuppar_level_zone_idx- Fast zone filteringpar_level_product_sku_idx- Fast product+SKU lookup
Methods
get_product_variant():
- Returns: ProductVariant instance matching this SKU
- Used for validation and SKU resolution
ensure_inventory_level_exists():
- Critical Method: Creates inventory tracking infrastructure
- Returns: InventoryLevel instance
- Pattern: Par level assignment automatically creates store inventory tracking
- Flow:
- Get store from zone relationship
- Get or create InventoryLevel for (product, variant_sku, store)
- Initialize with zero quantities if creating new
- Why Important: Zone assignments drive inventory tracking creation
clean():
- Validation: Ensures variant_sku exists for the product
- Raises: ValidationError if SKU doesn't match any ProductVariant
Par Level Usage Pattern
# Admin creates par level in Sanity or Django
par_level = ParLevelItem.objects.create(
zone_inventory=dairy_zone.zone_inventory,
product=milk_product,
variant_sku='MILK-GAL-WHOLE',
quantity=24 # Should have 24 gallons in dairy cooler
)
# Par level automatically ensures inventory tracking exists
inventory_level = par_level.ensure_inventory_level_exists()
# Now inventory system knows:
# - This product/variant should be tracked at this store
# - Ideal quantity is 24 units in the dairy cooler zone
# - Can trigger reorders when actual < par level
Planogram Models
Planogram
Purpose: Visual merchandising guide for product arrangement
Source Code:
# From models.py:270-319
class Planogram(SanityIntegratedModel, AuditableMixin):
"""
Planogram model that mirrors Sanity's planogram.js schema.
Provides merchandising guidance for zone product arrangements.
"""
planogram_name = models.CharField(
max_length=255,
help_text='Planogram Name'
)
version = models.CharField(
max_length=50,
null=True,
blank=True,
help_text='Version (e.g., 1.0, 2.1)'
)
effective_date = models.DateField(
null=True,
blank=True,
help_text='Effective Date'
)
campaign = models.ForeignKey(
'promotions.PromotionalCampaign',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='planograms',
help_text='Associated Campaign'
)
instructions = models.TextField(
blank=True,
help_text='Instructions (from bodyPortableText)'
)
diagram_url = models.URLField(
null=True,
blank=True,
help_text='URL to planogram diagram image'
)
Fields
planogram_name(CharField, max_length=255) - Planogram name- Examples: "Holiday 2025 Front Display", "Summer Beverage Cooler"
version(CharField, max_length=50, nullable) - Version number- Examples: "1.0", "2.1", "v3.2"
effective_date(DateField, nullable) - When planogram becomes active- Used to schedule future merchandising changes
campaign(ForeignKey → PromotionalCampaign, SET_NULL, nullable) - Related campaign- Links planogram to promotional campaigns
- SET_NULL: Keep planogram if campaign deleted
instructions(TextField) - Setup instructions- Rich text from Sanity's bodyPortableText
diagram_url(URLField, nullable) - Visual diagram- Link to image/PDF of planogram layout
Indexes:
planogram_name_idx- Fast name searchplanogram_date_idx- Fast date filteringplanogram_campaign_idx- Fast campaign lookup
Related Models
planogram_products- Products in this planogram (reverse relation)target_stores- Which stores this planogram applies totarget_zones- Which zones this planogram applies to
PlanogramProduct
Purpose: Specific product placements within planograms
Source Code:
# From models.py:322-387
class PlanogramProduct(SanityIntegratedModel, AuditableMixin):
"""
PlanogramProduct model that mirrors Sanity's planogramProduct.js schema.
Defines specific product placements within planograms.
"""
planogram = models.ForeignKey(
Planogram,
on_delete=models.CASCADE,
related_name='planogram_products',
help_text='Associated planogram'
)
product = models.ForeignKey(
'products.Product',
on_delete=models.CASCADE,
related_name='planogram_products',
help_text='Product reference'
)
variant_sku = models.CharField(
max_length=100,
db_index=True,
help_text='SKU of the specific product variant'
)
quantity = models.IntegerField(
help_text='Initial Quantity for Product Variant'
)
position_notes = models.CharField(
max_length=255,
blank=True,
help_text='Positioning Notes (e.g., "Top shelf, left corner")'
)
def get_product_variant(self):
"""Get the ProductVariant record that matches this planogram product's SKU."""
# Similar to ParLevelItem.get_product_variant()
...
def clean(self):
"""Validate that the variant_sku exists for the selected product."""
# Similar to ParLevelItem.clean()
...
Fields
planogram(ForeignKey → Planogram, CASCADE) - Parent planogram- Related name:
planogram_products(planogram.planogram_products.all())
- Related name:
product(ForeignKey → Product, CASCADE) - Product reference- Related name:
planogram_products(product.planogram_products.all())
- Related name:
variant_sku(CharField, max_length=100, indexed) - Specific variant- Must match existing ProductVariant.sku
quantity(IntegerField) - Initial quantity for display- How many units to place initially
position_notes(CharField, max_length=255) - Placement instructions- Examples: "Top shelf, left corner", "Eye level, center", "End cap, facing front"
Constraints:
unique_together = [['planogram', 'product', 'variant_sku']]- Each product variant appears once per planogram
Indexes:
planogram_prod_sku_idx- Fast SKU lookupplanogram_prod_plan_idx- Fast planogram filteringplanogram_prod_prod_sku_idx- Fast product+SKU lookup
Junction Tables
PlanogramTargetStore
Purpose: Which stores a planogram applies to
Source Code:
# From models.py:391-407
class PlanogramTargetStore(models.Model):
"""Junction table for planogram target stores."""
planogram = models.ForeignKey(
Planogram,
on_delete=models.CASCADE,
related_name='target_stores'
)
store = models.ForeignKey(
'stores.Store',
on_delete=models.CASCADE,
related_name='targeted_planograms'
)
unique_together = [['planogram', 'store']]
- Allows planograms to target specific stores
- Many-to-many relationship
PlanogramTargetZone
Purpose: Which zones a planogram applies to
Source Code:
# From models.py:409-424
class PlanogramTargetZone(models.Model):
"""Junction table for planogram target zones."""
planogram = models.ForeignKey(
Planogram,
on_delete=models.CASCADE,
related_name='target_zones'
)
zone = models.ForeignKey(
Zone,
on_delete=models.CASCADE,
related_name='targeted_planograms'
)
unique_together = [['planogram', 'zone']]
- Allows planograms to target specific zones
- Many-to-many relationship
ZoneMerchandisingRequiredProduct
Purpose: Required product types for zone merchandising
Source Code:
# From models.py:427-442
class ZoneMerchandisingRequiredProduct(models.Model):
"""Junction table for zone merchandising required product categories."""
zone_merchandising = models.ForeignKey(
ZoneMerchandising,
on_delete=models.CASCADE,
related_name='required_products'
)
product_type = models.ForeignKey(
'products.ProductType',
on_delete=models.CASCADE,
related_name='required_in_zones'
)
unique_together = [['zone_merchandising', 'product_type']]
- Defines which product categories must be present in a zone
- Examples: Dairy zone must have milk, cheese, yogurt types
Zone-Driven Inventory Architecture
Concept
Traditional Inventory: Track products per store Zone-Driven Inventory: Track products per zone within stores
Benefits
- Physical Organization: Inventory matches store layout
- Targeted Counts: Count high-turnover zones more frequently
- Compliance Integration: Inventory checks tied to zone compliance
- Merchandising Alignment: Par levels aligned with planograms
- Reordering Logic: Trigger reorders when zone falls below par
Flow
1. Create Zone (Dairy Cooler at Store A)
│
2. Create ZoneInventory (count daily)
│
3. Create ParLevelItem (Milk SKU-123, Qty: 24)
│
├─ Validates SKU exists
├─ ensure_inventory_level_exists() called
│ │
│ └─ Creates InventoryLevel (Store A, Milk SKU-123)
│
4. Inventory System Now Knows:
├─ Track Milk SKU-123 at Store A
├─ Ideal quantity is 24 in Dairy Cooler
├─ Count daily (from zone_inventory.count_frequency)
└─ Trigger reorder when actual < 24
Related Documentation
- services.md - Zone business logic
- serializers.md - Zone data validation
- views.md - Zone API endpoints
- signals.md - Zone sync patterns
- Products Models - Product, ProductVariant, InventoryLevel
- Stores Models - Store model