Zones Views - API Endpoints

Location: api/nextango/apps/zones/views.py Last Updated: 2025-12-21

Overview

The zones views module provides comprehensive REST API endpoints for zone management, merchandising, compliance, inventory tracking, and planogram operations. Built with Django REST Framework ViewSets, these endpoints support the zone-driven inventory architecture.

Key Features:

  • Zone management: CRUD operations with nested zone types (compliance, merchandising, inventory)
  • Par level operations: Product assignment management with SKU validation
  • Planogram management: Visual merchandising layouts with product assignments
  • Inventory sync: Automated infrastructure creation from par levels
  • Store-based filtering: Zone organization by store location
  • Service layer integration: ZoneAssignmentService for business logic
  • Redis caching: List/retrieve operations with signal-based invalidation

ViewSets (7 total):

  1. ZoneViewSet - Main zone CRUD with custom actions
  2. ZoneComplianceViewSet - Compliance configuration management
  3. ZoneMerchandisingViewSet - Merchandising standards management
  4. ZoneInventoryViewSet - Inventory configuration with validation
  5. ParLevelItemViewSet - Par level CRUD with inventory creation
  6. PlanogramViewSet - Planogram CRUD with target assignments
  7. PlanogramProductViewSet - Planogram product CRUD

URL Patterns:

/api/zones/                       # ZoneViewSet
/api/zones/compliance/            # ZoneComplianceViewSet
/api/zones/merchandising/         # ZoneMerchandisingViewSet
/api/zones/inventory/             # ZoneInventoryViewSet
/api/zones/par-levels/            # ParLevelItemViewSet
/api/planograms/                  # PlanogramViewSet
/api/planograms/products/         # PlanogramProductViewSet

ZoneViewSet

Main ViewSet for zone management with zone-specific actions.

Purpose

Provides comprehensive zone CRUD operations with nested zone type management (compliance, merchandising, inventory) and integration with ZoneAssignmentService for product assignments and inventory sync.

Caching

Cache Configuration:

  • Prefix: zone:
  • Timeout: 1200 seconds (20 minutes)
  • Cached Actions: list, retrieve
  • Invalidation: On Zone save/delete via signals

Base Configuration

Queryset Optimization:

def get_queryset(self):
    return Zone.objects.select_related(
        'store_location'
    ).prefetch_related(
        'zone_compliance',
        'zone_merchandising',
        'zone_inventory'
    )

Filtering:

  • Search: zone_name, store_location__name
  • Filters: is_shown, store_location, zone_name (icontains), store_name (icontains)
  • Ordering: zone_name, display_order, created_at
  • Default ordering: store_location__name, display_order, zone_name

Standard CRUD Operations

List Zones

URL: GET /api/zones/

Query Parameters:

  • zone_name - Filter by zone name (case-insensitive contains)
  • store_name - Filter by store name (case-insensitive contains)
  • is_shown - Filter by visibility (true/false)
  • store_location - Filter by store ID
  • search - Search zone name or store name
  • ordering - Order by zone_name, display_order, or created_at

Response:

[
  {
    "id": "uuid-123",
    "sanity_id": "zone-abc",
    "zone_name": "Coffee Bar",
    "store_location": "uuid-store-456",
    "zone_type": {
      "type": "inventory",
      "data": { /* zone inventory data */ }
    },
    "is_shown": true,
    "display_order": 1,
    "created_at": "2025-11-18T10:00:00Z",
    "updated_at": "2025-11-18T10:00:00Z"
  }
]

Retrieve Zone

URL: GET /api/zones/{id}/

Response: Single zone object with nested zone type data

Create Zone

URL: POST /api/zones/

Request Body:

{
  "sanity_id": "zone-xyz",
  "zone_name": "Bakery Display",
  "store_location": "uuid-store-789",
  "zone_type": {
    "type": "merchandising",
    "data": {
      "display_standards": ["Eye level placement", "FIFO rotation"]
    }
  },
  "is_shown": true,
  "display_order": 3
}

