# BizTrack — Complete Project Documentation

> A single-tenant business management system built with Django, HTMX, DaisyUI and PostgreSQL.
> This document explains every part of the project — what it does, how it works, and why it was built that way.

---

## Table of Contents

1. [Project Overview](#1-project-overview)
2. [Technology Stack](#2-technology-stack)
3. [Project Structure](#3-project-structure)
4. [Setup & Configuration](#4-setup--configuration)
5. [Apps — What Each One Does](#5-apps--what-each-one-does)
6. [Models — The Database](#6-models--the-database)
7. [Views — The Logic](#7-views--the-logic)
8. [Forms — User Input](#8-forms--user-input)
9. [URLs — Routing](#9-urls--routing)
10. [Templates — The UI](#10-templates--the-ui)
11. [Decorators — Access Control](#11-decorators--access-control)
12. [Utilities — Shared Helpers](#12-utilities--shared-helpers)
13. [Context Processors](#13-context-processors)
14. [Template Tags](#14-template-tags)
15. [Financial Logic Explained](#15-financial-logic-explained)
16. [How Each Feature Works End-to-End](#16-how-each-feature-works-end-to-end)
17. [Imports Reference](#17-imports-reference)
18. [Static Files & CSS](#18-static-files--css)
19. [Admin Panel](#19-admin-panel)
20. [Error Handling](#20-error-handling)
21. [CSV Exports](#21-csv-exports)
22. [Print & Receipt System](#22-print--receipt-system)
23. [Common Issues & Fixes](#23-common-issues--fixes)

---

## 1. Project Overview

BizTrack is a **single-tenant** business management system. Single-tenant means it is designed for **one business only** — there is no public signup, no multi-company support. The owner sets up the system once, creates staff accounts, and the team uses it to run their daily operations.

### What the system does

| Module | Purpose |
|---|---|
| **Sales** | Record sales, deduct stock, deposit to accounts |
| **Invoices** | Credit sales, track payments, mark as paid |
| **Inventory** | Manage products, categories, stock levels |
| **Purchase Orders** | Buy stock from suppliers, update inventory |
| **Expenses** | Record outgoing money by category |
| **Accounts** | Cash Till, Bank, Mobile Money balances |
| **Finance Dashboard** | Revenue, COGS, Gross Profit, Net Profit |
| **P&L Report** | Full Profit & Loss statement |
| **Users** | Staff accounts with role-based access |
| **CSV Export** | Download data as spreadsheets |
| **Print** | Receipts and invoices via browser print |

### What it is NOT

- Not multi-tenant (not SaaS)
- Not an accounting system (no double-entry bookkeeping)
- Not an e-commerce platform
- Not a payroll system

---

## 2. Technology Stack

### Backend
- **Python 3.9+** — programming language
- **Django 4.2** — web framework. Handles URLs, views, models, forms, authentication
- **PostgreSQL** — relational database. Stores all business data
- **psycopg2-binary** — Python adapter that connects Django to PostgreSQL
- **python-decouple** — reads environment variables from a `.env` file so secrets are not hardcoded
- **Pillow** — Python image library used for business logo uploads

### Frontend
- **DaisyUI** — component library built on top of TailwindCSS. Provides buttons, cards, tables, badges etc.
- **TailwindCSS v3** — utility-first CSS framework. You style elements by adding class names
- **HTMX** — allows pages to make server requests without writing JavaScript. Used for dynamic interactions

### Build Tools
- **Node.js / npm** — used only to build CSS. Not used at runtime
- **TailwindCSS CLI** — scans templates and generates only the CSS classes that are actually used

---

## 3. Project Structure

```
biztrack/
│
├── config/                     ← Django project settings
│   ├── settings.py             ← All configuration
│   ├── urls.py                 ← Root URL routing
│   └── wsgi.py                 ← Web server entry point
│
├── core/                       ← Shared utilities used by all apps
│   ├── decorators.py           ← role_required decorator
│   ├── views.py                ← 403 and 404 error handlers
│   ├── utils.py                ← CSV export helper
│   ├── context_processors.py   ← Injects business info into all templates
│   └── templatetags/
│       └── form_tags.py        ← Custom template filter for DaisyUI forms
│
├── accounts/                   ← User management & authentication
│   ├── models.py               ← Custom User model with roles
│   ├── views.py                ← Login, logout, user CRUD
│   ├── forms.py                ← Login, create user, change password forms
│   └── urls.py                 ← /accounts/ routes
│
├── business/                   ← Business profile (name, logo, currency etc)
│   ├── models.py               ← Business model (single instance)
│   ├── views.py                ← View/edit profile
│   ├── forms.py                ← Business form
│   └── urls.py                 ← /business/ routes
│
├── inventory/                  ← Products, stock, suppliers, purchase orders
│   ├── models.py               ← Product, Category, StockMovement, Supplier, PurchaseOrder
│   ├── views.py                ← All inventory views
│   ├── forms.py                ← Product, supplier, PO forms
│   ├── utils.py                ← add_stock() and deduct_stock() helpers
│   └── urls.py                 ← /inventory/ routes
│
├── sales/                      ← Sales and invoices
│   ├── models.py               ← Sale, SaleItem, Invoice, InvoiceItem, InvoicePayment
│   ├── views.py                ← All sales and invoice views + CSV exports
│   ├── forms.py                ← Sale form, formsets, invoice forms
│   ├── utils.py                ← process_sale() helper
│   └── urls.py                 ← /sales/ routes
│
├── finance/                    ← Accounts, expenses, income, reports
│   ├── models.py               ← Account, AccountTransaction, Expense, OtherIncome, OwnerDraw
│   ├── views.py                ← Dashboard, expense views, CSV exports
│   ├── forms.py                ← Account, expense, income, draw forms
│   ├── reports.py              ← Financial calculation functions
│   └── urls.py                 ← /finance/ routes
│
├── templates/                  ← All HTML templates
│   ├── base.html               ← Master layout (sidebar + navbar)
│   ├── 403.html                ← Access denied page
│   ├── 404.html                ← Not found page
│   ├── partials/
│   │   ├── navbar.html         ← Top bar (mobile only)
│   │   ├── sidebar.html        ← Navigation sidebar
│   │   └── pagination.html     ← Reusable pagination links
│   ├── accounts/               ← Login, user list, user form, profile
│   ├── business/               ← Business profile form
│   ├── inventory/              ← Product, category, supplier, PO templates
│   ├── sales/
│   │   ├── ...                 ← Sale and invoice templates
│   │   └── pdf/
│   │       ├── receipt.html    ← Printable receipt
│   │       └── invoice.html    ← Printable A4 invoice
│   └── finance/                ← Dashboard, expenses, accounts, P&L templates
│
├── static/
│   ├── src/
│   │   └── main.css            ← Tailwind source (input)
│   ├── css/
│   │   └── main.css            ← Built CSS (output, served to browser)
│   └── js/
│       └── htmx.min.js         ← HTMX library (local copy)
│
├── media/                      ← Uploaded files (business logo)
├── staticfiles/                ← collectstatic output (production)
├── .env                        ← Environment variables (never commit this)
├── manage.py                   ← Django management command runner
├── package.json                ← Node.js config for building CSS
└── tailwind.config.js          ← Tailwind + DaisyUI configuration
```

---

## 4. Setup & Configuration

### Environment Variables (`.env`)

```env
DEBUG=True
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgres://user:password@localhost:5432/biztrack_db
ALLOWED_HOSTS=localhost,127.0.0.1
```

These are read by `python-decouple` in `settings.py`. Never hardcode secrets in your code.

### `config/settings.py` — Key Settings Explained

```python
from pathlib import Path
from decouple import config

# BASE_DIR is the root of the project (where manage.py lives)
BASE_DIR = Path(__file__).resolve().parent.parent

# Read from .env file
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=True, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')

# AUTH_USER_MODEL tells Django to use our custom User model
# instead of the built-in one. MUST be set before first migration.
AUTH_USER_MODEL = 'accounts.User'

# Where Django looks for templates
TEMPLATES = [{
    'DIRS': [BASE_DIR / 'templates'],  # our templates/ folder
    ...
}]

# Static files (CSS, JS)
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']   # our static/ folder
STATIC_ROOT = BASE_DIR / 'staticfiles'     # where collectstatic outputs

# Media files (uploaded images)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Login settings
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = 'finance:dashboard'
LOGOUT_REDIRECT_URL = 'accounts:login'
```

### Why PostgreSQL instead of SQLite?

SQLite (Django's default) is a file-based database. It's fine for development but cannot handle concurrent writes — two users saving at the same time can corrupt data. PostgreSQL is a proper server database that handles many users simultaneously.

---

## 5. Apps — What Each One Does

### `core` — Shared Foundation
Not a user-facing feature. Contains tools used by every other app:
- `decorators.py` — controls who can access which views
- `utils.py` — CSV export helper used by sales, finance, and inventory
- `context_processors.py` — automatically passes business info to every template
- `templatetags/form_tags.py` — adds DaisyUI CSS classes to form fields

### `accounts` — Users & Auth
Handles who can log in and what role they have. Extends Django's built-in auth system with a `role` field.

### `business` — Business Profile
One model, one record. Stores business name, logo, currency, phone, address. Referenced everywhere — receipts, invoices, dashboard.

### `inventory` — Stock Management
- **Categories** — group products (Beverages, Dairy etc.)
- **Products** — items you sell with cost price, selling price, stock level
- **StockMovement** — log of every stock change (in/out/adjustment)
- **Suppliers** — who you buy from
- **PurchaseOrders** — record of stock being purchased

### `sales` — Revenue
- **Sale** — a completed transaction (cash or credit)
- **SaleItem** — individual line items within a sale
- **Invoice** — a credit sale with payment tracking
- **InvoiceItem** — line items on an invoice
- **InvoicePayment** — payments recorded against an invoice

### `finance` — Money Management
- **Account** — money containers (Cash Till, Bank, Mobile Money)
- **AccountTransaction** — every movement of money in/out of accounts
- **ExpenseCategory** — types of expenses (Rent, Salaries etc.)
- **Expense** — money going out for business operations
- **OtherIncome** — non-sale income (commission, rent received etc.)
- **OwnerDraw** — owner taking personal money from business

---

## 6. Models — The Database

### `accounts/models.py`

```python
class User(AbstractUser):
    # AbstractUser gives us username, password, email, first_name, last_name
    # We ADD role and phone on top of that

    class Role(models.TextChoices):
        # TextChoices creates a set of valid string values
        # Format: CONSTANT = 'db_value', 'Human readable label'
        OWNER = 'owner', 'Owner'
        MANAGER = 'manager', 'Manager'
        CASHIER = 'cashier', 'Cashier'
        STOREKEEPER = 'storekeeper', 'Storekeeper'

    role = models.CharField(max_length=20, choices=Role.choices, default=Role.CASHIER)
    phone = models.CharField(max_length=20, blank=True)  # blank=True means optional

    @property
    def is_owner(self):
        # @property means you call it like user.is_owner (no parentheses)
        return self.role == self.Role.OWNER
```

**Why extend AbstractUser?** Django's built-in User model is fixed. By extending AbstractUser we can add custom fields (role, phone) while keeping all of Django's authentication features (password hashing, sessions, login etc.).

---

### `business/models.py`

```python
class Business(models.Model):
    name = models.CharField(max_length=255)
    logo = models.ImageField(upload_to='business/', blank=True, null=True)
    # upload_to='business/' means logos are saved to media/business/
    currency = models.CharField(max_length=10, default='UGX')

    @classmethod
    def get(cls):
        # @classmethod means you call it on the class: Business.get()
        # Returns the first (and only) business record
        # Used in views: business = Business.get()
        return cls.objects.first()
```

---

### `finance/models.py`

```python
class Account(models.Model):
    name = models.CharField(max_length=100)      # "Cash Till"
    account_type = models.CharField(...)          # cash/bank/mobile/other
    balance = models.DecimalField(max_digits=15, decimal_places=2, default=0)
    # DecimalField is used for money — never use FloatField for currency
    # (floats have rounding errors, decimals are exact)

    def deposit(self, amount):
        # Increases balance and saves
        self.balance += amount
        self.save()

    def withdraw(self, amount):
        # Checks there is enough money first
        if self.balance < amount:
            raise ValueError(f"Insufficient balance in {self.name}")
        self.balance -= amount
        self.save()


class AccountTransaction(models.Model):
    # Records EVERY movement of money for audit purposes
    account = models.ForeignKey(Account, ...)   # which account
    tx_type = models.CharField(...)              # sale/expense/income/draw
    direction = models.CharField(...)            # 'in' or 'out'
    amount = models.DecimalField(...)
    description = models.CharField(...)
    date = models.DateField(...)
```

**Why log transactions separately?** The account balance tells you the current total. The transaction log tells you HOW you got there — every sale, every expense, every deposit. This is an audit trail. You can reconstruct the balance at any point in history.

---

### `inventory/models.py`

```python
class Product(models.Model):
    cost_price = models.DecimalField(...)     # what you paid for it
    selling_price = models.DecimalField(...)  # what you sell it for
    stock_quantity = models.DecimalField(...) # how many in stock

    @property
    def is_low_stock(self):
        return self.stock_quantity <= self.low_stock_alert

    @property
    def stock_value(self):
        # Total value of current stock at cost price
        return self.stock_quantity * self.cost_price


class StockMovement(models.Model):
    # Records every time stock changes — in, out, adjustment
    product = models.ForeignKey(Product, on_delete=models.PROTECT, ...)
    # PROTECT means: do not allow deleting a product if it has stock movements
    # This prevents accidental data loss
    movement_type = models.CharField(...)   # stock_in/stock_out/sale/adjustment
    quantity = models.DecimalField(...)
    cost_price = models.DecimalField(...)   # cost at time of movement
```

---

### `sales/models.py`

```python
class Sale(models.Model):
    reference = models.CharField(max_length=50, unique=True)  # SALE-0001
    # unique=True means no two sales can have the same reference

    @property
    def subtotal(self):
        # Sum of all line items before discount
        return sum(item.line_total for item in self.items.all())

    @property
    def total(self):
        return self.subtotal - self.discount

    @property
    def total_cost(self):
        # Total cost of goods sold
        return sum(item.line_cost for item in self.items.all())

    @property
    def gross_profit(self):
        return self.total - self.total_cost


class SaleItem(models.Model):
    sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name='items')
    # CASCADE means: if sale is deleted, delete all its items too
    # related_name='items' means: sale.items.all() gives you the items

    unit_price = models.DecimalField(...)  # selling price AT TIME OF SALE
    unit_cost = models.DecimalField(...)   # cost price AT TIME OF SALE
    # We store prices at time of sale because prices can change later
    # This gives you accurate historical records


class Invoice(models.Model):
    @property
    def total_paid(self):
        return sum(p.amount for p in self.payments.filter(is_cancelled=False))

    @property
    def balance_due(self):
        return self.total - self.total_paid

    def update_status(self):
        # Automatically sets status based on payment
        if self.total_paid <= 0:
            self.status = self.Status.UNPAID
        elif self.total_paid >= self.total:
            self.status = self.Status.PAID
        else:
            self.status = self.Status.PARTIAL
        self.save()
```

---

## 7. Views — The Logic

Views are Python functions that:
1. Receive an HTTP request
2. Do some work (query database, process form, calculate something)
3. Return an HTTP response (usually a rendered HTML template)

### Basic view pattern

```python
def some_view(request):
    if request.method == 'POST':
        # User submitted a form
        form = SomeForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, 'Saved!')
            return redirect('app:some_list')  # redirect after POST
    else:
        # User is loading the page for the first time
        form = SomeForm()

    return render(request, 'app/some_template.html', {
        'form': form,
        # anything in this dict is available in the template
    })
```

### Why `redirect` after POST?

This is called the **POST/Redirect/GET** pattern. After a form submission, we redirect the user to another page. This prevents the browser from resubmitting the form if the user refreshes the page (which would create duplicate records).

### `finance/reports.py` — Financial Calculations

This file is separate from `views.py` on purpose. It contains only pure calculation functions, no HTTP handling. This makes it reusable and easier to test.

```python
def get_summary(start_date, end_date):
    # Queries completed sales in date range
    sale_items = SaleItem.objects.filter(
        sale__date__range=(start_date, end_date),
        sale__status='completed'
    )
    # sale__date means: follow the ForeignKey from SaleItem to Sale,
    # then filter on Sale's date field

    revenue = sum(item.line_total for item in sale_items)
    cogs = sum(item.line_cost for item in sale_items)
    gross_profit = revenue - cogs

    total_expenses = Expense.objects.filter(
        date__range=(start_date, end_date)
    ).aggregate(total=Sum('amount'))['total'] or 0
    # aggregate() runs a SQL SUM — more efficient than loading all records
    # ['total'] gets the value, 'or 0' handles the case where there are no expenses

    net_profit = gross_profit + total_other_income - total_expenses

    return {
        'revenue': revenue,
        'cogs': cogs,
        'gross_profit': gross_profit,
        'net_profit': net_profit,
        ...
    }
```

---

## 8. Forms — User Input

Django forms do three things:
1. **Render** HTML form fields
2. **Validate** submitted data (required fields, correct types, custom rules)
3. **Save** valid data to the database (ModelForms)

### ModelForm vs Form

```python
# ModelForm — tied to a database model, can save automatically
class ExpenseForm(forms.ModelForm):
    class Meta:
        model = Expense
        fields = ['category', 'account', 'amount', 'description', 'date']

# Plain Form — not tied to a model, for custom logic
class LoginForm(forms.Form):
    username = forms.CharField(max_length=150)
    password = forms.CharField(widget=forms.PasswordInput)
```

### Custom validation

```python
def clean(self):
    # clean() runs after all individual fields are validated
    cleaned_data = super().clean()
    account = cleaned_data.get('account')
    amount = cleaned_data.get('amount')
    if account and amount:
        if account.balance < amount:
            # This error appears at the top of the form (non-field error)
            raise forms.ValidationError('Insufficient balance.')
    return cleaned_data
```

### Inline Formsets

Used in Sale and Invoice creation — allows multiple rows (items) in one form.

```python
# This creates a formset that handles multiple SaleItem rows
# linked to a single Sale
SaleItemFormSet = forms.inlineformset_factory(
    Sale,           # parent model
    SaleItem,       # child model
    form=SaleItemForm,
    extra=3,        # show 3 empty rows by default
    min_num=1,      # require at least 1 item
    validate_min=True,
    can_delete=True # show delete checkbox on each row
)
```

---

## 9. URLs — Routing

URLs map web addresses to view functions. Django checks each URL pattern in order and calls the matching view.

### Root `config/urls.py`

```python
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('finance.urls')),        # dashboard at root /
    path('accounts/', include('accounts.urls')),
    path('business/', include('business.urls')),
    path('inventory/', include('inventory.urls')),
    path('sales/', include('sales.urls')),
    path('finance/', include('finance.urls')),
]
```

`include()` delegates to the app's own urls.py. This keeps each app self-contained.

### App-level URLs with namespaces

```python
# sales/urls.py
app_name = 'sales'  # namespace

urlpatterns = [
    path('', views.sale_list, name='sale_list'),
    path('create/', views.sale_create, name='sale_create'),
    path('<int:pk>/', views.sale_detail, name='sale_detail'),
    # <int:pk> captures an integer from the URL and passes it to the view as 'pk'
]
```

Namespaced URLs are referenced as `sales:sale_list`, `sales:sale_detail` in templates and `redirect()` calls. The namespace prevents name conflicts between apps (two apps could both have a view called `list` without conflict).

---

## 10. Templates — The UI

### Template Inheritance

Every page extends `base.html`:

```html
{% extends 'base.html' %}       ← inherit the master layout

{% block title %}Sales{% endblock %}    ← fill in the title slot

{% block content %}             ← fill in the main content slot
  ... your page content here ...
{% endblock %}
```

`base.html` defines the slots with `{% block name %}{% endblock %}`. Child templates fill them in. Everything in `base.html` (sidebar, navbar, CSS) appears on every page automatically.

### Template Tags & Filters

```html
{{ variable }}                  ← output a variable
{{ amount|floatformat:0 }}      ← apply a filter (format number, 0 decimal places)
{{ date|date:"d M Y" }}         ← format a date

{% for item in items %}         ← loop
{% empty %}                     ← shown if list is empty
{% endfor %}

{% if user.is_owner %}          ← conditional
{% elif user.is_manager %}
{% else %}
{% endif %}

{% url 'sales:sale_list' %}     ← generate a URL by name
{% url 'sales:sale_detail' sale.pk %}  ← URL with argument

{% include 'partials/pagination.html' %}   ← include another template
{% load static %}               ← load the static files tag library
{% static 'css/main.css' %}     ← generate URL for a static file
{% load form_tags %}            ← load our custom template tags
{{ field|daisyui }}             ← apply our custom filter
{% csrf_token %}                ← security token (required in every form)
{% now "d M Y H:i" %}          ← current date/time
```

### `partials/` — Reusable Template Pieces

- `navbar.html` — top bar shown on mobile only (has hamburger menu button)
- `sidebar.html` — navigation menu shown on all pages
- `pagination.html` — previous/next page buttons, included at bottom of list pages

---

## 11. Decorators — Access Control

### What is a decorator?

A decorator wraps a function and runs code before (and/or after) it. In Django, view decorators typically check something and either allow the view to run or return an early response (like a redirect).

### `core/decorators.py`

```python
def role_required(*roles):
    # *roles means accept any number of role arguments
    # e.g. @role_required('owner', 'manager')

    def decorator(view_func):
        # decorator receives the view function

        def wrapper(request, *args, **kwargs):
            # wrapper runs BEFORE the actual view

            if not request.user.is_authenticated:
                # User is not logged in → send to login page
                # ?next= tells the login page where to redirect after login
                return redirect(f'/accounts/login/?next={request.path}')

            if request.user.role not in roles:
                # User is logged in but wrong role → show 403 Forbidden
                raise PermissionDenied

            # All checks passed → run the actual view
            return view_func(request, *args, **kwargs)

        wrapper.__name__ = view_func.__name__
        # This line preserves the function name for Django's URL resolver
        # Without it, all decorated views look the same to Django

        return wrapper
    return decorator
```

### How to use it

```python
# Only owners and managers can delete expenses
@role_required('owner', 'manager')
def expense_delete(request, pk):
    ...

# Only owners can access
@role_required('owner')
def draw_create(request):
    ...

# Cashiers, managers, and owners can create sales
@role_required('owner', 'manager', 'cashier')
def sale_create(request):
    ...
```

### Role permissions summary

| Feature | Owner | Manager | Cashier | Storekeeper |
|---|---|---|---|---|
| Dashboard & Reports | ✅ | ✅ | ❌ | ❌ |
| Create Sales | ✅ | ✅ | ✅ | ❌ |
| Cancel Sales | ✅ | ✅ | ❌ | ❌ |
| Delete Sales | ✅ | ❌ | ❌ | ❌ |
| Invoices | ✅ | ✅ | ✅ | ❌ |
| Products | ✅ | ✅ | ❌ | ✅ |
| Stock Adjust | ✅ | ✅ | ❌ | ✅ |
| Expenses | ✅ | ✅ | ❌ | ❌ |
| Accounts | ✅ | ✅ | ❌ | ❌ |
| Owner Draws | ✅ | ❌ | ❌ | ❌ |
| User Management | ✅ | ❌ | ❌ | ❌ |
| Business Settings | ✅ | ❌ | ❌ | ❌ |

---

## 12. Utilities — Shared Helpers

### `inventory/utils.py`

```python
def add_stock(product, quantity, cost_price=None, reference='', note='', user=None):
    """
    Adds stock to a product and logs the movement.

    Called when:
    - A purchase order is marked as received
    - A sale is cancelled (stock reversal)
    - Manual stock adjustment (stock_in)

    product      — the Product instance to update
    quantity     — how much to add
    cost_price   — the new cost price (updates product if provided)
    reference    — e.g. 'PO-0001' or 'SALE-0042'
    note         — human readable reason
    user         — who did this (for audit trail)
    """
    product.stock_quantity += quantity
    if cost_price is not None:
        product.cost_price = cost_price  # update cost on restock
    product.save()

    StockMovement.objects.create(
        product=product,
        movement_type=StockMovement.MovementType.STOCK_IN,
        quantity=quantity,
        cost_price=cost_price or product.cost_price,
        reference=reference,
        note=note,
        created_by=user,
    )


def deduct_stock(product, quantity, reference='', note='', user=None):
    """
    Removes stock from a product and logs the movement.

    Called when:
    - A sale is processed (automatic deduction)
    - Manual adjustment or loss

    Raises ValueError if not enough stock — this prevents
    going negative which would be a data integrity problem.
    """
    if product.stock_quantity < quantity:
        raise ValueError(f"Not enough stock for {product.name}")
    product.stock_quantity -= quantity
    product.save()

    StockMovement.objects.create(
        product=product,
        movement_type=StockMovement.MovementType.SALE,
        quantity=quantity,
        cost_price=product.cost_price,
        reference=reference,
        note=note,
        created_by=user,
    )
```

### `sales/utils.py`

```python
@transaction.atomic
def process_sale(sale, user=None):
    """
    Runs after a Sale and its SaleItems are saved.
    Does everything that needs to happen when a sale is completed.

    @transaction.atomic means: if ANYTHING fails inside this function,
    ALL database changes are rolled back. The sale either fully succeeds
    or fully fails — no partial states.

    Steps:
    1. Deduct stock for every item sold
    2. Deposit the sale total into the selected account
    3. Log the account transaction
    """
    for item in sale.items.all():
        deduct_stock(
            product=item.product,
            quantity=item.quantity,
            reference=sale.reference,
            note=f'Sale {sale.reference}',
            user=user,
        )

    if sale.account and sale.payment_method != 'credit':
        sale.account.deposit(sale.total)
        AccountTransaction.objects.create(
            account=sale.account,
            tx_type=AccountTransaction.TxType.SALE,
            direction='in',
            amount=sale.total,
            description=f'Sale {sale.reference}',
            reference=sale.reference,
            date=sale.date,
        )
```

### `core/utils.py`

```python
def export_csv(filename, headers, rows):
    """
    Generic CSV export. Returns an HTTP response that downloads a file.

    filename  — base name e.g. 'sales' → becomes 'sales-20260201.csv'
    headers   — list of column names for the first row
    rows      — list of lists, one per data row

    HttpResponse with content_type='text/csv' tells the browser
    this is a downloadable file, not a web page.

    Content-Disposition: attachment tells the browser to download it
    rather than display it.
    """
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = f'attachment; filename="{filename}-{datetime.now().strftime("%Y%m%d")}.csv"'
    writer = csv.writer(response)
    writer.writerow(headers)
    for row in rows:
        writer.writerow(row)
    return response
```

---

## 13. Context Processors

### What is a context processor?

Every Django template has a "context" — a dictionary of variables available to use. By default it includes things like `request`, `user`, `messages`. A context processor is a function that adds MORE variables to every template automatically.

### `core/context_processors.py`

```python
from business.models import Business

def business_info(request):
    """
    Runs on EVERY request and adds 'business' to every template's context.

    This means every template can use:
    {{ business.name }}
    {{ business.currency }}
    {{ business.logo.url }}
    ...without the view needing to pass it explicitly.
    """
    return {'business': Business.get()}
```

### Registering it in `settings.py`

```python
TEMPLATES = [{
    'OPTIONS': {
        'context_processors': [
            'django.template.context_processors.debug',
            'django.template.context_processors.request',
            'django.contrib.auth.context_processors.auth',
            'django.contrib.messages.context_processors.messages',
            'core.context_processors.business_info',  ← our custom one
        ],
    },
}]
```

---

## 14. Template Tags

### What are template tags?

Django templates cannot run arbitrary Python. Template tags and filters extend what you can do in templates without putting logic there.

### `core/templatetags/form_tags.py`

```python
from django import template
register = template.Library()

@register.filter
def daisyui(field):
    """
    A template filter that adds DaisyUI CSS classes to form fields.

    Without this, Django renders form fields with no styling.
    With this filter, fields get the right DaisyUI class automatically:
      - input → 'input input-bordered w-full'
      - select → 'select select-bordered w-full'
      - textarea → 'textarea textarea-bordered w-full'
      - checkbox → 'checkbox'

    Usage in template:
      {% load form_tags %}
      {{ field|daisyui }}
    """
    widget = field.field.widget
    if isinstance(widget, forms.Select):
        field.field.widget.attrs['class'] = 'select select-bordered w-full'
    elif isinstance(widget, forms.Textarea):
        field.field.widget.attrs['class'] = 'textarea textarea-bordered w-full'
    elif isinstance(widget, forms.CheckboxInput):
        field.field.widget.attrs['class'] = 'checkbox'
    else:
        field.field.widget.attrs['class'] = 'input input-bordered w-full'
    return field
```

The file must live in `core/templatetags/` and that folder must have an empty `__init__.py` file. This is how Django finds custom template tag libraries.

---

## 15. Financial Logic Explained

### How profit is calculated

```
Revenue       = Sum of (quantity × selling price) for all sale items
              - discounts applied

COGS          = Sum of (quantity × cost price at time of sale)
  (Cost of    NOTE: We store cost price on SaleItem at time of sale.
  Goods Sold)  If you later change the product's cost price, old sales
               are not affected. This gives accurate historical data.

Gross Profit  = Revenue - COGS
              (profit before business expenses like rent, salaries)

Expenses      = Sum of all expense records in the period

Other Income  = Non-sale income (commissions, rental income etc.)

Net Profit    = Gross Profit + Other Income - Expenses
              (the actual bottom line)
```

### How accounts work

Accounts are money containers. Every financial event moves money:

```
Sale completed      → money IN  to account (deposit)
Expense recorded    → money OUT of account (withdraw)
Invoice paid        → money IN  to account (deposit)
Owner draw          → money OUT of account (withdraw)
Stock purchased     → money OUT of account (withdraw, optional)
```

Every movement is logged in `AccountTransaction` for audit purposes.

### Why we don't allow negative balances

`Account.withdraw()` raises a `ValueError` if the account doesn't have enough money. This is enforced at:
1. The model level (withdraw method)
2. The form level (ExpenseForm.clean() checks balance before saving)

This prevents the business from recording expenses it hasn't paid yet.

---

## 16. How Each Feature Works End-to-End

### Recording a Sale

```
1. Cashier visits /sales/create/
2. Fills in: customer name, date, payment method, account, items
3. Submits form (POST request)
4. sale_create view runs:
   a. Validates SaleForm and SaleItemFormSet
   b. Generates reference: SALE-0001
   c. Sets created_by = logged in user
   d. Saves Sale to database
   e. For each item:
      - Sets unit_cost = product.cost_price (snapshot at time of sale)
      - Saves SaleItem
   f. Calls process_sale(sale):
      - Calls deduct_stock() for each item
        → Reduces product.stock_quantity
        → Creates StockMovement record
      - Calls account.deposit(sale.total)
        → Increases account.balance
        → Creates AccountTransaction record
5. Redirects to sale detail page
6. Dashboard now shows updated revenue and stock
```

### Recording an Expense

```
1. Manager visits /finance/expenses/create/
2. Fills in: category, account, amount, description, date
3. Form validates:
   - Checks account.balance >= amount (prevents negative balance)
4. expense_create view:
   a. Saves Expense record
   b. Calls account.withdraw(expense.amount)
      → Reduces account.balance
   c. Creates AccountTransaction (direction='out')
5. Redirects to expense list
```

### Receiving a Purchase Order

```
1. Storekeeper creates PO: picks supplier, adds products + quantities + costs
2. PO saved with status='pending'
3. Stock arrives → storekeeper opens PO → clicks "Mark Received"
4. po_receive view:
   a. For each PO item:
      - Calls add_stock(product, quantity, cost_price)
        → Increases product.stock_quantity
        → Updates product.cost_price (new cost from supplier)
        → Creates StockMovement (stock_in)
   b. If account selected:
      - Calls account.withdraw(po.total)
      - Creates AccountTransaction (direction='out')
   c. Sets po.status = 'received'
   d. Sets po.received_date = today
```

### Invoice Payment Flow

```
1. Invoice created → status = 'unpaid'
2. Customer makes partial payment:
   - Manager opens invoice → fills payment form
   - invoice_payment view:
     a. Saves InvoicePayment record
     b. Calls account.deposit(payment.amount)
     c. Creates AccountTransaction
     d. Calls invoice.update_status():
        - total_paid < total → status = 'partial'
3. Customer pays remainder:
   - Same flow
   - update_status():
     - total_paid >= total → status = 'paid'
```

---

## 17. Imports Reference

Understanding which modules to import from where:

### Django core imports
```python
from django.shortcuts import render, redirect, get_object_or_404
# render       → renders a template with context, returns HttpResponse
# redirect     → redirects to another URL
# get_object_or_404 → gets a model instance or shows 404 if not found

from django.contrib import messages
# messages.success(request, 'text') → flash message shown on next page
# messages.error(request, 'text')
# messages.warning(request, 'text')

from django.contrib.auth import authenticate, login, logout
# authenticate → checks username/password, returns user or None
# login        → creates session, logs user in
# logout       → destroys session, logs user out

from django.core.exceptions import PermissionDenied
# Raising this shows the 403 page

from django.db import models, transaction
# models.Q     → complex queries: Q(name='x') | Q(name='y')
# models.F     → reference a field in a query: filter(qty__lte=F('alert'))
# transaction.atomic → wrap database operations so they all succeed or all fail

from django.db.models import Sum, Count, Q, F
# Sum('amount') → SQL SUM aggregation
# Count('id')   → SQL COUNT

from django.core.paginator import Paginator
# Paginator(queryset, 20) → split queryset into pages of 20

from django.utils import timezone
# timezone.now() → current datetime (timezone-aware)
```

### Local app imports
```python
# Always import from the app's models/forms/utils
from inventory.models import Product, Category
from finance.models import Account, AccountTransaction
from .models import Sale, SaleItem   # . means current app
from .forms import SaleForm
from .utils import process_sale
from core.decorators import role_required
from core.utils import export_csv
```

---

## 18. Static Files & CSS

### How TailwindCSS + DaisyUI work

TailwindCSS scans your template files looking for class names. It then generates a CSS file containing ONLY the classes you actually used. This keeps the file small.

DaisyUI adds pre-built component classes (`btn`, `card`, `table`, `badge` etc.) on top of Tailwind.

### Build process

```
static/src/main.css          ← source (3 lines)
    ↓ TailwindCSS CLI scans templates/**/*.html
    ↓ generates CSS for all classes found
static/css/main.css          ← output (served to browser)
```

### Two terminals during development

```bash
# Terminal 1 — Django server
python manage.py runserver

# Terminal 2 — CSS watcher (auto-rebuilds when you change templates)
npm run dev
# = ./node_modules/.bin/tailwindcss -i static/src/main.css -o static/css/main.css --watch
```

### `tailwind.config.js`

```js
module.exports = {
  content: [
    './templates/**/*.html',   // scan all HTML templates
    './static/**/*.js',        // scan JS files too
  ],
  plugins: [require('daisyui')],
  daisyui: {
    themes: ['light'],   // only include the light theme
  },
}
```

### Loading static files in templates

```html
{% load static %}   ← must appear before using {% static %}
<link rel="stylesheet" href="{% static 'css/main.css' %}"/>
<script src="{% static 'js/htmx.min.js' %}" defer></script>
```

`{% static 'css/main.css' %}` generates the correct URL: `/static/css/main.css`

---

## 19. Admin Panel

Django provides a built-in admin interface at `/admin`. Every model registered in `admin.py` appears there.

### Why use admin?

- Create the first business profile
- Create opening stock and accounts
- Fix data issues directly
- View all records with filtering and search

### Registration pattern

```python
# inventory/admin.py
from django.contrib import admin
from .models import Product, Category, StockMovement

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ('name', 'stock_quantity', 'selling_price')
    # columns shown in the list view

    list_filter = ('category', 'is_active')
    # filter sidebar on the right

    search_fields = ('name', 'sku')
    # search box at the top
```

### TabularInline — show related records

```python
class SaleItemInline(admin.TabularInline):
    model = SaleItem
    extra = 0    # don't show empty rows

@admin.register(Sale)
class SaleAdmin(admin.ModelAdmin):
    inlines = [SaleItemInline]
    # Shows SaleItem rows inside the Sale admin page
```

---

## 20. Error Handling

### 403 — Permission Denied

Shown when `PermissionDenied` is raised (wrong role). Registered in `config/urls.py`:

```python
handler403 = 'core.views.error_403'
```

```python
# core/views.py
def error_403(request, exception=None):
    return render(request, '403.html', status=403)
```

### 404 — Not Found

Shown when `get_object_or_404()` fails or URL doesn't match. `get_object_or_404(Model, pk=pk)` is safer than `Model.objects.get(pk=pk)` — the latter raises an unhandled exception if the record doesn't exist.

### Important: Error pages only show with `DEBUG=False`

During development (`DEBUG=True`), Django shows its own detailed error page with a full traceback. This is useful for debugging. Custom error pages only appear in production (`DEBUG=False`).

### `transaction.atomic` — preventing partial saves

```python
@transaction.atomic
def process_sale(sale, user=None):
    # If deduct_stock() fails for any item,
    # ALL changes (stock, account balance, transactions) are rolled back
    # The database stays clean
    for item in sale.items.all():
        deduct_stock(...)
    account.deposit(sale.total)
```

---

## 21. CSV Exports

### How it works

```python
def export_csv(filename, headers, rows):
    # HttpResponse with csv content type triggers browser download
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = f'attachment; filename="{filename}.csv"'

    writer = csv.writer(response)
    writer.writerow(headers)    # column names
    for row in rows:
        writer.writerow(row)    # data rows

    return response
```

### Smart exports respect filters

Export buttons include the current query string:

```html
<a href="{% url 'sales:export_sales' %}?{{ request.GET.urlencode }}">
  Export CSV
</a>
```

If you filtered sales by date range `?start=2026-01-01&end=2026-01-31`, the export downloads only those filtered records — not everything.

### Available exports

| URL | File | Contents |
|---|---|---|
| `/sales/export/sales/` | sales-YYYYMMDD.csv | Sales summary |
| `/sales/export/sale-items/` | sale-items-YYYYMMDD.csv | Detailed line items |
| `/sales/export/invoices/` | invoices-YYYYMMDD.csv | Invoice status |
| `/finance/export/expenses/` | expenses-YYYYMMDD.csv | All expenses |
| `/finance/export/profit-loss/` | profit-loss-YYYYMMDD.csv | P&L report |
| `/inventory/export/products/` | products-YYYYMMDD.csv | Stock list |

---

## 22. Print & Receipt System

### No PDF library needed

WeasyPrint requires complex system libraries. Instead, we use browser printing:
- Open a clean HTML page (no sidebar, no navigation)
- Show a "Print" button
- `window.print()` opens the browser's print dialog
- `@media print { .no-print { display: none; } }` hides the button when printing
- User can print to paper OR save as PDF using "Save as PDF" in the print dialog

### Receipt template — 80mm thermal style

Designed to fit a standard 80mm thermal receipt printer width. Uses monospace font (Courier New) for alignment. The `@media print` section removes the print button and margins.

### Invoice template — A4 professional

Designed for A4 paper. Uses flexbox for the header layout (business info left, invoice info right). Purple accent color `#4f46e5` for the header row and totals.

### URLs

```
/sales/<pk>/receipt/          → sale receipt
/sales/invoices/<pk>/pdf/     → invoice
```

Both open in a new tab (`target="_blank"`) from the detail pages.

---

## 23. Common Issues & Fixes

### `TemplateDoesNotExist`
The template file doesn't exist. Check the path exactly — Django is case-sensitive. Template must be in `templates/appname/filename.html`.

### `NameError: name 'X' is not defined`
Missing import at the top of the file. Check which module `X` comes from and add the import.

### `AttributeError: module 'X.views' has no attribute 'Y'`
The view function `Y` doesn't exist in `views.py`. Either the function is missing or it's in the wrong file. Check the URL file is pointing to the right app's views.

### `ImproperlyConfigured: STATIC_ROOT`
`STATIC_ROOT` is not set in `settings.py`. Add:
```python
STATIC_ROOT = BASE_DIR / 'staticfiles'
```

### CSS not loading
1. Check `npm run dev` is running
2. Check `static/css/main.css` exists and is large (10,000+ lines)
3. Check `{% load static %}` is in the template head
4. Hard refresh: `Cmd + Shift + R`

### `ALLOWED_HOSTS` error
Set `DEBUG=True` in `.env` for development. `ALLOWED_HOSTS` enforcement is strict when `DEBUG=False`.

### Migrations out of sync
```bash
python manage.py makemigrations   # create new migration files
python manage.py migrate          # apply to database
```

Run both after any model change.

### Database connection error
Check PostgreSQL is running and `.env` has correct credentials:
```bash
psql -U youruser -d biztrack_db   # test connection
```

---

## Quick Reference: Adding a New Feature

1. **Add model** to `app/models.py`
2. **Run** `python manage.py makemigrations && python manage.py migrate`
3. **Add form** to `app/forms.py`
4. **Add views** to `app/views.py` with `@role_required()`
5. **Add URLs** to `app/urls.py`
6. **Create templates** in `templates/app/`
7. **Add `{% load form_tags %}`** to any template with form fields
8. **Add sidebar link** in `templates/partials/sidebar.html`
9. **Register in admin** in `app/admin.py`
10. **Rebuild CSS** — `npm run dev` picks up new class names

---

*BizTrack — Built step by step with Django 4.2, TailwindCSS v3, DaisyUI, HTMX and PostgreSQL.*
