the_inventory

Seeders App

The seeders app is an independent Django application responsible for populating the database with sample data for development and testing. It provides a flexible, extensible seeding system with support for multi-tenancy, idempotent operations, and selective model seeding.

Quick Start

Seed the entire database with all sample data:

python manage.py seed_database --clear --create-default

This will:

  1. Ensure default Wagtail Locale rows exist (same set as seeders/wagtail_locale_seeder.py; staff can add more in Wagtail admin)
  2. Create a “Default” tenant (if it doesn’t exist)
  3. Create platform operator accounts (platform_super, platform_staff) with no TenantMembership (Wagtail / platform-only; emails under @system.local)
  4. Create tenant users (owner, coordinator, manager, tenant_viewer) with is_staff=False and roles on the Default tenant (SPA + tenant JWT; emails under @org.seed.local)
  5. Create 10 product categories (hierarchical)
  6. Create 15 products across categories
  7. Create 17 stock locations (warehouse structure)
  8. Create 19 stock records (inventory levels)
  9. Create 10 stock movements (audit history)
  10. Create low-stock scenarios for testing alerts

After adding or removing locales in Wagtail admin (Settings → Locales), refresh the Next.js locale list and rebuild:

python manage.py sync_frontend_locales
cd frontend && yarn build

Docker / Render (auto-seed on deploy)

Configuration: set environment variables on the container (not flags on the gunicorn command, not values inside the_inventory.settings.*). Examples:

Platform Where
Render Web Service → Environment → add variables
Docker docker run -e AUTO_SEED_DATABASE=true … or Compose environment: / env_file:

When they run: entrypoint.sh reads them once at container start, after migrate and before gunicorn. They are ignored if you only run python manage.py runserver locally (use manage.py seed_database yourself in that case).

Truthiness: 1, true, yes, or on (case-insensitive).

Variable Effect
AUTO_SEED_DATABASE When truthy, runs seed_database after migrations
SEED_TENANT Optional slug → --tenant=… (if unset, entrypoint uses --create-default)
SEED_MODELS Optional → --models=… (comma-separated)
SEED_CLEAR When truthy → --clear (wipes seeded inventory tables first; destructive)
SEED_QUIET When truthy → --quiet

See .env.example (section AUTO-SEED) for the full notes.

App Structure

seeders/
├── management/
│   └── commands/
│       ├── seed_database.py      # Django management command
│       └── sync_frontend_locales.py  # Export locales → frontend/src/i18n/locales-config.json
├── tests/                         # Comprehensive test suite (72 tests)
│   ├── test_base_seeder.py
│   ├── test_seed_command.py
│   ├── test_seeder_manager.py
│   └── test_seeder_tenant.py
├── apps.py                        # Django app configuration
├── base.py                        # BaseSeeder abstract class
├── seeder_manager.py              # SeederManager orchestration
├── tenant_seeder.py               # TenantSeeder
├── platform_user_seeder.py        # PlatformUserSeeder (Wagtail operators, no membership)
├── user_seeder.py                 # UserSeeder (tenant members, is_staff=False)
├── category_seeder.py             # CategorySeeder
├── product_seeder.py              # ProductSeeder
├── stock_location_seeder.py       # StockLocationSeeder
├── stock_record_seeder.py         # StockRecordSeeder
├── stock_movement_seeder.py       # StockMovementSeeder
├── low_stock_seeder.py            # LowStockSeeder
├── wagtail_locale_seeder.py       # Default Wagtail Locale bootstrap
└── README.md                      # This file

Architecture

Seeder Execution Order

Seeders run in dependency order to respect foreign key relationships:

  1. Wagtail localesensure_default_wagtail_locales() (global, not tenant-scoped) runs first inside SeederManager.seed()
  2. TenantSeeder — Creates/retrieves the tenant
  3. PlatformUserSeeder — Platform operators (is_staff / is_superuser) without tenant memberships
  4. UserSeeder — Tenant-plane users (is_staff=False) with TenantMembership and roles
  5. CategorySeeder — Creates product categories (hierarchical)
  6. ProductSeeder — Creates products
  7. StockLocationSeeder — Creates warehouse locations
  8. StockRecordSeeder — Creates stock records (inventory levels)
  9. StockMovementSeeder — Creates stock movements (audit history)
  10. LowStockSeeder — Creates low-stock scenarios