Update Zone

URL: PUT /api/zones/{id}/ or PATCH /api/zones/{id}/

Delete Zone

URL: DELETE /api/zones/{id}/


Custom Actions

Get Zone Products

URL: GET /api/zones/{id}/products/

Purpose: Get all products assigned to this zone through par levels.

Service Integration:

@action(detail=True, methods=['get'])
def products(self, request, pk=None):
    zone = self.get_object()
    zone_service = ZoneAssignmentService()
    products = zone_service.get_products_for_zone(zone)
    return Response(products)

Response:

[
  {
    "product_id": "uuid-product-123",
    "product_name": "Dark Roast Coffee",
    "variant_sku": "DARK-12OZ",
    "variant_name": "12oz Bag",
    "par_quantity": 10,
    "par_level_id": "uuid-par-456"
  },
  {
    "product_id": "uuid-product-124",
    "product_name": "Light Roast Coffee",
    "variant_sku": "LIGHT-12OZ",
    "variant_name": "12oz Bag",
    "par_quantity": 8,
    "par_level_id": "uuid-par-457"
  }
]

Use Case: Display all products that should be stocked in a specific zone


Sync Inventory Infrastructure

URL: POST /api/zones/{id}/sync_inventory/

Purpose: Create InventoryLevel records for all par level items in this zone.

Service Integration:

@action(detail=True, methods=['post'])
def sync_inventory(self, request, pk=None):
    zone = self.get_object()
    zone_service = ZoneAssignmentService()
    stats = zone_service.create_inventory_infrastructure_for_zone(zone)
    return Response({
        'message': f'Inventory sync completed for zone {zone.zone_name}',
        'stats': stats
    })

Response:

{
  "message": "Inventory sync completed for zone Coffee Bar",
  "stats": {
    "created": 8,
    "existing": 2,
    "failed": 0,
    "errors": []
  }
}

Use Case:

  • After adding new par levels to a zone
  • Ensuring inventory tracking infrastructure exists
  • Bulk creation of InventoryLevel records

Get Zones by Store

URL: GET /api/zones/by_store/?store_id={store_id}

Purpose: Get all zones for a specific store location.

Query Parameters:

  • store_id (required) - UUID of the store

Response:

[
  {
    "id": "uuid-zone-1",
    "zone_name": "Coffee Bar",
    "zone_type": { "type": "inventory" },
    "display_order": 1
  },
  {
    "id": "uuid-zone-2",
    "zone_name": "Bakery Display",
    "zone_type": { "type": "merchandising" },
    "display_order": 2
  }
]

Error Response (400):

{
  "error": "store_id parameter is required"
}

Use Case: Display all zones for a specific store in management interface


ZoneComplianceViewSet

ViewSet for zone compliance configuration management.

Purpose

Manages compliance requirements for zones (checklists, photo requirements, frequency).

Configuration

Queryset Optimization:

def get_queryset(self):
    return ZoneCompliance.objects.select_related('zone__store_location')

Filtering:

  • Filters: frequency, requires_photo
  • Search: zone__zone_name, zone__store_location__name

CRUD Operations

Base URL: /api/zones/compliance/

List: GET /api/zones/compliance/

  • Filter by frequency (daily, weekly, monthly)
  • Filter by requires_photo (true/false)
  • Search by zone name or store name

Retrieve: GET /api/zones/compliance/{id}/

Create: POST /api/zones/compliance/

{
  "sanity_id": "compliance-xyz",
  "checklist_items": [
    "Verify zone cleanliness",
    "Check product placement",
    "Ensure proper lighting"
  ],
  "requires_photo": true,
  "frequency": "daily"
}

Update: PUT/PATCH /api/zones/compliance/{id}/

Delete: DELETE /api/zones/compliance/{id}/


ZoneMerchandisingViewSet

ViewSet for zone merchandising standards management.

Purpose

Manages merchandising standards and required product configurations for zones.

Configuration

Queryset Optimization:

