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 _type field: 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 store
  • zone_type_idx - Fast filtering by configuration type
  • zone_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"]
  • 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

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)
  • 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:

  • ZoneMerchandisingRequiredProduct junction 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)
  • count_frequency (CharField, choices, nullable) - Count schedule
    • Choices: Daily, Weekly, Bi-Weekly, Monthly
    • Determines how often physical inventory counts required
  • last_count_date (DateField, nullable) - Last count timestamp
    • Updated when inventory count completed
    • Used to calculate next count due date

Related:

  • par_levels reverse 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())
  • product (ForeignKey → Product, CASCADE) - Product reference
    • Related name: par_level_items (product.par_level_items.all())
  • 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 lookup
  • par_level_zone_idx - Fast zone filtering
  • par_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:
    1. Get store from zone relationship
    2. Get or create InventoryLevel for (product, variant_sku, store)
    3. 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 search
  • planogram_date_idx - Fast date filtering
  • planogram_campaign_idx - Fast campaign lookup

Related Models

  • planogram_products - Products in this planogram (reverse relation)
  • target_stores - Which stores this planogram applies to
  • target_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())
  • product (ForeignKey → Product, CASCADE) - Product reference
    • Related name: planogram_products (product.planogram_products.all())
  • 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 lookup
  • planogram_prod_plan_idx - Fast planogram filtering
  • planogram_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

  1. Physical Organization: Inventory matches store layout
  2. Targeted Counts: Count high-turnover zones more frequently
  3. Compliance Integration: Inventory checks tied to zone compliance
  4. Merchandising Alignment: Par levels aligned with planograms
  5. 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

Was this page helpful?