Key Components

BaseSeeder

Abstract base class providing common functionality:

SeederManager

Orchestrates all seeders in the correct dependency order:

Individual Seeders

Each seeder is responsible for one or more related models and implements idempotent operations.

Usage

Basic Seeding

Seed all models into the Default tenant:

python manage.py seed_database --create-default

Clear and Reseed

Clear existing data and reseed:

python manage.py seed_database --clear --create-default

Seed Specific Tenant

Seed into a specific tenant:

python manage.py seed_database --tenant=acme-corp

Seed Specific Models

Seed only certain models:

python manage.py seed_database --models=categories,products

Available models: users, categories, products, locations, records, movements

Quiet Mode

Suppress verbose output:

python manage.py seed_database --quiet --create-default

Idempotency

All seeders are idempotent — they can be safely re-run without creating duplicates:

Multi-Tenancy

All seeders support multi-tenancy:

  1. TenantSeeder creates/retrieves a tenant and returns it
  2. SeederManager passes the tenant to all downstream seeders
  3. Each seeder receives the tenant via execute(tenant=tenant_instance)
  4. All created models are automatically associated with the tenant

Example: Programmatic Seeding

from seeders import SeederManager
from tenants.models import Tenant
from tenants.context import set_current_tenant, clear_current_tenant

# Get or create tenant
tenant = Tenant.objects.get_or_create(
    slug="acme-corp",
    defaults={"name": "ACME Corporation", "is_active": True}
)[0]

# Set tenant context
set_current_tenant(tenant)

try:
    # Seed data
    manager = SeederManager(verbose=True, clear_data=False)
    manager.seed(tenant=tenant)
finally:
    # Clear tenant context
    clear_current_tenant()

Creating Custom Seeders

To create a custom seeder:

  1. Inherit from BaseSeeder
  2. Implement the seed() method
  3. Use self.create_with_tenant() or self.add_root_with_tenant() for model creation
  4. Add to SeederManager in the correct dependency order

Example

from seeders.base import BaseSeeder
from inventory.models import Product

class CustomSeeder(BaseSeeder):
    """Custom seeder for specific models."""

    def seed(self):
        """Seed custom data."""
        self.log("Creating custom products...")
        
        # Check if already exists
        if Product.objects.filter(sku="CUSTOM-001").exists():
            self.log("Custom product already exists, skipping")
            return
        
        # Create product
        product = self.create_with_tenant(
            Product,
            sku="CUSTOM-001",
            name="Custom Product",
            description="A custom product"
        )
        self.log(f"Created product: {product.name}")

Testing

Run all seeder tests:

python manage.py test seeders.tests

Run specific test class:

python manage.py test seeders.tests.test_seeder_tenant.TenantSeederTestCase

Run with verbose output:

python manage.py test seeders.tests --verbosity=2

Test Coverage

The seeders app includes 72 comprehensive tests covering:

Sample Data

Test users

Platform (no tenant membership — use Wagtail):

Tenant (memberships on seeded tenant — use SPA / tenant JWT):

See TEST_USERS.md for full tables and troubleshooting.

Categories

Products (15 total)

Warehouse Structure

Stock Levels

Movement History

Performance Considerations

Troubleshooting

“Default tenant does not exist”

Use --create-default flag:

python manage.py seed_database --create-default

“Tenant with slug ‘X’ not found”

Create the tenant first via admin or use --create-default for the Default tenant.

Duplicate data after re-running

All seeders are idempotent. If duplicates appear, check that:

  1. Seeders are using get_or_create() or existence checks
  2. Unique constraints are properly defined on models
  3. No custom seeders are bypassing idempotency checks

Integration with Django

The seeders app is registered in INSTALLED_APPS in the_inventory/settings/base.py:

INSTALLED_APPS = [
    # ...
    "seeders",
    # ...
]

This enables:

Future Enhancements