def get_queryset(self):
    return ZoneMerchandising.objects.select_related(
        'zone__store_location'
    ).prefetch_related('required_products')

Filtering:

  • Search: zone__zone_name, zone__store_location__name

CRUD Operations

Base URL: /api/zones/merchandising/

List: GET /api/zones/merchandising/

  • Search by zone name or store name
  • Includes required_products_count computed field

Retrieve: GET /api/zones/merchandising/{id}/

Create: POST /api/zones/merchandising/

{
  "sanity_id": "merchandising-xyz",
  "display_standards": [
    "Products at eye level",
    "FIFO rotation",
    "Price tags visible"
  ]
}

Update: PUT/PATCH /api/zones/merchandising/{id}/

Delete: DELETE /api/zones/merchandising/{id}/


ZoneInventoryViewSet

ViewSet for zone inventory configuration with SKU validation.

Purpose

Manages zone inventory configurations with par levels and provides SKU validation functionality.

Configuration

Queryset Optimization:

def get_queryset(self):
    return ZoneInventory.objects.select_related('zone__store_location')

Serializer Selection:

def get_serializer_class(self):
    if self.action == 'retrieve':
        return ZoneInventoryDetailSerializer  # Includes nested par levels
    return ZoneInventorySerializer

Filtering:

  • Filters: count_frequency
  • Search: zone__zone_name, zone__store_location__name

CRUD Operations

Base URL: /api/zones/inventory/

List: GET /api/zones/inventory/

  • Filter by count_frequency (daily, weekly, monthly)
  • Uses basic serializer (no nested par levels)

Retrieve: GET /api/zones/inventory/{id}/

  • Uses detailed serializer with nested par levels
  • Includes par_levels_count computed field

Response (Retrieve):

{
  "id": "uuid-123",
  "sanity_id": "inventory-abc",
  "count_frequency": "weekly",
  "last_count_date": "2025-11-15",
  "par_levels_count": 12,
  "par_levels": [
    {
      "id": "uuid-par-1",
      "product_name": "Dark Roast Coffee",
      "variant_sku": "DARK-12OZ",
      "quantity": 10,
      "variant_exists": true,
      "inventory_level_exists": true
    }
  ],
  "created_at": "2025-11-18T10:00:00Z",
  "updated_at": "2025-11-18T10:00:00Z"
}

Custom Action: Validate SKUs

URL: POST /api/zones/inventory/{id}/validate_skus/

Purpose: Validate that all par level SKUs match actual ProductVariant records.

Service Integration:

@action(detail=True, methods=['post'])
def validate_skus(self, request, pk=None):
    zone_inventory = self.get_object()
    zone_service = ZoneAssignmentService()
    results = zone_service.validate_sku_assignments(zone_inventory)
    return Response(results)

Response (All Valid):

{
  "valid_count": 12,
  "invalid_count": 0,
  "invalid_items": [],
  "total_count": 12
}

Response (Some Invalid):

{
  "valid_count": 10,
  "invalid_count": 2,
  "invalid_items": [
    {
      "par_level_id": "uuid-par-999",
      "product_name": "Dark Roast Coffee",
      "variant_sku": "INVALID-SKU",
      "error": "No ProductVariant found with SKU 'INVALID-SKU' for product 'Dark Roast Coffee'"
    },
    {
      "par_level_id": "uuid-par-998",
      "product_name": "Light Roast Coffee",
      "variant_sku": "WRONG-SKU",
      "error": "No ProductVariant found with SKU 'WRONG-SKU' for product 'Light Roast Coffee'"
    }
  ],
  "total_count": 12
}

Use Case:

  • After importing zone inventory from Sanity
  • Debugging SKU mismatches
  • Validating data integrity

ParLevelItemViewSet

ViewSet for par level item management with inventory infrastructure creation.

Purpose

Manages par level assignments (product quantities per zone) with automatic InventoryLevel creation and SKU validation.

Configuration

Queryset Optimization:

def get_queryset(self):
    return ParLevelItem.objects.select_related(
        'zone_inventory__zone__store_location', 'product'
    )

Serializer Selection:

def get_serializer_class(self):
    if self.action in ['create', 'update', 'partial_update']:
        return ParLevelItemCreateSerializer  # With validation
    return ParLevelItemSerializer  # Read-only

Filtering:

  • Filters: zone_inventory, product, zone_inventory__zone__store_location
  • Search: product__name, variant_sku, zone_inventory__zone__zone_name
  • Ordering: product__name, variant_sku, quantity, created_at
  • Default ordering: zone_inventory__zone__zone_name, product__name, variant_sku

CRUD Operations

Base URL: /api/zones/par-levels/

List: GET /api/zones/par-levels/

  • Filter by zone inventory, product, or store location
  • Search by product name, variant SKU, or zone name
  • Order by product name, SKU, quantity, or creation date

Retrieve: GET /api/zones/par-levels/{id}/

Response:

{
  "id": "uuid-par-123",
  "sanity_id": "par-level-abc",
  "product": "uuid-product-456",
  "product_name": "Dark Roast Coffee",
  "variant_sku": "DARK-12OZ",
  "quantity": 10,
  "variant_exists": true,
  "inventory_level_exists": true,
  "created_at": "2025-11-18T10:00:00Z",
  "updated_at": "2025-11-18T10:00:00Z"
}

Create: POST /api/zones/par-levels/

{
  "zone_inventory": "uuid-zone-inventory-123",
  "product": "uuid-product-456",
  "variant_sku": "DARK-12OZ",
  "quantity": 10,
  "sanity_id": "par-level-xyz"
}

Validation (Create/Update):

  • Validates variant_sku exists for the product
  • Prevents duplicate par levels for same zone/product/variant

Update: PUT/PATCH /api/zones/par-levels/{id}/

Delete: DELETE /api/zones/par-levels/{id}/


Custom Actions

Ensure Inventory Level

URL: POST /api/zones/par-levels/{id}/ensure_inventory_level/

Purpose: Create InventoryLevel record for this par level item if it doesn't exist.

Implementation:

@action(detail=True, methods=['post'])
def ensure_inventory_level(self, request, pk=None):
    par_level = self.get_object()
    inventory_level = par_level.ensure_inventory_level_exists()

    if inventory_level:
        return Response({
            'message': 'InventoryLevel ensured',
            'inventory_level_id': inventory_level.pk,
            'created': hasattr(inventory_level, '_created')
        })
    else:
        return Response(
            {'error': 'Failed to ensure InventoryLevel exists'},
            status=500
        )

Response (Created):

{
  "message": "InventoryLevel ensured",
  "inventory_level_id": "uuid-inv-level-789",
  "created": true
}

Response (Already Exists):

{
  "message": "InventoryLevel ensured",
  "inventory_level_id": "uuid-inv-level-789",
  "created": false
}

Error Response (500):

{
  "error": "Failed to ensure InventoryLevel exists"
}

Use Case:

  • After creating a new par level
  • Ensuring inventory tracking infrastructure exists
  • Debugging missing InventoryLevel records

Get Par Levels by Product

URL: GET /api/zones/par-levels/by_product/?product_id={product_id}&variant_sku={variant_sku}

Purpose: Get all par level assignments for a specific product variant across all zones.

Query Parameters:

  • product_id (required) - UUID of the product
  • variant_sku (required) - SKU of the variant

Response:

[
  {
    "id": "uuid-par-1",
    "zone_inventory": "uuid-zone-inv-1",
    "product_name": "Dark Roast Coffee",
    "variant_sku": "DARK-12OZ",
    "quantity": 10,
    "variant_exists": true,
    "inventory_level_exists": true
  },
  {
    "id": "uuid-par-2",
    "zone_inventory": "uuid-zone-inv-2",
    "product_name": "Dark Roast Coffee",
    "variant_sku": "DARK-12OZ",
    "quantity": 8,
    "variant_exists": true,
    "inventory_level_exists": true
  }
]

