diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51156b3..7b27e13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: env: DJANGO_SETTINGS_MODULE: the_inventory.settings.dev + PYTHONPATH: ${{ github.workspace }}/src:${{ github.workspace }} steps: - name: Check out repository @@ -36,14 +37,17 @@ jobs: pip install -r requirements-dev.txt - name: Run Django checks + working-directory: src run: python manage.py check - name: Run tests - run: python manage.py test + working-directory: src + run: python manage.py test tests - name: Check for missing migrations + working-directory: src run: python manage.py makemigrations --check --dry-run - name: Run Ruff - run: ruff check . + run: ruff check src tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e5b07b..8329490 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,19 +42,25 @@ Thank you for your interest in contributing! This guide will help you get starte pip install -r requirements-dev.txt # Ruff and other dev tools ``` -4. **Run migrations:** +4. **Run Django commands from `src/`** (where `manage.py` lives). The repo root holds `seeders/` and `tests/`; loading `the_inventory` adjusts `sys.path` so `INSTALLED_APPS` and the test suite resolve. From the repository root: + + ```bash + cd src + ``` + +5. **Run migrations:** ```bash python manage.py migrate ``` -5. **Create a superuser** (for accessing the Wagtail admin): +6. **Create a superuser** (for accessing the Wagtail admin): ```bash python manage.py createsuperuser ``` -6. **Start the development server:** +7. **Start the development server:** ```bash python manage.py runserver @@ -69,7 +75,7 @@ The project supports flexible environment configuration for different setups: | Context | Configuration | Details | |---------|---------------|---------| | **Local Development** | Default (dev settings) | No `.env` needed; SQLite, DEBUG=True, localhost CORS | -| **Local with `.env`** | Copy `.env.example` to `.env.local` | Override defaults for testing production-like setups | +| **Local with `.env`** | `.env` at **repository root** (optional) | Backend loads it via settings; copy from `.env.example` as needed | | **Docker/Containers** | Platform environment variables | Set via Render, Docker Compose, K8s, etc. | | **Frontend (Next.js)** | `.env.local` in `frontend/` | Must have `NEXT_PUBLIC_API_URL` pointing to backend | @@ -83,17 +89,20 @@ For complete setup instructions, environment variables reference, and troublesho ## Project Layout ``` -the_inventory/ # Project config (settings, URLs, WSGI) -├── settings/ -│ ├── base.py # Shared settings (all environments) -│ ├── dev.py # Development (DEBUG=True, SQLite) -│ └── production.py # Production (ManifestStaticFilesStorage) -home/ # Home page app -search/ # Site-wide search +src/ +├── manage.py +├── the_inventory/ # Project config (settings, URLs, WSGI) +│ └── settings/ # base, dev, production +├── api/, home/, inventory/, procurement/, reports/, sales/, search/, tenants/ +└── locale/ + +seeders/ # Django app + seeding commands (repo root) +tests/ # Django / pytest-style tests (repo root) +frontend/ # Next.js app docs/ # Documentation (Roadmap, Architecture) ``` -New feature apps (e.g. `inventory/`, `procurement/`) are created at the project root alongside `home/` and `search/`. +New Django apps live under **`src/`** next to the other apps. The **`seeders`** package stays at the repository root by design. ## Branching Strategy @@ -116,24 +125,36 @@ New feature apps (e.g. `inventory/`, `procurement/`) are created at the project ## Running Tests & Checks +From **`src/`** (after `cd src`): + ```bash -python manage.py test +python manage.py test tests +python manage.py check +python manage.py makemigrations --check --dry-run ``` -Before submitting a PR, also verify: +The test suite lives in the top-level **`tests`** package (repo root), not inside an installed app, so pass the **`tests`** label (or a dotted path such as `tests.api`) so Django discovers `TestCase` modules. + +**Ruff** is run from the **repository root** so paths match CI: ```bash -# Django system checks -python manage.py check +cd .. # if you are still inside src/ +ruff check src tests seeders +``` -# Ensure no missing migrations -python manage.py makemigrations --check --dry-run +CI sets `PYTHONPATH=/src:` and uses `working-directory: src` for Django steps. Locally, `cd src && python manage.py …` is enough because importing `the_inventory` adds the repo root and `src` to `sys.path`. + +When adding new features, **include tests** under the top-level **`tests/`** package (mirroring the app or domain you are changing). -# Ruff linting (matches CI) -ruff check . +### Docker + +From the repository root, build the image to confirm the Dockerfile and entrypoint (migrate + Gunicorn from `src/`) still work: + +```bash +docker build -t the_inventory . ``` -When adding new features, **include tests**. Place them in `/tests.py` or `/tests/` for larger test suites. +See [README — Docker](README.md#docker) for `docker run` examples and environment variables. ## Submitting a Pull Request @@ -151,9 +172,10 @@ When adding new features, **include tests**. Place them in `/tests.py` or ` ### PR Checklist -- [ ] Tests pass (`python manage.py test`) -- [ ] No missing migrations (`python manage.py makemigrations --check --dry-run`) -- [ ] `python manage.py check` reports no issues +- [ ] Tests pass (`cd src && python manage.py test tests`) +- [ ] No missing migrations (`cd src && python manage.py makemigrations --check --dry-run`) +- [ ] `cd src && python manage.py check` reports no issues +- [ ] Ruff clean from repo root (`ruff check src tests seeders`) - [ ] New features include tests - [ ] New models include migrations - [ ] Documentation updated if needed diff --git a/Dockerfile b/Dockerfile index b3dd995..1872656 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,8 @@ EXPOSE 8000 10000 # 2. Set PORT variable that is used by Gunicorn. This should match "EXPOSE" # command. ENV PYTHONUNBUFFERED=1 \ - PORT=8000 + PORT=8000 \ + PYTHONPATH=/app/src:/app # Install system packages required by Wagtail and Django. RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-recommends \ @@ -26,9 +27,9 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r # Install the project requirements (includes gunicorn). COPY requirements.txt / -RUN pip install -r /requirements.txt +RUN pip install --root-user-action=ignore -r /requirements.txt -# Use /app folder as a directory where the source code is stored. +# Repo root at /app (seeders, tests, frontend); Django project in /app/src. WORKDIR /app # Set this directory to be owned by the "wagtail" user. This Wagtail project @@ -46,6 +47,8 @@ USER wagtail # Collect static files using the same STORAGES["staticfiles"] as production # (ManifestStaticFilesStorage). Default manage.py uses dev settings, which # skips the manifest; runtime then raises "Missing staticfiles manifest entry". +WORKDIR /app/src + RUN DJANGO_SETTINGS_MODULE=the_inventory.settings.production \ SECRET_KEY=collectstatic-build-only-not-used-at-runtime \ python manage.py collectstatic --noinput --clear diff --git a/Procfile b/Procfile index 5039a69..5f62460 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: gunicorn the_inventory.wsgi:application +web: cd src && gunicorn the_inventory.wsgi:application diff --git a/README.md b/README.md index 5fbec5a..281d6eb 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt ``` +Django’s `manage.py` and the project package live under **`src/`**. The repository root also holds `seeders/` and `tests/`; run Django from `src` so imports resolve correctly (this matches CI and Docker). + +```bash +cd src +``` + ### 4. Run database migrations ```bash @@ -87,6 +93,8 @@ Visit [http://localhost:8000](http://localhost:8000) for the site, or [http://lo ## Docker +The image copies the full repository to `/app`, sets `PYTHONPATH=/app/src:/app`, and the entrypoint runs migrations and Gunicorn from **`/app/src`** (same layout as local `cd src`). + Build and run using Docker: ```bash @@ -105,21 +113,22 @@ For complete environment variable documentation and deployment guides (Docker, R ## Project Structure ``` -the_inventory/ # Project configuration (settings, URLs, WSGI) -├── settings/ -│ ├── base.py # Shared settings -│ ├── dev.py # Development overrides (DEBUG=True) -│ └── production.py # Production overrides -├── templates/ # Project-level templates (base, 404, 500) -└── static/ # Project-level static files - -home/ # Landing / home page app -search/ # Site-wide search app -inventory/ # [Phase 1] Core inventory (products, stock, movements) +src/ +├── manage.py # Django entry point (run commands from this directory) +├── the_inventory/ # Project configuration (settings, URLs, WSGI) +│ ├── settings/ +│ ├── templates/ +│ └── static/ +├── api/, home/, inventory/, procurement/, reports/, sales/, search/, tenants/ # Django apps +└── locale/ # Backend translation catalogs + +seeders/ # Seeding app (management commands) — repo root +tests/ # Test suite — repo root +frontend/ # Next.js tenant UI docs/ # Project documentation (Architecture, Roadmap) ``` -See [Architecture](docs/ARCHITECTURE.md) for the full technical design, including the Phase 1 schema and future apps (`procurement/`, `sales/`, `reports/`, `api/`). +See [Architecture](docs/ARCHITECTURE.md) for the full technical design, including the Phase 1 schema and app boundaries. ## Project Docs & Meta @@ -138,14 +147,16 @@ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). -Our CI workflow runs on pushes and pull requests to `main`, and will: +Our CI workflow runs on pushes and pull requests to `main` and `develop`, and will: -- Run `python manage.py check` -- Run `python manage.py test` -- Run `python manage.py makemigrations --check --dry-run` -- Run `ruff check .` +- Set `PYTHONPATH` to `/src:` (so `seeders` and `tests` import correctly) +- From **`src/`**: + - Run `python manage.py check` + - Run `python manage.py test tests` + - Run `python manage.py makemigrations --check --dry-run` +- From the repo root: `ruff check src tests seeders` -You can run the same commands locally using the instructions in `CONTRIBUTING.md` (including installing dev dependencies from `requirements-dev.txt`). +You can mirror that locally with `cd src && python manage.py …` and `ruff check src tests seeders` from the repository root; see [Contributing](CONTRIBUTING.md) (install dev tools from `requirements-dev.txt`). ## License diff --git a/entrypoint.sh b/entrypoint.sh index 1ff1eba..dcffa5e 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/sh set -xe +# Django lives in src/ (repo root is parent of this script in Docker: /app). +APP_ROOT=$(cd "$(dirname "$0")" && pwd) +cd "$APP_ROOT/src" || exit 1 + # Ensure environment variables are set with fallbacks export DJANGO_SETTINGS_MODULE="${DJANGO_SETTINGS_MODULE:-the_inventory.settings.production}" export DATABASE_URL="${DATABASE_URL}" diff --git a/frontend/public/locales/ar.json b/frontend/public/locales/ar.json index 307c842..92d864b 100644 --- a/frontend/public/locales/ar.json +++ b/frontend/public/locales/ar.json @@ -1936,7 +1936,10 @@ "pickDate": "اختر تاريخًا", "all": "الكل", "selectMethod": "اختر الطريقة", - "selectPeriod": "اختر الفترة" + "selectPeriod": "اختر الفترة", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "لا توجد بيانات لهذا التقرير." }, "filters": { "valuationMethod": "طريقة التقييم", diff --git a/frontend/public/locales/en.json b/frontend/public/locales/en.json index a69ee89..0cee916 100644 --- a/frontend/public/locales/en.json +++ b/frontend/public/locales/en.json @@ -1936,7 +1936,10 @@ "pickDate": "Pick a date", "all": "All", "selectMethod": "Select method", - "selectPeriod": "Select period" + "selectPeriod": "Select period", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "No data for this report." }, "filters": { "valuationMethod": "Valuation Method", diff --git a/frontend/public/locales/es.json b/frontend/public/locales/es.json index 7dcff3b..4b776d8 100644 --- a/frontend/public/locales/es.json +++ b/frontend/public/locales/es.json @@ -1936,7 +1936,10 @@ "pickDate": "Elegir fecha", "all": "Todos", "selectMethod": "Seleccionar método", - "selectPeriod": "Seleccionar período" + "selectPeriod": "Seleccionar período", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "No hay datos para este informe." }, "filters": { "valuationMethod": "Método de valoración", diff --git a/frontend/public/locales/fr.json b/frontend/public/locales/fr.json index 690876b..fc55a87 100644 --- a/frontend/public/locales/fr.json +++ b/frontend/public/locales/fr.json @@ -1936,7 +1936,10 @@ "pickDate": "Choisir une date", "all": "Tous", "selectMethod": "Choisir la méthode", - "selectPeriod": "Choisir la période" + "selectPeriod": "Choisir la période", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "Aucune donnée pour ce rapport." }, "filters": { "valuationMethod": "Méthode de valorisation", diff --git a/frontend/public/locales/rw.json b/frontend/public/locales/rw.json index d73112a..1962733 100644 --- a/frontend/public/locales/rw.json +++ b/frontend/public/locales/rw.json @@ -1927,220 +1927,223 @@ }, "Reports": { "index": { - "title": "Reports", - "description": "Generate and export inventory, procurement, and sales reports" + "title": "Raporo", + "description": "Kora no gusohora raporo z'ububiko, z'ibicuruzwa n'ubucuruzi" }, "shared": { - "dateFrom": "From", - "dateTo": "To", - "pickDate": "Pick a date", - "all": "All", - "selectMethod": "Select method", - "selectPeriod": "Select period" + "dateFrom": "Kuva", + "dateTo": "Kugeza", + "pickDate": "Hitamo itariki", + "all": "Byose", + "selectMethod": "Hitamo uburyo", + "selectPeriod": "Hitamo igihe", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "Nta makuru ahari kuri iyi raporo." }, "filters": { - "valuationMethod": "Valuation Method", - "period": "Period", - "movementType": "Movement type", - "varianceType": "Variance type", - "lookAheadDays": "Look-ahead (days)", - "thresholdMultiplier": "Threshold multiplier", - "productSku": "Product SKU", - "lotNumber": "Lot number" + "valuationMethod": "Uburyo bwo gushyira agaciro", + "period": "Igihe", + "movementType": "Ubwoko bw'impinduka", + "varianceType": "Ubwoko bw'itandukaniro", + "lookAheadDays": "Iminsi yo gutegereza imbere", + "thresholdMultiplier": "Ingano y'inzura y'ingenzi", + "productSku": "SKU y'igicuruzwa", + "lotNumber": "Nimero ya loti" }, "options": { "period": { - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly" + "daily": "Buri munsi", + "weekly": "Buri cyumweru", + "monthly": "Buri kwezi" }, "valuation": { - "weighted_average": "Weighted Average", - "latest_cost": "Latest Cost" + "weighted_average": "Intera y'ingenzi", + "latest_cost": "Igiciro cy'ubu" }, "movement": { - "receipt": "Receipt", - "issue": "Issue", - "transfer": "Transfer", - "adjustment": "Adjustment", - "return": "Return" + "receipt": "Kwakira", + "issue": "Gutanga", + "transfer": "Kohereza", + "adjustment": "Guhindura", + "return": "Gusubiza" }, "variance": { - "shortage": "Shortage", - "surplus": "Surplus", - "match": "Match" + "shortage": "Ubusa", + "surplus": "Urwunguko rw'inyongera", + "match": "Biringaniye" } }, "expiryBadge": { - "expired": "Expired", - "expiring": "Expiring" + "expired": "Byarangiye", + "expiring": "Biri kugenda birangira" }, "columns": { "cellEmpty": "—", "sku": "SKU", - "product": "Product", - "category": "Category", - "quantity": "Quantity", - "unitCost": "Unit Cost", - "totalValue": "Total Value", - "type": "Type", - "qty": "Qty", - "from": "From", - "to": "To", - "reference": "Reference", - "date": "Date", - "reorderPoint": "Reorder Point", - "currentStock": "Current Stock", - "deficit": "Deficit", - "threshold": "Threshold", - "excess": "Excess", - "period": "Period", - "orders": "Orders", - "totalCost": "Total Cost", - "totalRevenue": "Total Revenue", - "totalQty": "Total Qty", - "reserved": "Reserved", - "available": "Available", - "reservedValue": "Reserved Value", - "status": "Status", - "lotNumber": "Lot Number", - "expiryDate": "Expiry Date", - "daysLeft": "Days Left", - "qtyRemaining": "Qty Remaining", - "supplier": "Supplier", - "cycle": "Cycle", - "location": "Location", - "systemQty": "System Qty", - "physicalQty": "Physical Qty", - "variance": "Variance", - "resolution": "Resolution", - "action": "Action", - "fromToLocation": "From / Location", - "salesOrder": "Sales Order", - "name": "Name", - "scheduled": "Scheduled", - "lines": "Lines", - "variances": "Variances", - "shortages": "Shortages", - "surpluses": "Surpluses", - "netVariance": "Net Variance", - "allLocations": "All" + "product": "Igicuruzwa", + "category": "Icyiciro", + "quantity": "Umubare", + "unitCost": "Igiciro ku kimwe", + "totalValue": "Agaciro gasanzwe", + "type": "Ubwoko", + "qty": "Umubare", + "from": "Kuva", + "to": "Kugeza", + "reference": "Icyitonderwa", + "date": "Itariki", + "reorderPoint": "Inkengero yo kongeramo", + "currentStock": "Ububiko buri ubu", + "deficit": "Ubusa", + "threshold": "Inkengero", + "excess": "Inyongera", + "period": "Igihe", + "orders": "Amabwiriza", + "totalCost": "Igiciro cyose", + "totalRevenue": "Inyungu zose", + "totalQty": "Umubare wose", + "reserved": "Byasigiwe", + "available": "Bihari", + "reservedValue": "Agaciro kasigiwe", + "status": "Imiterere", + "lotNumber": "Nimero ya loti", + "expiryDate": "Itariki isoza", + "daysLeft": "Iminsi isigaranye", + "qtyRemaining": "Umubare usigaranye", + "supplier": "Utanga", + "cycle": "Uruziga", + "location": "Ahantu", + "systemQty": "Umubare mu sisitemu", + "physicalQty": "Umubare w'ukuri", + "variance": "Itandukaniro", + "resolution": "Igisubizo", + "action": "Igikorwa", + "fromToLocation": "Kuva / Ahantu", + "salesOrder": "Tegeko ry'ubucuruzi", + "name": "Izina", + "scheduled": "Byateguriwe", + "lines": "Imirongo", + "variances": "Itandukaniro", + "shortages": "Ubusa", + "surpluses": "Urwunguko", + "netVariance": "Itandukaniro rishya", + "allLocations": "Ahantu hose" }, "cards": { "stockValuation": { - "title": "Stock Valuation", - "description": "View total inventory value using weighted average or latest cost methods" + "title": "Agaciro k'ububiko", + "description": "Reba agaciro kose k'ububiko ukoresheje intera y'ingenzi cyangwa igiciro cy'ubu" }, "movementHistory": { - "title": "Movement History", - "description": "Browse all stock movements with date range and type filters" + "title": "Amateka y'impinduka", + "description": "Shakisha impinduka zose z'ububiko ukoresheje itariki n'ubwoko" }, "lowStock": { - "title": "Low Stock", - "description": "Products at or below their reorder point" + "title": "Ububiko buke", + "description": "Ibicuruzwa biri ku inkengero cyangwa biri munsi yayo yo kongeramo" }, "overstock": { - "title": "Overstock", - "description": "Products exceeding their reorder point threshold" + "title": "Ubwinshi bw'ububiko", + "description": "Ibicuruzwa byarenze inkengero yo kongeramo" }, "purchaseSummary": { - "title": "Purchase Summary", - "description": "Purchase order totals grouped by period" + "title": "Incamake y'ibicuruzwa byaguze", + "description": "Igiteranyo cy'amabwiriza yo kugura hakurikijwe igihe" }, "salesSummary": { - "title": "Sales Summary", - "description": "Sales order totals grouped by period" + "title": "Incamake y'ubucuruzi", + "description": "Igiteranyo cy'amabwiriza y'ubucuruzi hakurikijwe igihe" }, "availability": { - "title": "Availability", - "description": "Per-product stock, reserved, and available quantities" + "title": "Kuboneka", + "description": "Umubare w'ububiko, wasigiwe n'uboneka ku bicuruzwa" }, "productExpiry": { - "title": "Product Expiry", - "description": "Lots expiring soon or already expired" + "title": "Iherezo ry'igihe cy'ibicuruzwa", + "description": "Loti zizarangira vuba cyangwa zarangiye" }, "variances": { - "title": "Variances", - "description": "Inventory variance report from cycle counts" + "title": "Itandukaniro", + "description": "Raporo y'itandukaniro ry'ububiko ivuye mu kubara" }, "traceability": { - "title": "Traceability", - "description": "Full movement chain for a specific product and lot" + "title": "Guhuriza uruzi", + "description": "Urutonde rwuzuye rw'impinduka ku gicuruzwa na loti" } }, "pages": { "stockValuation": { - "title": "Stock Valuation", - "description": "Inventory value by product using selected costing method", - "summaryTotalProducts": "Total Products", - "summaryTotalQuantity": "Total Quantity", - "summaryTotalValue": "Total Value", - "empty": "No stock valuation data." + "title": "Agaciro k'ububiko", + "description": "Agaciro k'ububiko ku bicuruzwa ukoresheje uburyo bwahisemo", + "summaryTotalProducts": "Igiteranyo cy'ibicuruzwa", + "summaryTotalQuantity": "Umubare wose", + "summaryTotalValue": "Agaciro kose", + "empty": "Nta makuru y'agaciro k'ububiko." }, "movementHistory": { - "title": "Movement History", - "description": "All stock movements with filters", - "empty": "No movements found for the selected filters." + "title": "Amateka y'impinduka", + "description": "Impinduka zose z'ububiko n'imiyoboro", + "empty": "Nta mpinduka zabonetse ku miyoboro yahisemo." }, "lowStock": { - "title": "Low Stock Report", - "description": "Products at or below their reorder point", - "empty": "No low-stock items found." + "title": "Raporo y'ububiko buke", + "description": "Ibicuruzwa biri ku inkengero cyangwa biri munsi yayo yo kongeramo", + "empty": "Nta bicuruzwa bihari by'ububiko buke." }, "overstock": { - "title": "Overstock Report", - "description": "Products exceeding their reorder point by the threshold multiplier", - "empty": "No overstock items found." + "title": "Raporo y'ubwinshi bw'ububiko", + "description": "Ibicuruzwa byarenze inkengero yo kongeramo ku buryo bw'ingano y'ingenzi", + "empty": "Nta bicuruzwa bihari by'ubwinshi bw'ububiko." }, "purchaseSummary": { - "title": "Purchase Summary", - "description": "Purchase order totals grouped by period", - "totalOrders": "Total Orders", - "totalCost": "Total Cost", - "empty": "No purchase summary data for the selected period." + "title": "Incamake y'ibicuruzwa byaguze", + "description": "Igiteranyo cy'amabwiriza yo kugura hakurikijwe igihe", + "totalOrders": "Igiteranyo cy'amabwiriza", + "totalCost": "Igiciro cyose", + "empty": "Nta makuru y'incamake y'ibyaguwe ku gihe cyahisemo." }, "salesSummary": { - "title": "Sales Summary", - "description": "Sales order totals grouped by period", - "totalOrders": "Total Orders", - "totalRevenue": "Total Revenue", - "empty": "No sales summary data for the selected period." + "title": "Incamake y'ubucuruzi", + "description": "Igiteranyo cy'amabwiriza y'ubucuruzi hakurikijwe igihe", + "totalOrders": "Igiteranyo cy'amabwiriza", + "totalRevenue": "Inyungu zose", + "empty": "Nta makuru y'incamake y'ubucuruzi ku gihe cyahisemo." }, "availability": { - "title": "Availability Report", - "description": "Per-product stock, reserved, and available quantities", - "products": "Products", - "totalReservedValue": "Total Reserved Value", - "empty": "No availability data." + "title": "Raporo y'uboneka", + "description": "Umubare w'ububiko, wasigiwe n'uboneka ku bicuruzwa", + "products": "Ibicuruzwa", + "totalReservedValue": "Agaciro kasigiwe kose", + "empty": "Nta makuru y'uboneka." }, "productExpiry": { - "title": "Product Expiry Report", - "description": "Lots expiring soon or already expired", - "expired": "Expired lots", - "expiringSoon": "Expiring soon", - "totalLots": "Total lots (in window)", - "empty": "No expiry data for the selected window." + "title": "Raporo y'iherezo ry'igihe", + "description": "Loti zizarangira vuba cyangwa zarangiye", + "expired": "Loti zarangiye", + "expiringSoon": "Zizarangira vuba", + "totalLots": "Igiteranyo cy'loti (muri iyi nkengero y'igihe)", + "empty": "Nta makuru y'iherezo ry'igihe ku mpera y'igihe cyahisemo." }, "variances": { - "title": "Variance Report", - "description": "Inventory variances from cycle counts", - "totalVariances": "Total variances", - "shortages": "Shortages", - "surpluses": "Surpluses", - "netVariance": "Net variance", - "empty": "No variances found for the selected filters." + "title": "Raporo y'itandukaniro", + "description": "Itandukaniro ry'ububiko rivuye mu kubara", + "totalVariances": "Igiteranyo cy'itandukaniro", + "shortages": "Ubusa", + "surpluses": "Urwunguko", + "netVariance": "Itandukaniro rishya", + "empty": "Nta tandukaniro ryabonetse ku miyoboro yahisemo." }, "traceability": { - "title": "Product Traceability", - "description": "Full movement chain for a specific product and lot", - "placeholderSku": "Enter SKU", - "placeholderLot": "Enter lot number", - "promptEnterSkuAndLot": "Enter a product SKU and lot number to load the traceability chain.", - "productLabel": "Product", - "lotLabel": "Lot", - "expiresWithDate": "Expires {date}", - "supplierWithName": "Supplier: {name}", - "empty": "No traceability events found." + "title": "Guhuriza uruzi rw'igicuruzwa", + "description": "Urutonde rwuzuye rw'impinduka ku gicuruzwa na loti", + "placeholderSku": "Injiza SKU", + "placeholderLot": "Injiza nimero ya loti", + "promptEnterSkuAndLot": "Injiza SKU y'igicuruzwa na nimero ya loti kugira ngo ubone uruzi rw'impinduka.", + "productLabel": "Igicuruzwa", + "lotLabel": "Loti", + "expiresWithDate": "Birarangira {date}", + "supplierWithName": "Utanga: {name}", + "empty": "Nta bikorwa byo guhuriza uruzi byabonetse." } } } diff --git a/frontend/public/locales/sw.json b/frontend/public/locales/sw.json index 528e9b3..62c500b 100644 --- a/frontend/public/locales/sw.json +++ b/frontend/public/locales/sw.json @@ -1936,7 +1936,10 @@ "pickDate": "Pick a date", "all": "All", "selectMethod": "Select method", - "selectPeriod": "Select period" + "selectPeriod": "Select period", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "Hakuna data kwa ajili ya ripoti hii." }, "filters": { "valuationMethod": "Valuation Method", diff --git a/frontend/scripts/reports-locale-payload.json b/frontend/scripts/reports-locale-payload.json index a3e057a..d36dbbd 100644 --- a/frontend/scripts/reports-locale-payload.json +++ b/frontend/scripts/reports-locale-payload.json @@ -10,7 +10,10 @@ "pickDate": "Pick a date", "all": "All", "selectMethod": "Select method", - "selectPeriod": "Select period" + "selectPeriod": "Select period", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "No data for this report." }, "filters": { "valuationMethod": "Valuation Method", @@ -229,7 +232,10 @@ "pickDate": "Choisir une date", "all": "Tous", "selectMethod": "Choisir la méthode", - "selectPeriod": "Choisir la période" + "selectPeriod": "Choisir la période", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "Aucune donnée pour ce rapport." }, "filters": { "valuationMethod": "Méthode de valorisation", @@ -448,7 +454,10 @@ "pickDate": "Elegir fecha", "all": "Todos", "selectMethod": "Seleccionar método", - "selectPeriod": "Seleccionar período" + "selectPeriod": "Seleccionar período", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "No hay datos para este informe." }, "filters": { "valuationMethod": "Método de valoración", @@ -667,7 +676,10 @@ "pickDate": "اختر تاريخًا", "all": "الكل", "selectMethod": "اختر الطريقة", - "selectPeriod": "اختر الفترة" + "selectPeriod": "اختر الفترة", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "لا توجد بيانات لهذا التقرير." }, "filters": { "valuationMethod": "طريقة التقييم", @@ -877,220 +889,223 @@ }, "rw": { "index": { - "title": "Reports", - "description": "Generate and export inventory, procurement, and sales reports" + "title": "Raporo", + "description": "Kora no gusohora raporo z'ububiko, z'ibicuruzwa n'ubucuruzi" }, "shared": { - "dateFrom": "From", - "dateTo": "To", - "pickDate": "Pick a date", - "all": "All", - "selectMethod": "Select method", - "selectPeriod": "Select period" + "dateFrom": "Kuva", + "dateTo": "Kugeza", + "pickDate": "Hitamo itariki", + "all": "Byose", + "selectMethod": "Hitamo uburyo", + "selectPeriod": "Hitamo igihe", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "Nta makuru ahari kuri iyi raporo." }, "filters": { - "valuationMethod": "Valuation Method", - "period": "Period", - "movementType": "Movement type", - "varianceType": "Variance type", - "lookAheadDays": "Look-ahead (days)", - "thresholdMultiplier": "Threshold multiplier", - "productSku": "Product SKU", - "lotNumber": "Lot number" + "valuationMethod": "Uburyo bwo gushyira agaciro", + "period": "Igihe", + "movementType": "Ubwoko bw'impinduka", + "varianceType": "Ubwoko bw'itandukaniro", + "lookAheadDays": "Iminsi yo gutegereza imbere", + "thresholdMultiplier": "Ingano y'inzura y'ingenzi", + "productSku": "SKU y'igicuruzwa", + "lotNumber": "Nimero ya loti" }, "options": { "period": { - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly" + "daily": "Buri munsi", + "weekly": "Buri cyumweru", + "monthly": "Buri kwezi" }, "valuation": { - "weighted_average": "Weighted Average", - "latest_cost": "Latest Cost" + "weighted_average": "Intera y'ingenzi", + "latest_cost": "Igiciro cy'ubu" }, "movement": { - "receipt": "Receipt", - "issue": "Issue", - "transfer": "Transfer", - "adjustment": "Adjustment", - "return": "Return" + "receipt": "Kwakira", + "issue": "Gutanga", + "transfer": "Kohereza", + "adjustment": "Guhindura", + "return": "Gusubiza" }, "variance": { - "shortage": "Shortage", - "surplus": "Surplus", - "match": "Match" + "shortage": "Ubusa", + "surplus": "Urwunguko rw'inyongera", + "match": "Biringaniye" } }, "expiryBadge": { - "expired": "Expired", - "expiring": "Expiring" + "expired": "Byarangiye", + "expiring": "Biri kugenda birangira" }, "columns": { "cellEmpty": "—", "sku": "SKU", - "product": "Product", - "category": "Category", - "quantity": "Quantity", - "unitCost": "Unit Cost", - "totalValue": "Total Value", - "type": "Type", - "qty": "Qty", - "from": "From", - "to": "To", - "reference": "Reference", - "date": "Date", - "reorderPoint": "Reorder Point", - "currentStock": "Current Stock", - "deficit": "Deficit", - "threshold": "Threshold", - "excess": "Excess", - "period": "Period", - "orders": "Orders", - "totalCost": "Total Cost", - "totalRevenue": "Total Revenue", - "totalQty": "Total Qty", - "reserved": "Reserved", - "available": "Available", - "reservedValue": "Reserved Value", - "status": "Status", - "lotNumber": "Lot Number", - "expiryDate": "Expiry Date", - "daysLeft": "Days Left", - "qtyRemaining": "Qty Remaining", - "supplier": "Supplier", - "cycle": "Cycle", - "location": "Location", - "systemQty": "System Qty", - "physicalQty": "Physical Qty", - "variance": "Variance", - "resolution": "Resolution", - "action": "Action", - "fromToLocation": "From / Location", - "salesOrder": "Sales Order", - "name": "Name", - "scheduled": "Scheduled", - "lines": "Lines", - "variances": "Variances", - "shortages": "Shortages", - "surpluses": "Surpluses", - "netVariance": "Net Variance", - "allLocations": "All" + "product": "Igicuruzwa", + "category": "Icyiciro", + "quantity": "Umubare", + "unitCost": "Igiciro ku kimwe", + "totalValue": "Agaciro gasanzwe", + "type": "Ubwoko", + "qty": "Umubare", + "from": "Kuva", + "to": "Kugeza", + "reference": "Icyitonderwa", + "date": "Itariki", + "reorderPoint": "Inkengero yo kongeramo", + "currentStock": "Ububiko buri ubu", + "deficit": "Ubusa", + "threshold": "Inkengero", + "excess": "Inyongera", + "period": "Igihe", + "orders": "Amabwiriza", + "totalCost": "Igiciro cyose", + "totalRevenue": "Inyungu zose", + "totalQty": "Umubare wose", + "reserved": "Byasigiwe", + "available": "Bihari", + "reservedValue": "Agaciro kasigiwe", + "status": "Imiterere", + "lotNumber": "Nimero ya loti", + "expiryDate": "Itariki isoza", + "daysLeft": "Iminsi isigaranye", + "qtyRemaining": "Umubare usigaranye", + "supplier": "Utanga", + "cycle": "Uruziga", + "location": "Ahantu", + "systemQty": "Umubare mu sisitemu", + "physicalQty": "Umubare w'ukuri", + "variance": "Itandukaniro", + "resolution": "Igisubizo", + "action": "Igikorwa", + "fromToLocation": "Kuva / Ahantu", + "salesOrder": "Tegeko ry'ubucuruzi", + "name": "Izina", + "scheduled": "Byateguriwe", + "lines": "Imirongo", + "variances": "Itandukaniro", + "shortages": "Ubusa", + "surpluses": "Urwunguko", + "netVariance": "Itandukaniro rishya", + "allLocations": "Ahantu hose" }, "cards": { "stockValuation": { - "title": "Stock Valuation", - "description": "View total inventory value using weighted average or latest cost methods" + "title": "Agaciro k'ububiko", + "description": "Reba agaciro kose k'ububiko ukoresheje intera y'ingenzi cyangwa igiciro cy'ubu" }, "movementHistory": { - "title": "Movement History", - "description": "Browse all stock movements with date range and type filters" + "title": "Amateka y'impinduka", + "description": "Shakisha impinduka zose z'ububiko ukoresheje itariki n'ubwoko" }, "lowStock": { - "title": "Low Stock", - "description": "Products at or below their reorder point" + "title": "Ububiko buke", + "description": "Ibicuruzwa biri ku inkengero cyangwa biri munsi yayo yo kongeramo" }, "overstock": { - "title": "Overstock", - "description": "Products exceeding their reorder point threshold" + "title": "Ubwinshi bw'ububiko", + "description": "Ibicuruzwa byarenze inkengero yo kongeramo" }, "purchaseSummary": { - "title": "Purchase Summary", - "description": "Purchase order totals grouped by period" + "title": "Incamake y'ibicuruzwa byaguze", + "description": "Igiteranyo cy'amabwiriza yo kugura hakurikijwe igihe" }, "salesSummary": { - "title": "Sales Summary", - "description": "Sales order totals grouped by period" + "title": "Incamake y'ubucuruzi", + "description": "Igiteranyo cy'amabwiriza y'ubucuruzi hakurikijwe igihe" }, "availability": { - "title": "Availability", - "description": "Per-product stock, reserved, and available quantities" + "title": "Kuboneka", + "description": "Umubare w'ububiko, wasigiwe n'uboneka ku bicuruzwa" }, "productExpiry": { - "title": "Product Expiry", - "description": "Lots expiring soon or already expired" + "title": "Iherezo ry'igihe cy'ibicuruzwa", + "description": "Loti zizarangira vuba cyangwa zarangiye" }, "variances": { - "title": "Variances", - "description": "Inventory variance report from cycle counts" + "title": "Itandukaniro", + "description": "Raporo y'itandukaniro ry'ububiko ivuye mu kubara" }, "traceability": { - "title": "Traceability", - "description": "Full movement chain for a specific product and lot" + "title": "Guhuriza uruzi", + "description": "Urutonde rwuzuye rw'impinduka ku gicuruzwa na loti" } }, "pages": { "stockValuation": { - "title": "Stock Valuation", - "description": "Inventory value by product using selected costing method", - "summaryTotalProducts": "Total Products", - "summaryTotalQuantity": "Total Quantity", - "summaryTotalValue": "Total Value", - "empty": "No stock valuation data." + "title": "Agaciro k'ububiko", + "description": "Agaciro k'ububiko ku bicuruzwa ukoresheje uburyo bwahisemo", + "summaryTotalProducts": "Igiteranyo cy'ibicuruzwa", + "summaryTotalQuantity": "Umubare wose", + "summaryTotalValue": "Agaciro kose", + "empty": "Nta makuru y'agaciro k'ububiko." }, "movementHistory": { - "title": "Movement History", - "description": "All stock movements with filters", - "empty": "No movements found for the selected filters." + "title": "Amateka y'impinduka", + "description": "Impinduka zose z'ububiko n'imiyoboro", + "empty": "Nta mpinduka zabonetse ku miyoboro yahisemo." }, "lowStock": { - "title": "Low Stock Report", - "description": "Products at or below their reorder point", - "empty": "No low-stock items found." + "title": "Raporo y'ububiko buke", + "description": "Ibicuruzwa biri ku inkengero cyangwa biri munsi yayo yo kongeramo", + "empty": "Nta bicuruzwa bihari by'ububiko buke." }, "overstock": { - "title": "Overstock Report", - "description": "Products exceeding their reorder point by the threshold multiplier", - "empty": "No overstock items found." + "title": "Raporo y'ubwinshi bw'ububiko", + "description": "Ibicuruzwa byarenze inkengero yo kongeramo ku buryo bw'ingano y'ingenzi", + "empty": "Nta bicuruzwa bihari by'ubwinshi bw'ububiko." }, "purchaseSummary": { - "title": "Purchase Summary", - "description": "Purchase order totals grouped by period", - "totalOrders": "Total Orders", - "totalCost": "Total Cost", - "empty": "No purchase summary data for the selected period." + "title": "Incamake y'ibicuruzwa byaguze", + "description": "Igiteranyo cy'amabwiriza yo kugura hakurikijwe igihe", + "totalOrders": "Igiteranyo cy'amabwiriza", + "totalCost": "Igiciro cyose", + "empty": "Nta makuru y'incamake y'ibyaguwe ku gihe cyahisemo." }, "salesSummary": { - "title": "Sales Summary", - "description": "Sales order totals grouped by period", - "totalOrders": "Total Orders", - "totalRevenue": "Total Revenue", - "empty": "No sales summary data for the selected period." + "title": "Incamake y'ubucuruzi", + "description": "Igiteranyo cy'amabwiriza y'ubucuruzi hakurikijwe igihe", + "totalOrders": "Igiteranyo cy'amabwiriza", + "totalRevenue": "Inyungu zose", + "empty": "Nta makuru y'incamake y'ubucuruzi ku gihe cyahisemo." }, "availability": { - "title": "Availability Report", - "description": "Per-product stock, reserved, and available quantities", - "products": "Products", - "totalReservedValue": "Total Reserved Value", - "empty": "No availability data." + "title": "Raporo y'uboneka", + "description": "Umubare w'ububiko, wasigiwe n'uboneka ku bicuruzwa", + "products": "Ibicuruzwa", + "totalReservedValue": "Agaciro kasigiwe kose", + "empty": "Nta makuru y'uboneka." }, "productExpiry": { - "title": "Product Expiry Report", - "description": "Lots expiring soon or already expired", - "expired": "Expired lots", - "expiringSoon": "Expiring soon", - "totalLots": "Total lots (in window)", - "empty": "No expiry data for the selected window." + "title": "Raporo y'iherezo ry'igihe", + "description": "Loti zizarangira vuba cyangwa zarangiye", + "expired": "Loti zarangiye", + "expiringSoon": "Zizarangira vuba", + "totalLots": "Igiteranyo cy'loti (muri iyi nkengero y'igihe)", + "empty": "Nta makuru y'iherezo ry'igihe ku mpera y'igihe cyahisemo." }, "variances": { - "title": "Variance Report", - "description": "Inventory variances from cycle counts", - "totalVariances": "Total variances", - "shortages": "Shortages", - "surpluses": "Surpluses", - "netVariance": "Net variance", - "empty": "No variances found for the selected filters." + "title": "Raporo y'itandukaniro", + "description": "Itandukaniro ry'ububiko rivuye mu kubara", + "totalVariances": "Igiteranyo cy'itandukaniro", + "shortages": "Ubusa", + "surpluses": "Urwunguko", + "netVariance": "Itandukaniro rishya", + "empty": "Nta tandukaniro ryabonetse ku miyoboro yahisemo." }, "traceability": { - "title": "Product Traceability", - "description": "Full movement chain for a specific product and lot", - "placeholderSku": "Enter SKU", - "placeholderLot": "Enter lot number", - "promptEnterSkuAndLot": "Enter a product SKU and lot number to load the traceability chain.", - "productLabel": "Product", - "lotLabel": "Lot", - "expiresWithDate": "Expires {date}", - "supplierWithName": "Supplier: {name}", - "empty": "No traceability events found." + "title": "Guhuriza uruzi rw'igicuruzwa", + "description": "Urutonde rwuzuye rw'impinduka ku gicuruzwa na loti", + "placeholderSku": "Injiza SKU", + "placeholderLot": "Injiza nimero ya loti", + "promptEnterSkuAndLot": "Injiza SKU y'igicuruzwa na nimero ya loti kugira ngo ubone uruzi rw'impinduka.", + "productLabel": "Igicuruzwa", + "lotLabel": "Loti", + "expiresWithDate": "Birarangira {date}", + "supplierWithName": "Utanga: {name}", + "empty": "Nta bikorwa byo guhuriza uruzi byabonetse." } } }, @@ -1105,7 +1120,10 @@ "pickDate": "Pick a date", "all": "All", "selectMethod": "Select method", - "selectPeriod": "Select period" + "selectPeriod": "Select period", + "csv": "CSV", + "pdf": "PDF", + "emptyDefault": "Hakuna data kwa ajili ya ripoti hii." }, "filters": { "valuationMethod": "Valuation Method", diff --git a/locale/ar/LC_MESSAGES/django.mo b/locale/ar/LC_MESSAGES/django.mo deleted file mode 100644 index 47dbff0..0000000 Binary files a/locale/ar/LC_MESSAGES/django.mo and /dev/null differ diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo deleted file mode 100644 index 418e0f9..0000000 Binary files a/locale/es/LC_MESSAGES/django.mo and /dev/null differ diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 5764f42..0000000 Binary files a/locale/fr/LC_MESSAGES/django.mo and /dev/null differ diff --git a/locale/rw/LC_MESSAGES/django.mo b/locale/rw/LC_MESSAGES/django.mo deleted file mode 100644 index fd7aa87..0000000 Binary files a/locale/rw/LC_MESSAGES/django.mo and /dev/null differ diff --git a/locale/sw/LC_MESSAGES/django.mo b/locale/sw/LC_MESSAGES/django.mo deleted file mode 100644 index 569477d..0000000 Binary files a/locale/sw/LC_MESSAGES/django.mo and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index aad754e..651a0d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,5 +9,12 @@ description = "An open-source inventory management system built with Wagtail CMS requires-python = ">=3.12" dynamic = ["dependencies"] +[tool.setuptools.packages.find] +where = ["src"] +exclude = ["tests*"] + +[tool.ruff.lint.per-file-ignores] +"src/the_inventory/__init__.py" = ["E402"] + [tool.setuptools.dynamic] dependencies = { file = ["requirements.txt"] } diff --git a/requirements.txt b/requirements.txt index e34e148..f6ada86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Django>=6,<6.1 wagtail==7.3.1 -wagtail-localize[polib]>=1.0.0 +wagtail-localize>=1.0.0 +polib>=1.2.0 django-filter>=24.0,<25 djangorestframework-simplejwt>=5.4,<6 django-cors-headers>=4.6,<5 diff --git a/seeders/management/commands/seed_database.py b/seeders/management/commands/seed_database.py index ef3e9d8..d1ede32 100644 --- a/seeders/management/commands/seed_database.py +++ b/seeders/management/commands/seed_database.py @@ -135,7 +135,7 @@ def _resolve_tenant(self, tenant_slug, create_default, verbose): tenant = Tenant.objects.get(slug="default") if verbose: self.stdout.write( - self.style.SUCCESS(f"✓ Using existing Default tenant") + self.style.SUCCESS("✓ Using existing Default tenant") ) return tenant except Tenant.DoesNotExist: @@ -157,10 +157,10 @@ def _resolve_tenant(self, tenant_slug, create_default, verbose): if verbose: if created: self.stdout.write( - self.style.SUCCESS(f"✓ Created Default tenant") + self.style.SUCCESS("✓ Created Default tenant") ) else: self.stdout.write( - self.style.SUCCESS(f"✓ Using existing Default tenant") + self.style.SUCCESS("✓ Using existing Default tenant") ) return tenant diff --git a/seeders/management/commands/sync_frontend_locales.py b/seeders/management/commands/sync_frontend_locales.py index a839b2b..3b3786d 100644 --- a/seeders/management/commands/sync_frontend_locales.py +++ b/seeders/management/commands/sync_frontend_locales.py @@ -24,16 +24,16 @@ def add_arguments(self, parser): "--output", default=None, help=( - "Output path (default: /frontend/src/i18n/locales-config.json)" + "Output path (default: /frontend/src/i18n/locales-config.json)" ), ) def handle(self, *args, **options): - base = Path(settings.BASE_DIR) + repo_root = Path(settings.REPO_ROOT) out = ( Path(options["output"]) if options["output"] - else base / "frontend" / "src" / "i18n" / "locales-config.json" + else repo_root / "frontend" / "src" / "i18n" / "locales-config.json" ) try: diff --git a/api/__init__.py b/src/api/__init__.py similarity index 100% rename from api/__init__.py rename to src/api/__init__.py diff --git a/api/apps.py b/src/api/apps.py similarity index 100% rename from api/apps.py rename to src/api/apps.py diff --git a/api/language.py b/src/api/language.py similarity index 100% rename from api/language.py rename to src/api/language.py diff --git a/api/middleware.py b/src/api/middleware.py similarity index 100% rename from api/middleware.py rename to src/api/middleware.py diff --git a/api/mixins/__init__.py b/src/api/mixins/__init__.py similarity index 100% rename from api/mixins/__init__.py rename to src/api/mixins/__init__.py diff --git a/api/mixins/translatable_read.py b/src/api/mixins/translatable_read.py similarity index 100% rename from api/mixins/translatable_read.py rename to src/api/mixins/translatable_read.py diff --git a/api/pagination.py b/src/api/pagination.py similarity index 100% rename from api/pagination.py rename to src/api/pagination.py diff --git a/api/permissions.py b/src/api/permissions.py similarity index 100% rename from api/permissions.py rename to src/api/permissions.py diff --git a/api/schema_i18n.py b/src/api/schema_i18n.py similarity index 100% rename from api/schema_i18n.py rename to src/api/schema_i18n.py diff --git a/api/serializers/__init__.py b/src/api/serializers/__init__.py similarity index 100% rename from api/serializers/__init__.py rename to src/api/serializers/__init__.py diff --git a/api/serializers/audit.py b/src/api/serializers/audit.py similarity index 100% rename from api/serializers/audit.py rename to src/api/serializers/audit.py diff --git a/api/serializers/auth.py b/src/api/serializers/auth.py similarity index 100% rename from api/serializers/auth.py rename to src/api/serializers/auth.py diff --git a/api/serializers/bulk.py b/src/api/serializers/bulk.py similarity index 100% rename from api/serializers/bulk.py rename to src/api/serializers/bulk.py diff --git a/api/serializers/cycle.py b/src/api/serializers/cycle.py similarity index 100% rename from api/serializers/cycle.py rename to src/api/serializers/cycle.py diff --git a/api/serializers/dashboard.py b/src/api/serializers/dashboard.py similarity index 100% rename from api/serializers/dashboard.py rename to src/api/serializers/dashboard.py diff --git a/api/serializers/inventory.py b/src/api/serializers/inventory.py similarity index 100% rename from api/serializers/inventory.py rename to src/api/serializers/inventory.py diff --git a/api/serializers/invitations.py b/src/api/serializers/invitations.py similarity index 100% rename from api/serializers/invitations.py rename to src/api/serializers/invitations.py diff --git a/api/serializers/jobs.py b/src/api/serializers/jobs.py similarity index 100% rename from api/serializers/jobs.py rename to src/api/serializers/jobs.py diff --git a/api/serializers/localized_strings.py b/src/api/serializers/localized_strings.py similarity index 100% rename from api/serializers/localized_strings.py rename to src/api/serializers/localized_strings.py diff --git a/api/serializers/procurement.py b/src/api/serializers/procurement.py similarity index 100% rename from api/serializers/procurement.py rename to src/api/serializers/procurement.py diff --git a/api/serializers/reports.py b/src/api/serializers/reports.py similarity index 100% rename from api/serializers/reports.py rename to src/api/serializers/reports.py diff --git a/api/serializers/reservation.py b/src/api/serializers/reservation.py similarity index 100% rename from api/serializers/reservation.py rename to src/api/serializers/reservation.py diff --git a/api/serializers/sales.py b/src/api/serializers/sales.py similarity index 100% rename from api/serializers/sales.py rename to src/api/serializers/sales.py diff --git a/api/serializers/tenants.py b/src/api/serializers/tenants.py similarity index 100% rename from api/serializers/tenants.py rename to src/api/serializers/tenants.py diff --git a/api/serializers/translatable_representation.py b/src/api/serializers/translatable_representation.py similarity index 100% rename from api/serializers/translatable_representation.py rename to src/api/serializers/translatable_representation.py diff --git a/api/serializers/translatable_writable.py b/src/api/serializers/translatable_writable.py similarity index 100% rename from api/serializers/translatable_writable.py rename to src/api/serializers/translatable_writable.py diff --git a/api/serializers/users.py b/src/api/serializers/users.py similarity index 100% rename from api/serializers/users.py rename to src/api/serializers/users.py diff --git a/api/urls.py b/src/api/urls.py similarity index 100% rename from api/urls.py rename to src/api/urls.py diff --git a/api/views/__init__.py b/src/api/views/__init__.py similarity index 100% rename from api/views/__init__.py rename to src/api/views/__init__.py diff --git a/api/views/audit.py b/src/api/views/audit.py similarity index 100% rename from api/views/audit.py rename to src/api/views/audit.py diff --git a/api/views/auth.py b/src/api/views/auth.py similarity index 100% rename from api/views/auth.py rename to src/api/views/auth.py diff --git a/api/views/billing.py b/src/api/views/billing.py similarity index 100% rename from api/views/billing.py rename to src/api/views/billing.py diff --git a/api/views/bulk.py b/src/api/views/bulk.py similarity index 97% rename from api/views/bulk.py rename to src/api/views/bulk.py index 0a3f8c1..bf48593 100644 --- a/api/views/bulk.py +++ b/src/api/views/bulk.py @@ -48,7 +48,7 @@ def post(self, request): created_by=created_by, fail_fast=data.get("fail_fast", False), ) - except InventoryError as e: + except InventoryError: logger.exception("Bulk transfer operation failed with an inventory error.") return Response( {"detail": "Bulk transfer operation could not be completed due to a conflict."}, @@ -90,7 +90,7 @@ def post(self, request): created_by=created_by, fail_fast=data.get("fail_fast", False), ) - except InventoryError as e: + except InventoryError: logger.exception("Bulk adjustment operation failed with an inventory error.") return Response( {"detail": "Bulk adjustment operation could not be completed due to a conflict."}, @@ -131,7 +131,7 @@ def post(self, request): items=items, fail_fast=data.get("fail_fast", False), ) - except InventoryError as e: + except InventoryError: logger.exception("Bulk revalue operation failed with an inventory error.") return Response( {"detail": "Bulk revalue operation could not be completed due to a conflict."}, diff --git a/api/views/cycle.py b/src/api/views/cycle.py similarity index 100% rename from api/views/cycle.py rename to src/api/views/cycle.py diff --git a/api/views/dashboard.py b/src/api/views/dashboard.py similarity index 100% rename from api/views/dashboard.py rename to src/api/views/dashboard.py diff --git a/api/views/imports.py b/src/api/views/imports.py similarity index 100% rename from api/views/imports.py rename to src/api/views/imports.py diff --git a/api/views/inventory.py b/src/api/views/inventory.py similarity index 99% rename from api/views/inventory.py rename to src/api/views/inventory.py index 46d757e..43e0b06 100644 --- a/api/views/inventory.py +++ b/src/api/views/inventory.py @@ -283,7 +283,6 @@ def get_queryset(self): @action(detail=False, methods=["get"]) def low_stock(self, request): """Return stock records that are at or below the product's reorder point.""" - tenant = self._get_current_tenant() records = [r for r in self.get_queryset() if r.is_low_stock] serializer = self.get_serializer(records, many=True) return Response(serializer.data) @@ -375,7 +374,7 @@ def create(self, request, *args, **kwargs): {"detail": str(e)}, status=status.HTTP_422_UNPROCESSABLE_ENTITY, ) - except DjangoValidationError as e: + except DjangoValidationError: return Response( {"detail": "Invalid stock movement data."}, status=status.HTTP_422_UNPROCESSABLE_ENTITY, diff --git a/api/views/invitations.py b/src/api/views/invitations.py similarity index 100% rename from api/views/invitations.py rename to src/api/views/invitations.py diff --git a/api/views/jobs.py b/src/api/views/jobs.py similarity index 100% rename from api/views/jobs.py rename to src/api/views/jobs.py diff --git a/api/views/locales.py b/src/api/views/locales.py similarity index 100% rename from api/views/locales.py rename to src/api/views/locales.py diff --git a/api/views/procurement.py b/src/api/views/procurement.py similarity index 97% rename from api/views/procurement.py rename to src/api/views/procurement.py index f2b5be5..cc5f8c1 100644 --- a/api/views/procurement.py +++ b/src/api/views/procurement.py @@ -70,7 +70,7 @@ def confirm(self, request, pk=None): purchase_order=po, confirmed_by=request.user, ) - except DjangoValidationError as e: + except DjangoValidationError: logger.exception("Validation error while confirming purchase order %s", po.pk) return Response( {"detail": "Could not confirm this purchase order due to validation errors."}, @@ -85,7 +85,7 @@ def cancel(self, request, pk=None): service = ProcurementService() try: service.cancel_order(purchase_order=po) - except DjangoValidationError as e: + except DjangoValidationError: logger.exception("Validation error while cancelling purchase order %s", po.pk) return Response( {"detail": "Could not cancel this purchase order due to validation errors."}, @@ -118,7 +118,7 @@ def receive(self, request, pk=None): goods_received_note=grn, received_by=request.user, ) - except DjangoValidationError as e: + except DjangoValidationError: logger.exception("Validation error while receiving goods for GRN %s", grn.pk) return Response( {"detail": "Could not process this goods received note due to validation errors."}, diff --git a/api/views/reports.py b/src/api/views/reports.py similarity index 100% rename from api/views/reports.py rename to src/api/views/reports.py diff --git a/api/views/reservation.py b/src/api/views/reservation.py similarity index 100% rename from api/views/reservation.py rename to src/api/views/reservation.py diff --git a/api/views/sales.py b/src/api/views/sales.py similarity index 100% rename from api/views/sales.py rename to src/api/views/sales.py diff --git a/api/views/tenants.py b/src/api/views/tenants.py similarity index 100% rename from api/views/tenants.py rename to src/api/views/tenants.py diff --git a/api/views/users.py b/src/api/views/users.py similarity index 100% rename from api/views/users.py rename to src/api/views/users.py diff --git a/home/__init__.py b/src/home/__init__.py similarity index 100% rename from home/__init__.py rename to src/home/__init__.py diff --git a/home/apps.py b/src/home/apps.py similarity index 100% rename from home/apps.py rename to src/home/apps.py diff --git a/home/i18n_sync.py b/src/home/i18n_sync.py similarity index 100% rename from home/i18n_sync.py rename to src/home/i18n_sync.py diff --git a/home/migrations/0001_initial.py b/src/home/migrations/0001_initial.py similarity index 100% rename from home/migrations/0001_initial.py rename to src/home/migrations/0001_initial.py diff --git a/home/migrations/0002_create_homepage.py b/src/home/migrations/0002_create_homepage.py similarity index 100% rename from home/migrations/0002_create_homepage.py rename to src/home/migrations/0002_create_homepage.py diff --git a/home/migrations/0003_seed_wagtail_locales.py b/src/home/migrations/0003_seed_wagtail_locales.py similarity index 100% rename from home/migrations/0003_seed_wagtail_locales.py rename to src/home/migrations/0003_seed_wagtail_locales.py diff --git a/home/migrations/0004_seed_locale_ar.py b/src/home/migrations/0004_seed_locale_ar.py similarity index 100% rename from home/migrations/0004_seed_locale_ar.py rename to src/home/migrations/0004_seed_locale_ar.py diff --git a/home/migrations/__init__.py b/src/home/migrations/__init__.py similarity index 100% rename from home/migrations/__init__.py rename to src/home/migrations/__init__.py diff --git a/home/models.py b/src/home/models.py similarity index 100% rename from home/models.py rename to src/home/models.py diff --git a/home/static/css/home_page.css b/src/home/static/css/home_page.css similarity index 100% rename from home/static/css/home_page.css rename to src/home/static/css/home_page.css diff --git a/home/static/css/welcome_page.css b/src/home/static/css/welcome_page.css similarity index 100% rename from home/static/css/welcome_page.css rename to src/home/static/css/welcome_page.css diff --git a/home/templates/home/home_page.html b/src/home/templates/home/home_page.html similarity index 100% rename from home/templates/home/home_page.html rename to src/home/templates/home/home_page.html diff --git a/home/templates/home/welcome_page.html b/src/home/templates/home/welcome_page.html similarity index 100% rename from home/templates/home/welcome_page.html rename to src/home/templates/home/welcome_page.html diff --git a/inventory/__init__.py b/src/inventory/__init__.py similarity index 100% rename from inventory/__init__.py rename to src/inventory/__init__.py diff --git a/inventory/admin.py b/src/inventory/admin.py similarity index 100% rename from inventory/admin.py rename to src/inventory/admin.py diff --git a/inventory/apps.py b/src/inventory/apps.py similarity index 100% rename from inventory/apps.py rename to src/inventory/apps.py diff --git a/inventory/exceptions.py b/src/inventory/exceptions.py similarity index 100% rename from inventory/exceptions.py rename to src/inventory/exceptions.py diff --git a/inventory/filters.py b/src/inventory/filters.py similarity index 100% rename from inventory/filters.py rename to src/inventory/filters.py diff --git a/inventory/imports/__init__.py b/src/inventory/imports/__init__.py similarity index 100% rename from inventory/imports/__init__.py rename to src/inventory/imports/__init__.py diff --git a/inventory/imports/forms.py b/src/inventory/imports/forms.py similarity index 100% rename from inventory/imports/forms.py rename to src/inventory/imports/forms.py diff --git a/inventory/imports/importers.py b/src/inventory/imports/importers.py similarity index 84% rename from inventory/imports/importers.py rename to src/inventory/imports/importers.py index 0e02867..49f45a3 100644 --- a/inventory/imports/importers.py +++ b/src/inventory/imports/importers.py @@ -22,6 +22,8 @@ from tenants.context import get_current_tenant +_UNSET_LOCALE = object() + @dataclass class ImportResult: @@ -46,12 +48,30 @@ class BaseImporter: REQUIRED_COLUMNS: tuple[str, ...] = () MODEL = None + #: Set True for :class:`~wagtail.models.TranslatableMixin` catalog rows. + REQUIRES_WAGTAIL_LOCALE = False def __init__(self, *, rows: list[dict], tenant=None, user=None): self.rows = rows self.tenant = tenant or get_current_tenant() self.user = user self.result = ImportResult() + self._default_wagtail_locale_cache = _UNSET_LOCALE + + def get_default_wagtail_locale(self): + """Wagtail locale for tenant canonical language, else first configured locale.""" + if self._default_wagtail_locale_cache is not _UNSET_LOCALE: + return self._default_wagtail_locale_cache + from wagtail.models import Locale + + from api.language import resolve_canonical_language_code, wagtail_locale_for_language + + code = resolve_canonical_language_code(self.tenant) + loc = wagtail_locale_for_language(code) + if loc is None: + loc = Locale.objects.order_by("pk").first() + self._default_wagtail_locale_cache = loc + return loc def run(self) -> ImportResult: if not self.rows: @@ -62,6 +82,14 @@ def run(self) -> ImportResult: if self.result.errors: return self.result + if self.REQUIRES_WAGTAIL_LOCALE and self.get_default_wagtail_locale() is None: + self.result.add_error( + 0, + "", + "No Wagtail locale configured. Add at least one locale in Wagtail admin.", + ) + return self.result + instances = [] for i, row in enumerate(self.rows, start=2): instance = self._process_row(i, row) @@ -117,6 +145,7 @@ class ProductImporter(BaseImporter): """ REQUIRED_COLUMNS = ("sku", "name") + REQUIRES_WAGTAIL_LOCALE = True @property def MODEL(self): @@ -158,6 +187,7 @@ def build_instance(self, row): reorder_point=int(row["reorder_point"].strip()) if row.get("reorder_point", "").strip() else 0, is_active=row.get("is_active", "true").strip().lower() not in ("false", "0", "no"), tenant=self.tenant, + locale=self.get_default_wagtail_locale(), created_by=self.user, ) @@ -172,6 +202,7 @@ class SupplierImporter(BaseImporter): """ REQUIRED_COLUMNS = ("code", "name") + REQUIRES_WAGTAIL_LOCALE = True @property def MODEL(self): @@ -200,6 +231,7 @@ def build_instance(self, row): return Supplier( code=row["code"].strip(), name=row["name"].strip(), + locale=self.get_default_wagtail_locale(), contact_name=row.get("contact_name", "").strip(), email=row.get("email", "").strip(), phone=row.get("phone", "").strip(), @@ -222,6 +254,7 @@ class CustomerImporter(BaseImporter): """ REQUIRED_COLUMNS = ("code", "name") + REQUIRES_WAGTAIL_LOCALE = True @property def MODEL(self): @@ -238,6 +271,7 @@ def build_instance(self, row): return Customer( code=row["code"].strip(), name=row["name"].strip(), + locale=self.get_default_wagtail_locale(), contact_name=row.get("contact_name", "").strip(), email=row.get("email", "").strip(), phone=row.get("phone", "").strip(), diff --git a/inventory/imports/parsers.py b/src/inventory/imports/parsers.py similarity index 100% rename from inventory/imports/parsers.py rename to src/inventory/imports/parsers.py diff --git a/inventory/imports/views.py b/src/inventory/imports/views.py similarity index 100% rename from inventory/imports/views.py rename to src/inventory/imports/views.py diff --git a/inventory/management/__init__.py b/src/inventory/management/__init__.py similarity index 100% rename from inventory/management/__init__.py rename to src/inventory/management/__init__.py diff --git a/inventory/management/commands/__init__.py b/src/inventory/management/commands/__init__.py similarity index 100% rename from inventory/management/commands/__init__.py rename to src/inventory/management/commands/__init__.py diff --git a/inventory/management/commands/expire_reservations.py b/src/inventory/management/commands/expire_reservations.py similarity index 100% rename from inventory/management/commands/expire_reservations.py rename to src/inventory/management/commands/expire_reservations.py diff --git a/inventory/migrations/0001_initial.py b/src/inventory/migrations/0001_initial.py similarity index 100% rename from inventory/migrations/0001_initial.py rename to src/inventory/migrations/0001_initial.py diff --git a/inventory/migrations/0002_product.py b/src/inventory/migrations/0002_product.py similarity index 100% rename from inventory/migrations/0002_product.py rename to src/inventory/migrations/0002_product.py diff --git a/inventory/migrations/0003_productimage.py b/src/inventory/migrations/0003_productimage.py similarity index 100% rename from inventory/migrations/0003_productimage.py rename to src/inventory/migrations/0003_productimage.py diff --git a/inventory/migrations/0004_producttag_product_tags.py b/src/inventory/migrations/0004_producttag_product_tags.py similarity index 100% rename from inventory/migrations/0004_producttag_product_tags.py rename to src/inventory/migrations/0004_producttag_product_tags.py diff --git a/inventory/migrations/0005_stocklocation.py b/src/inventory/migrations/0005_stocklocation.py similarity index 100% rename from inventory/migrations/0005_stocklocation.py rename to src/inventory/migrations/0005_stocklocation.py diff --git a/inventory/migrations/0006_stockmovement_stockrecord.py b/src/inventory/migrations/0006_stockmovement_stockrecord.py similarity index 100% rename from inventory/migrations/0006_stockmovement_stockrecord.py rename to src/inventory/migrations/0006_stockmovement_stockrecord.py diff --git a/inventory/migrations/0007_alter_category_description_alter_category_is_active_and_more.py b/src/inventory/migrations/0007_alter_category_description_alter_category_is_active_and_more.py similarity index 100% rename from inventory/migrations/0007_alter_category_description_alter_category_is_active_and_more.py rename to src/inventory/migrations/0007_alter_category_description_alter_category_is_active_and_more.py diff --git a/inventory/migrations/0008_category_tenant_product_tenant_stocklocation_tenant_and_more.py b/src/inventory/migrations/0008_category_tenant_product_tenant_stocklocation_tenant_and_more.py similarity index 100% rename from inventory/migrations/0008_category_tenant_product_tenant_stocklocation_tenant_and_more.py rename to src/inventory/migrations/0008_category_tenant_product_tenant_stocklocation_tenant_and_more.py diff --git a/inventory/migrations/0009_alter_category_slug_alter_product_sku_and_more.py b/src/inventory/migrations/0009_alter_category_slug_alter_product_sku_and_more.py similarity index 100% rename from inventory/migrations/0009_alter_category_slug_alter_product_sku_and_more.py rename to src/inventory/migrations/0009_alter_category_slug_alter_product_sku_and_more.py diff --git a/inventory/migrations/0010_lot_and_movement_lot.py b/src/inventory/migrations/0010_lot_and_movement_lot.py similarity index 100% rename from inventory/migrations/0010_lot_and_movement_lot.py rename to src/inventory/migrations/0010_lot_and_movement_lot.py diff --git a/inventory/migrations/0011_compliance_audit_log.py b/src/inventory/migrations/0011_compliance_audit_log.py similarity index 100% rename from inventory/migrations/0011_compliance_audit_log.py rename to src/inventory/migrations/0011_compliance_audit_log.py diff --git a/inventory/migrations/0012_stockreservation.py b/src/inventory/migrations/0012_stockreservation.py similarity index 100% rename from inventory/migrations/0012_stockreservation.py rename to src/inventory/migrations/0012_stockreservation.py diff --git a/inventory/migrations/0013_reservation_rule.py b/src/inventory/migrations/0013_reservation_rule.py similarity index 100% rename from inventory/migrations/0013_reservation_rule.py rename to src/inventory/migrations/0013_reservation_rule.py diff --git a/inventory/migrations/0014_add_stock_lot_to_reservation.py b/src/inventory/migrations/0014_add_stock_lot_to_reservation.py similarity index 100% rename from inventory/migrations/0014_add_stock_lot_to_reservation.py rename to src/inventory/migrations/0014_add_stock_lot_to_reservation.py diff --git a/inventory/migrations/0015_inventorycycle.py b/src/inventory/migrations/0015_inventorycycle.py similarity index 100% rename from inventory/migrations/0015_inventorycycle.py rename to src/inventory/migrations/0015_inventorycycle.py diff --git a/inventory/migrations/0016_cycle_count_line.py b/src/inventory/migrations/0016_cycle_count_line.py similarity index 100% rename from inventory/migrations/0016_cycle_count_line.py rename to src/inventory/migrations/0016_cycle_count_line.py diff --git a/inventory/migrations/0017_inventoryvariance.py b/src/inventory/migrations/0017_inventoryvariance.py similarity index 100% rename from inventory/migrations/0017_inventoryvariance.py rename to src/inventory/migrations/0017_inventoryvariance.py diff --git a/inventory/migrations/0018_stocklocation_max_capacity.py b/src/inventory/migrations/0018_stocklocation_max_capacity.py similarity index 100% rename from inventory/migrations/0018_stocklocation_max_capacity.py rename to src/inventory/migrations/0018_stocklocation_max_capacity.py diff --git a/inventory/migrations/0019_async_job.py b/src/inventory/migrations/0019_async_job.py similarity index 100% rename from inventory/migrations/0019_async_job.py rename to src/inventory/migrations/0019_async_job.py diff --git a/inventory/migrations/0020_alter_complianceauditlog_action.py b/src/inventory/migrations/0020_alter_complianceauditlog_action.py similarity index 100% rename from inventory/migrations/0020_alter_complianceauditlog_action.py rename to src/inventory/migrations/0020_alter_complianceauditlog_action.py diff --git a/inventory/migrations/0021_backfill_and_enforce_tenant.py b/src/inventory/migrations/0021_backfill_and_enforce_tenant.py similarity index 100% rename from inventory/migrations/0021_backfill_and_enforce_tenant.py rename to src/inventory/migrations/0021_backfill_and_enforce_tenant.py diff --git a/inventory/migrations/0022_alter_cyclecountline_tenant_and_more.py b/src/inventory/migrations/0022_alter_cyclecountline_tenant_and_more.py similarity index 100% rename from inventory/migrations/0022_alter_cyclecountline_tenant_and_more.py rename to src/inventory/migrations/0022_alter_cyclecountline_tenant_and_more.py diff --git a/inventory/migrations/0023_add_translatable_mixin_product_category.py b/src/inventory/migrations/0023_add_translatable_mixin_product_category.py similarity index 100% rename from inventory/migrations/0023_add_translatable_mixin_product_category.py rename to src/inventory/migrations/0023_add_translatable_mixin_product_category.py diff --git a/inventory/migrations/0024_align_translation_key_with_wagtail.py b/src/inventory/migrations/0024_align_translation_key_with_wagtail.py similarity index 100% rename from inventory/migrations/0024_align_translation_key_with_wagtail.py rename to src/inventory/migrations/0024_align_translation_key_with_wagtail.py diff --git a/inventory/migrations/__init__.py b/src/inventory/migrations/__init__.py similarity index 100% rename from inventory/migrations/__init__.py rename to src/inventory/migrations/__init__.py diff --git a/inventory/models/__init__.py b/src/inventory/models/__init__.py similarity index 100% rename from inventory/models/__init__.py rename to src/inventory/models/__init__.py diff --git a/inventory/models/audit.py b/src/inventory/models/audit.py similarity index 100% rename from inventory/models/audit.py rename to src/inventory/models/audit.py diff --git a/inventory/models/base.py b/src/inventory/models/base.py similarity index 100% rename from inventory/models/base.py rename to src/inventory/models/base.py diff --git a/inventory/models/category.py b/src/inventory/models/category.py similarity index 100% rename from inventory/models/category.py rename to src/inventory/models/category.py diff --git a/inventory/models/cycle.py b/src/inventory/models/cycle.py similarity index 100% rename from inventory/models/cycle.py rename to src/inventory/models/cycle.py diff --git a/inventory/models/job.py b/src/inventory/models/job.py similarity index 100% rename from inventory/models/job.py rename to src/inventory/models/job.py diff --git a/inventory/models/lot.py b/src/inventory/models/lot.py similarity index 100% rename from inventory/models/lot.py rename to src/inventory/models/lot.py diff --git a/inventory/models/product.py b/src/inventory/models/product.py similarity index 100% rename from inventory/models/product.py rename to src/inventory/models/product.py diff --git a/inventory/models/reservation.py b/src/inventory/models/reservation.py similarity index 100% rename from inventory/models/reservation.py rename to src/inventory/models/reservation.py diff --git a/inventory/models/stock.py b/src/inventory/models/stock.py similarity index 100% rename from inventory/models/stock.py rename to src/inventory/models/stock.py diff --git a/inventory/panels/__init__.py b/src/inventory/panels/__init__.py similarity index 100% rename from inventory/panels/__init__.py rename to src/inventory/panels/__init__.py diff --git a/inventory/panels/expiring_lots.py b/src/inventory/panels/expiring_lots.py similarity index 100% rename from inventory/panels/expiring_lots.py rename to src/inventory/panels/expiring_lots.py diff --git a/inventory/panels/low_stock.py b/src/inventory/panels/low_stock.py similarity index 100% rename from inventory/panels/low_stock.py rename to src/inventory/panels/low_stock.py diff --git a/inventory/panels/movement_trend_chart.py b/src/inventory/panels/movement_trend_chart.py similarity index 100% rename from inventory/panels/movement_trend_chart.py rename to src/inventory/panels/movement_trend_chart.py diff --git a/inventory/panels/order_status_chart.py b/src/inventory/panels/order_status_chart.py similarity index 100% rename from inventory/panels/order_status_chart.py rename to src/inventory/panels/order_status_chart.py diff --git a/inventory/panels/pending_reservations.py b/src/inventory/panels/pending_reservations.py similarity index 100% rename from inventory/panels/pending_reservations.py rename to src/inventory/panels/pending_reservations.py diff --git a/inventory/panels/recent_movements.py b/src/inventory/panels/recent_movements.py similarity index 100% rename from inventory/panels/recent_movements.py rename to src/inventory/panels/recent_movements.py diff --git a/inventory/panels/stock_by_location_chart.py b/src/inventory/panels/stock_by_location_chart.py similarity index 100% rename from inventory/panels/stock_by_location_chart.py rename to src/inventory/panels/stock_by_location_chart.py diff --git a/inventory/panels/stock_summary.py b/src/inventory/panels/stock_summary.py similarity index 100% rename from inventory/panels/stock_summary.py rename to src/inventory/panels/stock_summary.py diff --git a/inventory/services/__init__.py b/src/inventory/services/__init__.py similarity index 100% rename from inventory/services/__init__.py rename to src/inventory/services/__init__.py diff --git a/inventory/services/audit.py b/src/inventory/services/audit.py similarity index 100% rename from inventory/services/audit.py rename to src/inventory/services/audit.py diff --git a/inventory/services/bulk.py b/src/inventory/services/bulk.py similarity index 100% rename from inventory/services/bulk.py rename to src/inventory/services/bulk.py diff --git a/inventory/services/cache.py b/src/inventory/services/cache.py similarity index 100% rename from inventory/services/cache.py rename to src/inventory/services/cache.py diff --git a/inventory/services/cycle.py b/src/inventory/services/cycle.py similarity index 100% rename from inventory/services/cycle.py rename to src/inventory/services/cycle.py diff --git a/inventory/services/localization.py b/src/inventory/services/localization.py similarity index 100% rename from inventory/services/localization.py rename to src/inventory/services/localization.py diff --git a/inventory/services/reservation.py b/src/inventory/services/reservation.py similarity index 100% rename from inventory/services/reservation.py rename to src/inventory/services/reservation.py diff --git a/inventory/services/stock.py b/src/inventory/services/stock.py similarity index 100% rename from inventory/services/stock.py rename to src/inventory/services/stock.py diff --git a/inventory/static/css/inventory-admin.css b/src/inventory/static/css/inventory-admin.css similarity index 100% rename from inventory/static/css/inventory-admin.css rename to src/inventory/static/css/inventory-admin.css diff --git a/inventory/tasks.py b/src/inventory/tasks.py similarity index 100% rename from inventory/tasks.py rename to src/inventory/tasks.py diff --git a/inventory/templates/inventory/import.html b/src/inventory/templates/inventory/import.html similarity index 100% rename from inventory/templates/inventory/import.html rename to src/inventory/templates/inventory/import.html diff --git a/inventory/templates/inventory/low_stock_alerts.html b/src/inventory/templates/inventory/low_stock_alerts.html similarity index 100% rename from inventory/templates/inventory/low_stock_alerts.html rename to src/inventory/templates/inventory/low_stock_alerts.html diff --git a/inventory/templates/inventory/panels/expiring_lots.html b/src/inventory/templates/inventory/panels/expiring_lots.html similarity index 100% rename from inventory/templates/inventory/panels/expiring_lots.html rename to src/inventory/templates/inventory/panels/expiring_lots.html diff --git a/inventory/templates/inventory/panels/low_stock.html b/src/inventory/templates/inventory/panels/low_stock.html similarity index 100% rename from inventory/templates/inventory/panels/low_stock.html rename to src/inventory/templates/inventory/panels/low_stock.html diff --git a/inventory/templates/inventory/panels/movement_trend_chart.html b/src/inventory/templates/inventory/panels/movement_trend_chart.html similarity index 100% rename from inventory/templates/inventory/panels/movement_trend_chart.html rename to src/inventory/templates/inventory/panels/movement_trend_chart.html diff --git a/inventory/templates/inventory/panels/order_status_chart.html b/src/inventory/templates/inventory/panels/order_status_chart.html similarity index 100% rename from inventory/templates/inventory/panels/order_status_chart.html rename to src/inventory/templates/inventory/panels/order_status_chart.html diff --git a/inventory/templates/inventory/panels/pending_reservations.html b/src/inventory/templates/inventory/panels/pending_reservations.html similarity index 100% rename from inventory/templates/inventory/panels/pending_reservations.html rename to src/inventory/templates/inventory/panels/pending_reservations.html diff --git a/inventory/templates/inventory/panels/recent_movements.html b/src/inventory/templates/inventory/panels/recent_movements.html similarity index 100% rename from inventory/templates/inventory/panels/recent_movements.html rename to src/inventory/templates/inventory/panels/recent_movements.html diff --git a/inventory/templates/inventory/panels/stock_by_location_chart.html b/src/inventory/templates/inventory/panels/stock_by_location_chart.html similarity index 100% rename from inventory/templates/inventory/panels/stock_by_location_chart.html rename to src/inventory/templates/inventory/panels/stock_by_location_chart.html diff --git a/inventory/templates/inventory/panels/stock_summary.html b/src/inventory/templates/inventory/panels/stock_summary.html similarity index 100% rename from inventory/templates/inventory/panels/stock_summary.html rename to src/inventory/templates/inventory/panels/stock_summary.html diff --git a/inventory/templates/inventory/search.html b/src/inventory/templates/inventory/search.html similarity index 100% rename from inventory/templates/inventory/search.html rename to src/inventory/templates/inventory/search.html diff --git a/inventory/views.py b/src/inventory/views.py similarity index 100% rename from inventory/views.py rename to src/inventory/views.py diff --git a/inventory/wagtail_hooks.py b/src/inventory/wagtail_hooks.py similarity index 100% rename from inventory/wagtail_hooks.py rename to src/inventory/wagtail_hooks.py diff --git a/locale/.gitkeep b/src/locale/.gitkeep similarity index 100% rename from locale/.gitkeep rename to src/locale/.gitkeep diff --git a/locale/ar/LC_MESSAGES/django.po b/src/locale/ar/LC_MESSAGES/django.po similarity index 100% rename from locale/ar/LC_MESSAGES/django.po rename to src/locale/ar/LC_MESSAGES/django.po diff --git a/locale/es/LC_MESSAGES/django.po b/src/locale/es/LC_MESSAGES/django.po similarity index 100% rename from locale/es/LC_MESSAGES/django.po rename to src/locale/es/LC_MESSAGES/django.po diff --git a/locale/fr/LC_MESSAGES/django.po b/src/locale/fr/LC_MESSAGES/django.po similarity index 100% rename from locale/fr/LC_MESSAGES/django.po rename to src/locale/fr/LC_MESSAGES/django.po diff --git a/locale/rw/LC_MESSAGES/django.po b/src/locale/rw/LC_MESSAGES/django.po similarity index 100% rename from locale/rw/LC_MESSAGES/django.po rename to src/locale/rw/LC_MESSAGES/django.po diff --git a/locale/sw/LC_MESSAGES/django.po b/src/locale/sw/LC_MESSAGES/django.po similarity index 100% rename from locale/sw/LC_MESSAGES/django.po rename to src/locale/sw/LC_MESSAGES/django.po diff --git a/manage.py b/src/manage.py similarity index 100% rename from manage.py rename to src/manage.py diff --git a/media/images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.max-165x165.jpg b/src/media/images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.max-165x165.jpg similarity index 100% rename from media/images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.max-165x165.jpg rename to src/media/images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.max-165x165.jpg diff --git a/media/images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35.max-165x165.jpg b/src/media/images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35.max-165x165.jpg similarity index 100% rename from media/images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35.max-165x165.jpg rename to src/media/images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35.max-165x165.jpg diff --git a/media/original_images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.jpeg b/src/media/original_images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.jpeg similarity index 100% rename from media/original_images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.jpeg rename to src/media/original_images/WhatsApp_Image_2026-02-24_at_3.04.35_PM.jpeg diff --git a/media/original_images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35_PM.jpeg b/src/media/original_images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35_PM.jpeg similarity index 100% rename from media/original_images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35_PM.jpeg rename to src/media/original_images/WhatsApp_Image_2026-02-24_at_3_VjZk4YO.04.35_PM.jpeg diff --git a/procurement/__init__.py b/src/procurement/__init__.py similarity index 100% rename from procurement/__init__.py rename to src/procurement/__init__.py diff --git a/procurement/admin.py b/src/procurement/admin.py similarity index 100% rename from procurement/admin.py rename to src/procurement/admin.py diff --git a/procurement/apps.py b/src/procurement/apps.py similarity index 100% rename from procurement/apps.py rename to src/procurement/apps.py diff --git a/procurement/migrations/0001_initial.py b/src/procurement/migrations/0001_initial.py similarity index 100% rename from procurement/migrations/0001_initial.py rename to src/procurement/migrations/0001_initial.py diff --git a/procurement/migrations/0002_goodsreceivednote_tenant_purchaseorder_tenant_and_more.py b/src/procurement/migrations/0002_goodsreceivednote_tenant_purchaseorder_tenant_and_more.py similarity index 100% rename from procurement/migrations/0002_goodsreceivednote_tenant_purchaseorder_tenant_and_more.py rename to src/procurement/migrations/0002_goodsreceivednote_tenant_purchaseorder_tenant_and_more.py diff --git a/procurement/migrations/0003_alter_goodsreceivednote_grn_number_and_more.py b/src/procurement/migrations/0003_alter_goodsreceivednote_grn_number_and_more.py similarity index 100% rename from procurement/migrations/0003_alter_goodsreceivednote_grn_number_and_more.py rename to src/procurement/migrations/0003_alter_goodsreceivednote_grn_number_and_more.py diff --git a/procurement/migrations/0004_add_tenant_to_purchaseorderline.py b/src/procurement/migrations/0004_add_tenant_to_purchaseorderline.py similarity index 100% rename from procurement/migrations/0004_add_tenant_to_purchaseorderline.py rename to src/procurement/migrations/0004_add_tenant_to_purchaseorderline.py diff --git a/procurement/migrations/0005_align_tenant_nonnull.py b/src/procurement/migrations/0005_align_tenant_nonnull.py similarity index 100% rename from procurement/migrations/0005_align_tenant_nonnull.py rename to src/procurement/migrations/0005_align_tenant_nonnull.py diff --git a/procurement/migrations/0006_supplier_locale_supplier_translation_key_and_more.py b/src/procurement/migrations/0006_supplier_locale_supplier_translation_key_and_more.py similarity index 100% rename from procurement/migrations/0006_supplier_locale_supplier_translation_key_and_more.py rename to src/procurement/migrations/0006_supplier_locale_supplier_translation_key_and_more.py diff --git a/procurement/migrations/__init__.py b/src/procurement/migrations/__init__.py similarity index 100% rename from procurement/migrations/__init__.py rename to src/procurement/migrations/__init__.py diff --git a/procurement/models/__init__.py b/src/procurement/models/__init__.py similarity index 100% rename from procurement/models/__init__.py rename to src/procurement/models/__init__.py diff --git a/procurement/models/order.py b/src/procurement/models/order.py similarity index 100% rename from procurement/models/order.py rename to src/procurement/models/order.py diff --git a/procurement/models/supplier.py b/src/procurement/models/supplier.py similarity index 100% rename from procurement/models/supplier.py rename to src/procurement/models/supplier.py diff --git a/procurement/services/__init__.py b/src/procurement/services/__init__.py similarity index 100% rename from procurement/services/__init__.py rename to src/procurement/services/__init__.py diff --git a/procurement/services/procurement.py b/src/procurement/services/procurement.py similarity index 100% rename from procurement/services/procurement.py rename to src/procurement/services/procurement.py diff --git a/procurement/wagtail_hooks.py b/src/procurement/wagtail_hooks.py similarity index 100% rename from procurement/wagtail_hooks.py rename to src/procurement/wagtail_hooks.py diff --git a/reports/__init__.py b/src/reports/__init__.py similarity index 100% rename from reports/__init__.py rename to src/reports/__init__.py diff --git a/reports/apps.py b/src/reports/apps.py similarity index 100% rename from reports/apps.py rename to src/reports/apps.py diff --git a/reports/exports.py b/src/reports/exports.py similarity index 100% rename from reports/exports.py rename to src/reports/exports.py diff --git a/reports/filters.py b/src/reports/filters.py similarity index 100% rename from reports/filters.py rename to src/reports/filters.py diff --git a/reports/pdf.py b/src/reports/pdf.py similarity index 100% rename from reports/pdf.py rename to src/reports/pdf.py diff --git a/reports/services/__init__.py b/src/reports/services/__init__.py similarity index 100% rename from reports/services/__init__.py rename to src/reports/services/__init__.py diff --git a/reports/services/inventory_reports.py b/src/reports/services/inventory_reports.py similarity index 100% rename from reports/services/inventory_reports.py rename to src/reports/services/inventory_reports.py diff --git a/reports/services/order_reports.py b/src/reports/services/order_reports.py similarity index 100% rename from reports/services/order_reports.py rename to src/reports/services/order_reports.py diff --git a/reports/templates/reports/expiry_report.html b/src/reports/templates/reports/expiry_report.html similarity index 100% rename from reports/templates/reports/expiry_report.html rename to src/reports/templates/reports/expiry_report.html diff --git a/reports/templates/reports/low_stock_report.html b/src/reports/templates/reports/low_stock_report.html similarity index 100% rename from reports/templates/reports/low_stock_report.html rename to src/reports/templates/reports/low_stock_report.html diff --git a/reports/templates/reports/movement_history.html b/src/reports/templates/reports/movement_history.html similarity index 100% rename from reports/templates/reports/movement_history.html rename to src/reports/templates/reports/movement_history.html diff --git a/reports/templates/reports/overstock_report.html b/src/reports/templates/reports/overstock_report.html similarity index 100% rename from reports/templates/reports/overstock_report.html rename to src/reports/templates/reports/overstock_report.html diff --git a/reports/templates/reports/purchase_summary.html b/src/reports/templates/reports/purchase_summary.html similarity index 100% rename from reports/templates/reports/purchase_summary.html rename to src/reports/templates/reports/purchase_summary.html diff --git a/reports/templates/reports/sales_summary.html b/src/reports/templates/reports/sales_summary.html similarity index 100% rename from reports/templates/reports/sales_summary.html rename to src/reports/templates/reports/sales_summary.html diff --git a/reports/templates/reports/stock_valuation.html b/src/reports/templates/reports/stock_valuation.html similarity index 100% rename from reports/templates/reports/stock_valuation.html rename to src/reports/templates/reports/stock_valuation.html diff --git a/reports/views.py b/src/reports/views.py similarity index 100% rename from reports/views.py rename to src/reports/views.py diff --git a/reports/wagtail_hooks.py b/src/reports/wagtail_hooks.py similarity index 100% rename from reports/wagtail_hooks.py rename to src/reports/wagtail_hooks.py diff --git a/sales/__init__.py b/src/sales/__init__.py similarity index 100% rename from sales/__init__.py rename to src/sales/__init__.py diff --git a/sales/admin.py b/src/sales/admin.py similarity index 100% rename from sales/admin.py rename to src/sales/admin.py diff --git a/sales/apps.py b/src/sales/apps.py similarity index 100% rename from sales/apps.py rename to src/sales/apps.py diff --git a/sales/migrations/0001_initial.py b/src/sales/migrations/0001_initial.py similarity index 100% rename from sales/migrations/0001_initial.py rename to src/sales/migrations/0001_initial.py diff --git a/sales/migrations/0002_customer_tenant_dispatch_tenant_salesorder_tenant_and_more.py b/src/sales/migrations/0002_customer_tenant_dispatch_tenant_salesorder_tenant_and_more.py similarity index 100% rename from sales/migrations/0002_customer_tenant_dispatch_tenant_salesorder_tenant_and_more.py rename to src/sales/migrations/0002_customer_tenant_dispatch_tenant_salesorder_tenant_and_more.py diff --git a/sales/migrations/0003_alter_customer_code_alter_dispatch_dispatch_number_and_more.py b/src/sales/migrations/0003_alter_customer_code_alter_dispatch_dispatch_number_and_more.py similarity index 100% rename from sales/migrations/0003_alter_customer_code_alter_dispatch_dispatch_number_and_more.py rename to src/sales/migrations/0003_alter_customer_code_alter_dispatch_dispatch_number_and_more.py diff --git a/sales/migrations/0004_add_tenant_to_salesorderline.py b/src/sales/migrations/0004_add_tenant_to_salesorderline.py similarity index 100% rename from sales/migrations/0004_add_tenant_to_salesorderline.py rename to src/sales/migrations/0004_add_tenant_to_salesorderline.py diff --git a/sales/migrations/0005_alter_customer_tenant_alter_dispatch_tenant_and_more.py b/src/sales/migrations/0005_alter_customer_tenant_alter_dispatch_tenant_and_more.py similarity index 100% rename from sales/migrations/0005_alter_customer_tenant_alter_dispatch_tenant_and_more.py rename to src/sales/migrations/0005_alter_customer_tenant_alter_dispatch_tenant_and_more.py diff --git a/sales/migrations/0006_customer_locale_customer_translation_key_and_more.py b/src/sales/migrations/0006_customer_locale_customer_translation_key_and_more.py similarity index 100% rename from sales/migrations/0006_customer_locale_customer_translation_key_and_more.py rename to src/sales/migrations/0006_customer_locale_customer_translation_key_and_more.py diff --git a/sales/migrations/__init__.py b/src/sales/migrations/__init__.py similarity index 100% rename from sales/migrations/__init__.py rename to src/sales/migrations/__init__.py diff --git a/sales/models/__init__.py b/src/sales/models/__init__.py similarity index 100% rename from sales/models/__init__.py rename to src/sales/models/__init__.py diff --git a/sales/models/customer.py b/src/sales/models/customer.py similarity index 100% rename from sales/models/customer.py rename to src/sales/models/customer.py diff --git a/sales/models/order.py b/src/sales/models/order.py similarity index 100% rename from sales/models/order.py rename to src/sales/models/order.py diff --git a/sales/services/__init__.py b/src/sales/services/__init__.py similarity index 100% rename from sales/services/__init__.py rename to src/sales/services/__init__.py diff --git a/sales/services/sales.py b/src/sales/services/sales.py similarity index 100% rename from sales/services/sales.py rename to src/sales/services/sales.py diff --git a/sales/wagtail_hooks.py b/src/sales/wagtail_hooks.py similarity index 100% rename from sales/wagtail_hooks.py rename to src/sales/wagtail_hooks.py diff --git a/search/__init__.py b/src/search/__init__.py similarity index 100% rename from search/__init__.py rename to src/search/__init__.py diff --git a/search/templates/search/search.html b/src/search/templates/search/search.html similarity index 100% rename from search/templates/search/search.html rename to src/search/templates/search/search.html diff --git a/search/views.py b/src/search/views.py similarity index 100% rename from search/views.py rename to src/search/views.py diff --git a/tenants/__init__.py b/src/tenants/__init__.py similarity index 100% rename from tenants/__init__.py rename to src/tenants/__init__.py diff --git a/tenants/admin.py b/src/tenants/admin.py similarity index 100% rename from tenants/admin.py rename to src/tenants/admin.py diff --git a/tenants/apps.py b/src/tenants/apps.py similarity index 100% rename from tenants/apps.py rename to src/tenants/apps.py diff --git a/tenants/context.py b/src/tenants/context.py similarity index 100% rename from tenants/context.py rename to src/tenants/context.py diff --git a/tenants/context_processors.py b/src/tenants/context_processors.py similarity index 100% rename from tenants/context_processors.py rename to src/tenants/context_processors.py diff --git a/tenants/django_admin_site.py b/src/tenants/django_admin_site.py similarity index 100% rename from tenants/django_admin_site.py rename to src/tenants/django_admin_site.py diff --git a/tenants/management/__init__.py b/src/tenants/management/__init__.py similarity index 100% rename from tenants/management/__init__.py rename to src/tenants/management/__init__.py diff --git a/tenants/management/commands/__init__.py b/src/tenants/management/commands/__init__.py similarity index 100% rename from tenants/management/commands/__init__.py rename to src/tenants/management/commands/__init__.py diff --git a/tenants/management/commands/createtenant.py b/src/tenants/management/commands/createtenant.py similarity index 100% rename from tenants/management/commands/createtenant.py rename to src/tenants/management/commands/createtenant.py diff --git a/tenants/management/commands/export_tenant.py b/src/tenants/management/commands/export_tenant.py similarity index 100% rename from tenants/management/commands/export_tenant.py rename to src/tenants/management/commands/export_tenant.py diff --git a/tenants/managers.py b/src/tenants/managers.py similarity index 100% rename from tenants/managers.py rename to src/tenants/managers.py diff --git a/tenants/middleware.py b/src/tenants/middleware.py similarity index 100% rename from tenants/middleware.py rename to src/tenants/middleware.py diff --git a/tenants/migrations/0001_initial.py b/src/tenants/migrations/0001_initial.py similarity index 100% rename from tenants/migrations/0001_initial.py rename to src/tenants/migrations/0001_initial.py diff --git a/tenants/migrations/0002_populate_default_tenant.py b/src/tenants/migrations/0002_populate_default_tenant.py similarity index 100% rename from tenants/migrations/0002_populate_default_tenant.py rename to src/tenants/migrations/0002_populate_default_tenant.py diff --git a/tenants/migrations/0003_add_tenant_invitation.py b/src/tenants/migrations/0003_add_tenant_invitation.py similarity index 100% rename from tenants/migrations/0003_add_tenant_invitation.py rename to src/tenants/migrations/0003_add_tenant_invitation.py diff --git a/tenants/migrations/0004_alter_tenantmembership_tenant.py b/src/tenants/migrations/0004_alter_tenantmembership_tenant.py similarity index 100% rename from tenants/migrations/0004_alter_tenantmembership_tenant.py rename to src/tenants/migrations/0004_alter_tenantmembership_tenant.py diff --git a/tenants/migrations/0005_add_tenant_limit_overrides.py b/src/tenants/migrations/0005_add_tenant_limit_overrides.py similarity index 100% rename from tenants/migrations/0005_add_tenant_limit_overrides.py rename to src/tenants/migrations/0005_add_tenant_limit_overrides.py diff --git a/tenants/migrations/0006_add_billing_notes.py b/src/tenants/migrations/0006_add_billing_notes.py similarity index 100% rename from tenants/migrations/0006_add_billing_notes.py rename to src/tenants/migrations/0006_add_billing_notes.py diff --git a/tenants/migrations/0007_add_tenant_preferred_language.py b/src/tenants/migrations/0007_add_tenant_preferred_language.py similarity index 100% rename from tenants/migrations/0007_add_tenant_preferred_language.py rename to src/tenants/migrations/0007_add_tenant_preferred_language.py diff --git a/tenants/migrations/0008_alter_tenant_preferred_language_wagtail_locales.py b/src/tenants/migrations/0008_alter_tenant_preferred_language_wagtail_locales.py similarity index 100% rename from tenants/migrations/0008_alter_tenant_preferred_language_wagtail_locales.py rename to src/tenants/migrations/0008_alter_tenant_preferred_language_wagtail_locales.py diff --git a/tenants/migrations/__init__.py b/src/tenants/migrations/__init__.py similarity index 100% rename from tenants/migrations/__init__.py rename to src/tenants/migrations/__init__.py diff --git a/tenants/models.py b/src/tenants/models.py similarity index 100% rename from tenants/models.py rename to src/tenants/models.py diff --git a/tenants/panels/__init__.py b/src/tenants/panels/__init__.py similarity index 100% rename from tenants/panels/__init__.py rename to src/tenants/panels/__init__.py diff --git a/tenants/panels/platform_overview.py b/src/tenants/panels/platform_overview.py similarity index 100% rename from tenants/panels/platform_overview.py rename to src/tenants/panels/platform_overview.py diff --git a/tenants/panels/recent_tenants.py b/src/tenants/panels/recent_tenants.py similarity index 100% rename from tenants/panels/recent_tenants.py rename to src/tenants/panels/recent_tenants.py diff --git a/tenants/panels/subscription_stats.py b/src/tenants/panels/subscription_stats.py similarity index 100% rename from tenants/panels/subscription_stats.py rename to src/tenants/panels/subscription_stats.py diff --git a/tenants/permissions.py b/src/tenants/permissions.py similarity index 100% rename from tenants/permissions.py rename to src/tenants/permissions.py diff --git a/tenants/services.py b/src/tenants/services.py similarity index 100% rename from tenants/services.py rename to src/tenants/services.py diff --git a/tenants/snippets.py b/src/tenants/snippets.py similarity index 100% rename from tenants/snippets.py rename to src/tenants/snippets.py diff --git a/tenants/static/css/platform-admin.css b/src/tenants/static/css/platform-admin.css similarity index 100% rename from tenants/static/css/platform-admin.css rename to src/tenants/static/css/platform-admin.css diff --git a/tenants/templates/tenants/admin/confirm_deactivate.html b/src/tenants/templates/tenants/admin/confirm_deactivate.html similarity index 100% rename from tenants/templates/tenants/admin/confirm_deactivate.html rename to src/tenants/templates/tenants/admin/confirm_deactivate.html diff --git a/tenants/templates/tenants/admin/confirm_reactivate.html b/src/tenants/templates/tenants/admin/confirm_reactivate.html similarity index 100% rename from tenants/templates/tenants/admin/confirm_reactivate.html rename to src/tenants/templates/tenants/admin/confirm_reactivate.html diff --git a/tenants/templates/tenants/panels/platform_overview.html b/src/tenants/templates/tenants/panels/platform_overview.html similarity index 100% rename from tenants/templates/tenants/panels/platform_overview.html rename to src/tenants/templates/tenants/panels/platform_overview.html diff --git a/tenants/templates/tenants/panels/recent_tenants.html b/src/tenants/templates/tenants/panels/recent_tenants.html similarity index 100% rename from tenants/templates/tenants/panels/recent_tenants.html rename to src/tenants/templates/tenants/panels/recent_tenants.html diff --git a/tenants/templates/tenants/panels/subscription_stats.html b/src/tenants/templates/tenants/panels/subscription_stats.html similarity index 100% rename from tenants/templates/tenants/panels/subscription_stats.html rename to src/tenants/templates/tenants/panels/subscription_stats.html diff --git a/tenants/wagtail_forms.py b/src/tenants/wagtail_forms.py similarity index 100% rename from tenants/wagtail_forms.py rename to src/tenants/wagtail_forms.py diff --git a/tenants/wagtail_hooks.py b/src/tenants/wagtail_hooks.py similarity index 100% rename from tenants/wagtail_hooks.py rename to src/tenants/wagtail_hooks.py diff --git a/tenants/wagtail_locales.py b/src/tenants/wagtail_locales.py similarity index 100% rename from tenants/wagtail_locales.py rename to src/tenants/wagtail_locales.py diff --git a/tenants/wagtail_views.py b/src/tenants/wagtail_views.py similarity index 100% rename from tenants/wagtail_views.py rename to src/tenants/wagtail_views.py diff --git a/src/the_inventory/__init__.py b/src/the_inventory/__init__.py new file mode 100644 index 0000000..77e9b19 --- /dev/null +++ b/src/the_inventory/__init__.py @@ -0,0 +1,19 @@ +import sys +from pathlib import Path + +# Repo root holds ``seeders`` and ``tests``; ``src/`` holds Django apps. When +# ``manage.py`` or Gunicorn run with cwd ``src/``, only ``src`` is on ``sys.path`` +# by default — add both roots so ``seeders`` and ``tests`` import correctly. +_src_dir = Path(__file__).resolve().parent.parent +_repo_root = _src_dir.parent +for _path in (_repo_root, _src_dir): + _s = str(_path) + try: + sys.path.remove(_s) + except ValueError: + pass + sys.path.insert(0, _s) + +from .celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/the_inventory/celery.py b/src/the_inventory/celery.py similarity index 100% rename from the_inventory/celery.py rename to src/the_inventory/celery.py diff --git a/the_inventory/settings/__init__.py b/src/the_inventory/settings/__init__.py similarity index 100% rename from the_inventory/settings/__init__.py rename to src/the_inventory/settings/__init__.py diff --git a/the_inventory/settings/base.py b/src/the_inventory/settings/base.py similarity index 97% rename from the_inventory/settings/base.py rename to src/the_inventory/settings/base.py index 9bc66e8..0010de0 100644 --- a/the_inventory/settings/base.py +++ b/src/the_inventory/settings/base.py @@ -18,24 +18,27 @@ from .env_utils import env_bool, env_int, env_list, env_str -# Load environment variables from .env file +PROJECT_DIR = Path(__file__).resolve().parent.parent +BASE_DIR = PROJECT_DIR.parent +# Repository root (parent of src/); .env, frontend/, etc. live here. +REPO_ROOT = BASE_DIR.parent + +# Load environment variables from .env at repository root +_env_file = REPO_ROOT / ".env" try: from dotenv import load_dotenv - load_dotenv() + + load_dotenv(_env_file) except ImportError: # If python-dotenv is not installed, manually load .env - env_file = Path(__file__).resolve().parent.parent.parent / ".env" - if env_file.exists(): - with open(env_file) as f: + if _env_file.exists(): + with open(_env_file) as f: for line in f: line = line.strip() if line and not line.startswith("#") and "=" in line: key, value = line.split("=", 1) os.environ.setdefault(key.strip(), value.strip()) -PROJECT_DIR = Path(__file__).resolve().parent.parent -BASE_DIR = PROJECT_DIR.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ diff --git a/the_inventory/settings/dev.py b/src/the_inventory/settings/dev.py similarity index 100% rename from the_inventory/settings/dev.py rename to src/the_inventory/settings/dev.py diff --git a/the_inventory/settings/env_utils.py b/src/the_inventory/settings/env_utils.py similarity index 100% rename from the_inventory/settings/env_utils.py rename to src/the_inventory/settings/env_utils.py diff --git a/the_inventory/settings/production.py b/src/the_inventory/settings/production.py similarity index 100% rename from the_inventory/settings/production.py rename to src/the_inventory/settings/production.py diff --git a/the_inventory/static/css/main.css b/src/the_inventory/static/css/main.css similarity index 100% rename from the_inventory/static/css/main.css rename to src/the_inventory/static/css/main.css diff --git a/the_inventory/static/css/the_inventory.css b/src/the_inventory/static/css/the_inventory.css similarity index 100% rename from the_inventory/static/css/the_inventory.css rename to src/the_inventory/static/css/the_inventory.css diff --git a/the_inventory/static/js/the_inventory.js b/src/the_inventory/static/js/the_inventory.js similarity index 100% rename from the_inventory/static/js/the_inventory.js rename to src/the_inventory/static/js/the_inventory.js diff --git a/the_inventory/templates/404.html b/src/the_inventory/templates/404.html similarity index 100% rename from the_inventory/templates/404.html rename to src/the_inventory/templates/404.html diff --git a/the_inventory/templates/500.html b/src/the_inventory/templates/500.html similarity index 100% rename from the_inventory/templates/500.html rename to src/the_inventory/templates/500.html diff --git a/the_inventory/templates/base.html b/src/the_inventory/templates/base.html similarity index 100% rename from the_inventory/templates/base.html rename to src/the_inventory/templates/base.html diff --git a/the_inventory/urls.py b/src/the_inventory/urls.py similarity index 100% rename from the_inventory/urls.py rename to src/the_inventory/urls.py diff --git a/the_inventory/wsgi.py b/src/the_inventory/wsgi.py similarity index 100% rename from the_inventory/wsgi.py rename to src/the_inventory/wsgi.py diff --git a/tests/api/test_api_i18n_authoring.py b/tests/api/test_api_i18n_authoring.py index 57fbd4a..8481abe 100644 --- a/tests/api/test_api_i18n_authoring.py +++ b/tests/api/test_api_i18n_authoring.py @@ -79,7 +79,7 @@ def test_post_french_creates_linked_translation(self): tenant=self.tenant, ) response = self.client.post( - f"/api/v1/products/?language=fr", + "/api/v1/products/?language=fr", { "translation_of": p_en.pk, "sku": p_en.sku, diff --git a/tests/api/test_regression_tenant_scoping.py b/tests/api/test_regression_tenant_scoping.py index 7c5d8df..06b1d68 100644 --- a/tests/api/test_regression_tenant_scoping.py +++ b/tests/api/test_regression_tenant_scoping.py @@ -23,8 +23,8 @@ create_category, create_location, create_product, create_stock_record, create_tenant, ) -from tenants.context import get_current_tenant, set_current_tenant -from tenants.models import Tenant, TenantMembership, TenantRole +from tenants.context import set_current_tenant +from tenants.models import TenantMembership, TenantRole User = get_user_model() diff --git a/tests/conftest.py b/tests/conftest.py index bd21060..a7547bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ import pytest from django.contrib.auth import get_user_model -from tenants.models import Tenant, TenantMembership, TenantRole +from tenants.models import TenantMembership, TenantRole from tenants.context import set_current_tenant, clear_current_tenant from tests.fixtures.factories import create_tenant as factory_create_tenant diff --git a/tests/inventory/test_timestamped_model.py b/tests/inventory/test_timestamped_model.py index 48f8160..6f10baf 100644 --- a/tests/inventory/test_timestamped_model.py +++ b/tests/inventory/test_timestamped_model.py @@ -405,7 +405,7 @@ def test_filter_by_current_tenant_with_multiple_models(self): slug="cat-a", tenant=self.tenant_a, ) - cat_b = Category.objects.create( + Category.objects.create( name="Category B", slug="cat-b", tenant=self.tenant_b, diff --git a/tests/reports/test_views.py b/tests/reports/test_views.py index ee3b9dd..0cbea5b 100644 --- a/tests/reports/test_views.py +++ b/tests/reports/test_views.py @@ -9,7 +9,6 @@ from django.contrib.auth import get_user_model from django.core.exceptions import PermissionDenied -from django.http import HttpRequest from django.test import RequestFactory, TestCase from tenants.context import clear_current_tenant, set_current_tenant diff --git a/tests/seeders/test_base_seeder.py b/tests/seeders/test_base_seeder.py index 2242734..6f16924 100644 --- a/tests/seeders/test_base_seeder.py +++ b/tests/seeders/test_base_seeder.py @@ -1,6 +1,6 @@ """Tests for BaseSeeder and its tenant context functionality.""" -from django.test import TestCase, TransactionTestCase +from django.test import TransactionTestCase from tenants.models import Tenant from inventory.models.product import Product from inventory.models.category import Category diff --git a/tests/seeders/test_seed_command.py b/tests/seeders/test_seed_command.py index 7eb927b..4f387fe 100644 --- a/tests/seeders/test_seed_command.py +++ b/tests/seeders/test_seed_command.py @@ -1,7 +1,7 @@ """Tests for seed_database management command with tenant context.""" from io import StringIO -from django.test import TestCase, TransactionTestCase +from django.test import TransactionTestCase from django.core.management import call_command from django.core.management.base import CommandError @@ -77,7 +77,7 @@ def test_seed_command_fails_without_default_tenant(self): def test_seed_command_uses_specified_tenant(self): """Test: seed_database seeds into specified tenant when --tenant is provided.""" # Create a specific tenant - acme_tenant = create_tenant(name="Acme Corp", slug="acme-corp") + create_tenant(name="Acme Corp", slug="acme-corp") # Call command with --tenant flag call_command( diff --git a/tests/seeders/test_seeder_manager.py b/tests/seeders/test_seeder_manager.py index 8c75153..98f5350 100644 --- a/tests/seeders/test_seeder_manager.py +++ b/tests/seeders/test_seeder_manager.py @@ -1,9 +1,8 @@ """Tests for SeederManager.""" -from django.test import TestCase, TransactionTestCase +from django.test import TransactionTestCase from tenants.models import Tenant from seeders import SeederManager -from seeders.tenant_seeder import TenantSeeder class SeederManagerTestCase(TransactionTestCase): @@ -93,7 +92,7 @@ def test_seeder_manager_passes_tenant_to_seeders(self): def test_seeder_manager_accepts_tenant_parameter(self): """Test that SeederManager.seed() accepts optional tenant parameter.""" # Create a tenant first - custom_tenant = Tenant.objects.create( + Tenant.objects.create( name="Custom", slug="custom", is_active=True, @@ -217,14 +216,14 @@ def test_seeder_manager_obeys_clear_data_flag(self): from inventory.models import Category manager1 = SeederManager(verbose=False, clear_data=False) - result1 = manager1.seed() + manager1.seed() categories_after_first = Category.objects.count() self.assertGreater(categories_after_first, 0) # Run with clear_data=True manager2 = SeederManager(verbose=False, clear_data=True) - result2 = manager2.seed() + manager2.seed() # Categories should be repopulated (data should be cleared and reseeded) categories_after_clear = Category.objects.count() @@ -253,7 +252,7 @@ def test_seeder_manager_with_different_tenants(self): # Note: We don't run seed() here because child seeders haven't been updated # to filter by tenant yet (TS-04 handles that). This test just verifies # that SeederManager accepts and stores the tenant parameter. - manager2 = SeederManager(verbose=False, clear_data=False) + _ = SeederManager(verbose=False, clear_data=False) # Verify different tenants can be created and distinguished self.assertNotEqual(tenant1.id, custom_tenant.id) diff --git a/tests/seeders/test_seeder_tenant.py b/tests/seeders/test_seeder_tenant.py index 891e528..6058294 100644 --- a/tests/seeders/test_seeder_tenant.py +++ b/tests/seeders/test_seeder_tenant.py @@ -14,7 +14,6 @@ from django.core.management.base import CommandError from tenants.models import Tenant -from tenants.context import get_current_tenant, set_current_tenant from seeders.tenant_seeder import TenantSeeder from seeders.seeder_manager import SeederManager from inventory.models import ( @@ -521,7 +520,7 @@ def test_seeder_manager_full_pipeline_creates_tenant_scoped_data(self): tenant = Tenant.objects.create(name="Test", slug="test") manager = SeederManager(verbose=False) - result = manager.seed(tenant=tenant) + manager.seed(tenant=tenant) # Verify data was created self.assertGreater(Category.objects.count(), 0) @@ -663,7 +662,7 @@ def test_seed_command_fails_with_missing_nonexistent_tenant(self): def test_seed_command_output_shows_tenant_info(self): """Test: seed_database output includes tenant information.""" - custom_tenant = Tenant.objects.create(name="Test Tenant", slug="test-tenant") + Tenant.objects.create(name="Test Tenant", slug="test-tenant") call_command( "seed_database", diff --git a/tests/test_locale_catalogs.py b/tests/test_locale_catalogs.py index 0520d75..d3a4903 100644 --- a/tests/test_locale_catalogs.py +++ b/tests/test_locale_catalogs.py @@ -12,9 +12,9 @@ class LocaleCatalogTests(SimpleTestCase): def test_django_po_files_compile(self) -> None: - root = Path(__file__).resolve().parent.parent + repo_root = Path(__file__).resolve().parent.parent for lang in ("fr", "sw", "rw", "es", "ar"): - po = root / "locale" / lang / "LC_MESSAGES" / "django.po" + po = repo_root / "src" / "locale" / lang / "LC_MESSAGES" / "django.po" self.assertTrue( po.is_file(), f"Missing {po}", diff --git a/the_inventory/__init__.py b/the_inventory/__init__.py deleted file mode 100644 index 53f4ccb..0000000 --- a/the_inventory/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .celery import app as celery_app - -__all__ = ("celery_app",)