This guide is for contributors who want to develop and extend The Inventory backend.
- Development Setup
- Project Structure
- Architecture Overview
- Testing
- Code Standards
- Common Development Tasks
- Debugging
- Python 3.12+
- Git
- PostgreSQL (optional, SQLite for dev)
# Clone repository
git clone https://github.com/Ndevu12/the_inventory.git
cd the_inventory
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txt
# Navigate to backend
cd src
# Run migrations
python manage.py migrate
# Create superuser
python manage.py createsuperuser
# Seed database (optional)
python manage.py seed_database --clear --create-default
# Start development server
python manage.py runserver# Check Django setup
python manage.py check
# Run tests
python manage.py test
# Access admin
# Visit http://localhost:8000/admin/src/
├── manage.py # Django entry point
├── the_inventory/ # Project configuration
│ ├── settings/
│ │ ├── base.py # Shared settings
│ │ ├── dev.py # Development settings
│ │ └── production.py # Production settings
│ ├── urls.py # Root URL configuration
│ ├── wsgi.py # WSGI application
│ └── templates/ # Project templates
│
├── api/ # REST API app
│ ├── views.py # API viewsets
│ ├── serializers.py # DRF serializers
│ ├── permissions.py # Permission classes
│ ├── filters.py # Filtering logic
│ └── tests/ # API tests
│
├── inventory/ # Core inventory app
│ ├── models/ # Domain models
│ │ ├── base.py # TimeStampedModel
│ │ ├── product.py # Product, ProductImage
│ │ ├── category.py # Category
│ │ └── stock.py # Stock models
│ ├── services/ # Business logic
│ │ └── stock.py # StockService
│ ├── admin.py # Django admin
│ └── tests/ # Inventory tests
│
├── procurement/ # Procurement app
├── sales/ # Sales app
├── reports/ # Reporting app
├── tenants/ # Multi-tenancy app
└── locale/ # Translations
tests/ # Test suite (repo root)
├── runner.py # Custom test runner
├── api/ # API tests
├── inventory/ # Inventory tests
└── seeders/ # Seeder tests
┌─────────────────────────────────────────┐
│ REST API Layer (api/) │
│ - Viewsets, Serializers, Permissions │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ Service Layer (*/services/) │
│ - Business Logic, Validation │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ Model Layer (*/models/) │
│ - Domain Models, Relationships │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ Database (PostgreSQL) │
└─────────────────────────────────────────┘
Object-Oriented Programming (OOP)
- All business logic in classes
- Services encapsulate domain operations
- Models define data structure
Multi-Tenancy
- All models inherit from
TimeStampedModel tenantFK on all modelsTenantMiddlewareresolves tenant per requestTenantAwareManagerauto-scopes queries
Immutable Movements
- Stock movements are immutable (no update/delete)
- Complete audit trail
- Point-in-time cost tracking
cd src
# All tests (seeders excluded)
python manage.py test
# Specific app
python manage.py test tests.api
python manage.py test tests.inventory
# Specific test class
python manage.py test tests.api.test_auth.AuthTestCase
# Specific test method
python manage.py test tests.api.test_auth.AuthTestCase.test_login
# With coverage
coverage run --source='.' manage.py test
coverage reportExample test:
from django.test import TestCase
from inventory.models import Product, Category
class ProductTestCase(TestCase):
def setUp(self):
self.category = Category.objects.create(
name="Electronics",
slug="electronics"
)
def test_create_product(self):
product = Product.objects.create(
sku="PROD-001",
name="Widget",
category=self.category,
unit_of_measure="pcs",
unit_cost=10.00
)
self.assertEqual(product.sku, "PROD-001")
self.assertEqual(product.name, "Widget")
def test_product_str(self):
product = Product.objects.create(
sku="PROD-001",
name="Widget",
category=self.category,
unit_of_measure="pcs",
unit_cost=10.00
)
self.assertEqual(str(product), "Widget (PROD-001)")- Unit tests — Test individual models/functions
- Integration tests — Test workflows across services
- API tests — Test REST endpoints
- Seeder tests — Test database seeding
Follow PEP 8:
# Check code style
ruff check src tests
# Format code
ruff format src tests- Models: Singular, PascalCase (
Product, notProducts) - Functions: Lowercase with underscores (
get_product_by_sku) - Constants: UPPERCASE (
MAX_PAGE_SIZE) - Private methods: Leading underscore (
_validate_quantity)
class StockService:
"""Service for managing stock movements and levels."""
def process_movement(self, movement):
"""
Process a stock movement and update stock records.
Args:
movement: StockMovement instance
Returns:
Updated StockRecord
Raises:
ValidationError: If movement is invalid
"""
# ImplementationOrganize imports:
# Standard library
import json
from datetime import datetime
# Third-party
from django.db import models
from rest_framework import serializers
# Local
from inventory.models import Product
from .services import StockService- Create model in
app/models/:
# inventory/models/product.py
class Product(TimeStampedModel):
sku = models.CharField(max_length=100, unique=True)
name = models.CharField(max_length=255)
# ... fields- Create migration:
python manage.py makemigrations
python manage.py migrate- Create serializer in
api/serializers.py:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'sku', 'name', ...]- Create viewset in
api/views.py:
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [IsAuthenticated]- Register in URLs in
api/urls.py:
router.register(r'products', ProductViewSet)- Write tests in
tests/api/test_products.py:
class ProductAPITestCase(TestCase):
def test_list_products(self):
# Test implementation- Create viewset method:
class ProductViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def activate(self, request, pk=None):
product = self.get_object()
product.is_active = True
product.save()
return Response({'status': 'activated'})- Test the endpoint:
def test_activate_product(self):
response = self.client.post(f'/api/v1/products/{product.id}/activate/')
self.assertEqual(response.status_code, 200)- Create service method:
class StockService:
def get_low_stock_items(self, tenant):
"""Get all items below reorder point."""
return StockRecord.objects.filter(
tenant=tenant,
quantity__lte=F('product__reorder_point')
)- Test the service:
def test_get_low_stock_items(self):
items = StockService().get_low_stock_items(self.tenant)
self.assertEqual(len(items), 1)- Use in viewset:
class StockRecordsViewSet(viewsets.ReadOnlyModelViewSet):
@action(detail=False)
def low_stock(self, request):
items = StockService().get_low_stock_items(request.tenant)
serializer = self.get_serializer(items, many=True)
return Response(serializer.data)python manage.py shell
# Import models
from inventory.models import Product
# Query data
products = Product.objects.all()
product = Product.objects.get(sku='PROD-001')
# Create data
product = Product.objects.create(
sku='PROD-002',
name='New Product',
unit_of_measure='pcs',
unit_cost=15.00
)
# Update data
product.name = 'Updated Name'
product.save()
# Delete data
product.delete()import logging
logger = logging.getLogger(__name__)
def process_movement(self, movement):
logger.debug(f"Processing movement: {movement.id}")
logger.info(f"Movement type: {movement.movement_type}")
logger.error(f"Error processing movement: {error}")Install and configure:
pip install django-debug-toolbarAdd to settings/dev.py:
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1']Access at: http://localhost:8000/__debug__/
def process_movement(self, movement):
breakpoint() # Execution pauses here
# Use pdb commands: n (next), s (step), c (continue), p (print)Use select_related() and prefetch_related():
# Bad: N+1 queries
products = Product.objects.all()
for product in products:
print(product.category.name) # Query per product
# Good: Single query
products = Product.objects.select_related('category')
for product in products:
print(product.category.name) # No additional queriesfrom django.core.cache import cache
def get_products(self):
cached = cache.get('products_list')
if cached:
return cached
products = Product.objects.all()
cache.set('products_list', products, 300) # Cache for 5 minutes
return productsAdd database indexes for frequently queried fields:
class Product(models.Model):
sku = models.CharField(max_length=100, unique=True, db_index=True)
name = models.CharField(max_length=255, db_index=True)- Read Architecture: See Architecture Guide
- Contributing: See Contributing Guide
- Testing: See Testing Guide
- Deployment: See Deployment Guide
- 📖 Check FAQ
- 🐛 See Troubleshooting Guide
- 💬 Open an Issue on GitHub