Error Response (400):

{
  "error": "product_id and variant_sku parameters are required"
}

Use Case:

  • Display all locations where a product should be stocked
  • Product distribution analysis
  • Par level management interface

PlanogramViewSet

ViewSet for planogram management with target store and zone assignments.

Purpose

Manages planograms (visual merchandising layouts) with product assignments, campaign relationships, and target store/zone configurations.

Configuration

Queryset Optimization:

def get_queryset(self):
    return Planogram.objects.select_related('campaign').prefetch_related(
        Prefetch('planogram_products', queryset=PlanogramProduct.objects.select_related('product')),
        'target_stores', 'target_zones'
    )

Filtering:

  • Filters: campaign, effective_date
  • Search: planogram_name, version, campaign__name
  • Ordering: planogram_name, effective_date, created_at
  • Default ordering: planogram_name, -effective_date (newest first)

CRUD Operations

Base URL: /api/planograms/

List: GET /api/planograms/

  • Filter by campaign or effective date
  • Search by planogram name, version, or campaign name
  • Order by name, effective date, or creation date

Retrieve: GET /api/planograms/{id}/

  • Includes nested planogram products
  • Includes target stores and target zones

Create: POST /api/planograms/

{
  "sanity_id": "planogram-xyz",
  "planogram_name": "Holiday Coffee Display",
  "version": "v2.0",
  "effective_date": "2025-12-01",
  "campaign": "uuid-campaign-123",
  "diagram_asset": {
    "_type": "image",
    "asset": {
      "_ref": "image-abc123"
    }
  }
}

Update: PUT/PATCH /api/planograms/{id}/

Delete: DELETE /api/planograms/{id}/


Custom Action: Get Target Assignments

URL: GET /api/planograms/{id}/target_assignments/

Purpose: Get all target store and zone assignments for this planogram.

Implementation:

@action(detail=True, methods=['get'])
def target_assignments(self, request, pk=None):
    planogram = self.get_object()

    target_stores = list(planogram.target_stores.values('id', 'name', 'store_code'))
    target_zones = list(planogram.target_zones.select_related('store_location').values(
        'id', 'zone_name', 'store_location__name', 'zone_type'
    ))

    return Response({
        'target_stores': target_stores,
        'target_zones': target_zones
    })

Response:

{
  "target_stores": [
    {
      "id": "uuid-store-1",
      "name": "Downtown Location",
      "store_code": "DT-001"
    },
    {
      "id": "uuid-store-2",
      "name": "Airport Location",
      "store_code": "AP-002"
    }
  ],
  "target_zones": [
    {
      "id": "uuid-zone-1",
      "zone_name": "Coffee Bar",
      "store_location__name": "Downtown Location",
      "zone_type": {"type": "merchandising"}
    },
    {
      "id": "uuid-zone-2",
      "zone_name": "Coffee Bar",
      "store_location__name": "Airport Location",
      "zone_type": {"type": "merchandising"}
    }
  ]
}

Use Case:

  • Display planogram rollout plan
  • Track which stores/zones should implement planogram
  • Planogram assignment management

PlanogramProductViewSet

ViewSet for planogram product assignments.

Purpose

Manages product assignments within planograms (which products, quantities, positioning).

Configuration

Queryset Optimization:

def get_queryset(self):
    return PlanogramProduct.objects.select_related('planogram', 'product')

Serializer Selection:

def get_serializer_class(self):
    if self.action in ['create', 'update', 'partial_update']:
        return PlanogramProductCreateSerializer  # With validation
    return PlanogramProductSerializer  # Read-only

Filtering:

  • Filters: planogram, product
  • Search: product__name, variant_sku, planogram__planogram_name
  • Ordering: product__name, variant_sku, quantity, created_at
  • Default ordering: planogram__planogram_name, product__name, variant_sku

CRUD Operations

Base URL: /api/planograms/products/

List: GET /api/planograms/products/

  • Filter by planogram or product
  • Search by product name, variant SKU, or planogram name
  • Order by product name, SKU, quantity, or creation date

Retrieve: GET /api/planograms/products/{id}/

Response:

{
  "id": "uuid-plano-prod-123",
  "sanity_id": "planogram-product-abc",
  "planogram": "uuid-planogram-456",
  "product": "uuid-product-789",
  "product_name": "Dark Roast Coffee",
  "variant_sku": "DARK-12OZ",
  "quantity": 15,
  "shelf_position": "A3",
  "facing_count": 3,
  "created_at": "2025-11-18T10:00:00Z",
  "updated_at": "2025-11-18T10:00:00Z"
}

Create: POST /api/planograms/products/

{
  "planogram": "uuid-planogram-456",
  "product": "uuid-product-789",
  "variant_sku": "DARK-12OZ",
  "quantity": 15,
  "shelf_position": "A3",
  "facing_count": 3,
  "sanity_id": "planogram-product-xyz"
}

Validation (Create/Update):

  • Validates variant_sku exists for the product
  • Prevents duplicate products in same planogram

Update: PUT/PATCH /api/planograms/products/{id}/

Delete: DELETE /api/planograms/products/{id}/


ZoneFilter

Custom filter for Zone model with enhanced filtering capabilities.

Configuration

class ZoneFilter(django_filters.FilterSet):
    zone_name = django_filters.CharFilter(lookup_expr='icontains')
    store_name = django_filters.CharFilter(field_name='store_location__name', lookup_expr='icontains')

    class Meta:
        model = Zone
        fields = {
            'is_shown': ['exact'],
            'store_location': ['exact'],
        }

Available Filters

FilterTypeLookupDescription
zone_nameCharFiltericontainsCase-insensitive zone name search
store_nameCharFiltericontainsCase-insensitive store name search
is_shownBooleanFilterexactFilter by visibility status
store_locationUUIDFilterexactFilter by store ID

Note: zone_type filter was removed because zone_type is now a JSONField with complex data structure, not a simple string choice field.


Permission Configuration

Current State: All ViewSets have permission_classes = [] (no authentication required)

Production Recommendation:

from rest_framework.permissions import IsAuthenticated

class ZoneViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated]  # Require authentication

Granular Permissions:

from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions

class ZoneViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, DjangoModelPermissions]
    # Requires: zones.view_zone, zones.add_zone, zones.change_zone, zones.delete_zone

Integration Patterns

Zone Creation from Sanity Webhook

Workflow:

1. Sanity CMS sends webhook with zone data
2. ZoneSerializer.to_internal_value() converts camelCase to snake_case
3. ZoneSerializer.create() creates Zone instance
4. ZoneSerializer._process_zone_type_modules() creates nested zone type
5. Returns Zone with nested relationships

Example:

# Webhook payload (camelCase)
{
  "_id": "zone-abc",
  "zoneName": "Coffee Bar",
  "storeLocation": {"_ref": "store-123"},
  "zoneType": [{
    "_type": "zoneInventory",
    "countFrequency": "weekly"
  }]
}

# Result: Zone created with ZoneInventory nested object

Inventory Infrastructure Creation

Workflow:

1. Par levels created in zone via Sanity webhook
2. POST /api/zones/{id}/sync_inventory/ to create infrastructure
3. ZoneAssignmentService.create_inventory_infrastructure_for_zone()
4. For each par level: ParLevelItem.ensure_inventory_level_exists()
5. InventoryLevel records created for tracking

Product Distribution Analysis

Workflow:

1. GET /api/zones/par-levels/by_product/?product_id=X&variant_sku=Y
2. Returns all zones where product should be stocked
3. Shows par quantities across all locations
4. Enables distribution planning and analysis

Zones Domain

Related Domains

DRF Documentation


Summary: Zones views provide comprehensive REST API endpoints for zone management, par level operations, planogram management, and inventory infrastructure creation with integration to ZoneAssignmentService for business logic.

Was this page helpful?