[PR #1184] [MERGED] [Feature] Add Stock forecasting and Logistics view #1168

Closed
opened 2026-05-06 12:35:28 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/maziggy/bambuddy/pull/1184
Author: @Keybored02
Created: 5/1/2026
Status: Merged
Merged: 5/5/2026
Merged by: @maziggy

Base: devHead: feature/stock_track


📝 Commits (10+)

  • fbfa433 Add stock prediction
  • 0fe972e Add Logistics view, testing, locales, permission compliance
  • bd5fda2 Fix misc
  • 3c1ab14 Fix misc 2
  • 306b4ec fix misc 3
  • eb042c8 refactor: simplify formatter functions in UsageChart and CartLogisticsRow
  • 337daee feat: add RequireAnyPermissionIfAuthEnabled for flexible permission checks
  • abcbe39 feat: enhance permission checks in ForecastPanel and update database migration logic
  • de32247 feat: add alerts_snoozed field to FilamentSkuSettings and update related components
  • cef2bc8 feat: add alerts_snoozed handling in ForecastRow component

📊 Changes

28 files changed (+4242 additions, -25 deletions)

View changed files

📝 backend/app/api/routes/inventory.py (+256 -2)
📝 backend/app/api/routes/settings.py (+1 -0)
📝 backend/app/core/auth.py (+89 -0)
📝 backend/app/core/database.py (+201 -0)
📝 backend/app/core/permissions.py (+7 -0)
backend/app/models/filament_sku_settings.py (+28 -0)
📝 backend/app/models/notification.py (+4 -0)
📝 backend/app/models/notification_template.py (+13 -0)
backend/app/models/shopping_list.py (+22 -0)
📝 backend/app/schemas/settings.py (+8 -0)
📝 backend/app/services/notification_service.py (+54 -0)
📝 frontend/src/__tests__/components/AddNotificationModal.test.tsx (+117 -0)
frontend/src/__tests__/components/ForecastPanelPermissions.test.tsx (+266 -0)
📝 frontend/src/__tests__/components/NotificationProviderCard.test.tsx (+2 -0)
frontend/src/__tests__/components/NotificationProviderCardStockAlerts.test.tsx (+187 -0)
📝 frontend/src/api/client.ts (+66 -0)
📝 frontend/src/components/AddNotificationModal.tsx (+27 -0)
frontend/src/components/ForecastPanel.tsx (+1822 -0)
📝 frontend/src/components/NotificationProviderCard.tsx (+33 -0)
📝 frontend/src/i18n/locales/de.ts (+124 -0)

...and 8 more files

📄 Description

Description

Adds a full Forecast & Reorder Intelligence tab to the Inventory page. The panel analyses historical filament consumption, computes statistical reorder points, projects stock runout dates, and integrates a shopping list workflow — all with permission controls and i18n coverage across 8 locales.

Fixes #1172

Documentation

Companion docs PRs (delete lines that don't apply):

Pick one:

  • Docs PR(s) linked above
  • No docs update required — reason: ___

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test addition or update

Changes Made

1. Stock Consumption Forecast Engine

Daily rate calculation — two-tier:

  • History-based (preferred, requires ≥2 print events): iterates consecutive print-event pairs,
    computing g/day per interval. Each observation is weighted by exp(−λ × age_days) where
    λ = ln(2)/30, giving a 30-day half-life so recent prints dominate. Computes weighted mean and
    weighted variance → std dev.
  • Delta-based (fallback): total_grams_used / days_since_spool_added — used when only one
    print event exists.

Per-SKU forecast object computed for every material/brand/subtype group:

effective_lead_time        = max(global_lead_time, sku_lead_time)  [min: 1 day]
statistical_safety_stock   = Z_95 (1.645) × std_dev × √(effective_lead_time)
safety_margin              = user_margin_days × daily_rate  OR  user_margin_g directly
safety_stock               = statistical_safety_stock + safety_margin
reorder_point              = daily_rate × effective_lead_time + safety_stock
days_remaining             = current_stock_g / daily_rate
days_until_ROP             = (current_stock_g − reorder_point) / daily_rate

Two alert levels:

Level Condition Meaning
reorderAlert days_until_ROP ≤ 0 Stock has dropped below reorder point — order now
stockBreakAlert days_remaining ≤ effective_lead_time Stock will run out before reorder arrives

2. Forecast Table UI

Sortable table with columns: material, stock, rate, days remaining, empty-by date, reorder-by date.
Each row expands to show:

  • Logistics strip: effective lead time, safety stock, reorder point — each with contextual hints
  • Per-SKU editable settings: lead time override and safety margin (value + unit: days or grams),
    saved on confirm via POST /inventory/sku-settings
  • Individual spool breakdown: list of each physical spool in the group with remaining weight
    and usage
  • Projected stock chart: 60-day area chart (recharts) for the top 5 materials showing stock
    trajectory, reorder point reference line, and safety stock band

3. Alert Banner

Collapsible banner above the table listing all active alerts (both tiers). Colour-coded: red for
stock-break risk, yellow for reorder-now. Hidden automatically when no alerts are present.


4. Alert Snooze per SKU

A BellOff toggle on each row (write-permission gated) mutes alerts for a specific SKU — useful
for materials being intentionally wound down or kept at low stock. When snoozed:

  • Row renders at 50% opacity
  • Alert background tint suppressed
  • Status icon hidden
  • Days-remaining text stays neutral grey
  • SKU excluded from the alert banner
  • Reorder-by date loses yellow highlight

Persisted as alerts_snoozed: bool on FilamentSkuSettings. The toggle fires an upsert
immediately, carrying the current lead time and safety margin values through unchanged.


5. Shopping List

A slide-out panel (cart icon in toolbar) for managing filament purchase intent:

  • Add to cart modal — two modes:
    • By Quantity: explicit spool count
    • By Duration: days of stock desired → auto-calculates quantity from daily rate (falls back
      to 1 if no usage data)
  • Optional note field per item
  • Item list with status badges (pending / ordered / received), inline status cycling,
    and individual delete
  • Clear all button with confirmation
  • Badge on cart button showing pending item count
  • Per-item urgency indicator: stock-break-before-replenishment warning shown inline on cart rows
    matching an active stockBreakAlert

Backend endpoints:

Method Path Description
GET /inventory/shopping-list List all items
POST /inventory/shopping-list Add item
PATCH /inventory/shopping-list/{id}/status Cycle item status
DELETE /inventory/shopping-list/{id} Remove single item
DELETE /inventory/shopping-list Clear all items

6. Global Lead Time Setting

Editable field in the panel toolbar (write-permission gated) that sets
forecast_global_lead_time_days via PUT /settings/. Acts as a floor — per-SKU overrides only
apply when they exceed the global value.


7. Permission System

Two new permissions:

Permission Controls
inventory:forecast_read View the forecast tab (tab button disabled without it)
inventory:forecast_write Edit SKU settings, lead times, safety margins, shopping list, snooze

Backward compatibility via RequireAnyPermissionIfAuthEnabled:

A new dependency factory accepts a list of permissions and passes if the user holds any of them.
All forecast write routes accept either inventory:forecast_write or the pre-existing
inventory:update, so existing users are not locked out after upgrade.

Frontend: ForecastPanel uses hasAnyPermission('inventory:forecast_write', 'inventory:update')
for canWrite, mirroring the backend logic exactly.

Startup migration: on every startup, groups that have inventory:read automatically receive
inventory:forecast_read, and groups with inventory:update automatically receive
inventory:forecast_write. Idempotent — only adds if not already present.


8. Database Schema & Migrations

filament_sku_settings

Column Type Default
id INTEGER PK
material VARCHAR(50)
subtype VARCHAR(50) NULL
brand VARCHAR(100) NULL
lead_time_days INTEGER 0
safety_margin_value INTEGER 14
safety_margin_unit VARCHAR(10) 'days'
alerts_snoozed BOOLEAN false

Unique constraint on (material, subtype, brand).

filament_shopping_list

Column Type Default
id INTEGER PK
material VARCHAR(50)
subtype VARCHAR(50) NULL
brand VARCHAR(100) NULL
quantity_spools INTEGER 1
status VARCHAR(20) 'pending'
note VARCHAR(500) NULL
created_at DATETIME now()

Startup migrations (idempotent)

  • Table creation for both tables
  • safety_margin_value / safety_margin_unit column additions
  • Legacy safety_margin_days column removal (SQLite table rebuild — ALTER TABLE DROP COLUMN
    not supported pre-3.35)
  • alerts_snoozed column addition
  • Permission backfill for existing groups

9. i18n

~50 new keys added to the forecast: namespace across all 8 locales
(en, de, fr, it, ja, pt-BR, zh-CN, zh-TW):

  • Column headers, alert text, setting labels, modal copy
  • Pluralised counts: alertCount_one/other, spoolCount_one/other,
    moreSpools_one/other, addNSpools_one/other
  • Snooze tooltip pair: alertsSnoozed / alertsEnabled

10. Tests

New test file ForecastPanelPermissions.test.tsx:

  • Forecast tab button disabled without inventory:forecast_read
  • Forecast tab button enabled with inventory:forecast_read
  • Lock screen shown when user lacks read permission
  • Forecast table renders correctly with read permission
  • Cart button visible when auth is disabled (all-pass baseline)
  • Clicking a disabled tab does not enter forecast view

Screenshots

forecast_main forecast_cart Screenshot 2026-04-30 111648

Testing

  • I have tested this on my local machine
  • I have tested with my printer model: H2C

Checklist

  • My code follows the project's coding style
  • I have commented my code where necessary
  • My changes generate no new warnings
  • I have tested my changes thoroughly

Additional Notes


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/maziggy/bambuddy/pull/1184 **Author:** [@Keybored02](https://github.com/Keybored02) **Created:** 5/1/2026 **Status:** ✅ Merged **Merged:** 5/5/2026 **Merged by:** [@maziggy](https://github.com/maziggy) **Base:** `dev` ← **Head:** `feature/stock_track` --- ### 📝 Commits (10+) - [`fbfa433`](https://github.com/maziggy/bambuddy/commit/fbfa4334fefb1bcb15d0e7bf2c289e6f11f178e3) Add stock prediction - [`0fe972e`](https://github.com/maziggy/bambuddy/commit/0fe972e37e2b0980a0e1dd43b962caa34f3ae616) Add Logistics view, testing, locales, permission compliance - [`bd5fda2`](https://github.com/maziggy/bambuddy/commit/bd5fda25fd8f9ecda6cec21597c641c47a5dfa96) Fix misc - [`3c1ab14`](https://github.com/maziggy/bambuddy/commit/3c1ab14b31c2027e133247c4d7021a4ad4fdf779) Fix misc 2 - [`306b4ec`](https://github.com/maziggy/bambuddy/commit/306b4ecdd295be0574f3b490fe4bbeeee15de010) fix misc 3 - [`eb042c8`](https://github.com/maziggy/bambuddy/commit/eb042c86883aa024d8b4df3e0cfdcec2480112bd) refactor: simplify formatter functions in UsageChart and CartLogisticsRow - [`337daee`](https://github.com/maziggy/bambuddy/commit/337daee465e24d7acdc1dece4ed62729c3ad5c6f) feat: add RequireAnyPermissionIfAuthEnabled for flexible permission checks - [`abcbe39`](https://github.com/maziggy/bambuddy/commit/abcbe39fa6e7dbf172b0d097d0cb8b2d500cf7d4) feat: enhance permission checks in ForecastPanel and update database migration logic - [`de32247`](https://github.com/maziggy/bambuddy/commit/de3224792e3054ccae2491cc91fffd77c03d3b9f) feat: add alerts_snoozed field to FilamentSkuSettings and update related components - [`cef2bc8`](https://github.com/maziggy/bambuddy/commit/cef2bc8f45942d03b3593ad6ea7d49fc4de6a1f9) feat: add alerts_snoozed handling in ForecastRow component ### 📊 Changes **28 files changed** (+4242 additions, -25 deletions) <details> <summary>View changed files</summary> 📝 `backend/app/api/routes/inventory.py` (+256 -2) 📝 `backend/app/api/routes/settings.py` (+1 -0) 📝 `backend/app/core/auth.py` (+89 -0) 📝 `backend/app/core/database.py` (+201 -0) 📝 `backend/app/core/permissions.py` (+7 -0) ➕ `backend/app/models/filament_sku_settings.py` (+28 -0) 📝 `backend/app/models/notification.py` (+4 -0) 📝 `backend/app/models/notification_template.py` (+13 -0) ➕ `backend/app/models/shopping_list.py` (+22 -0) 📝 `backend/app/schemas/settings.py` (+8 -0) 📝 `backend/app/services/notification_service.py` (+54 -0) 📝 `frontend/src/__tests__/components/AddNotificationModal.test.tsx` (+117 -0) ➕ `frontend/src/__tests__/components/ForecastPanelPermissions.test.tsx` (+266 -0) 📝 `frontend/src/__tests__/components/NotificationProviderCard.test.tsx` (+2 -0) ➕ `frontend/src/__tests__/components/NotificationProviderCardStockAlerts.test.tsx` (+187 -0) 📝 `frontend/src/api/client.ts` (+66 -0) 📝 `frontend/src/components/AddNotificationModal.tsx` (+27 -0) ➕ `frontend/src/components/ForecastPanel.tsx` (+1822 -0) 📝 `frontend/src/components/NotificationProviderCard.tsx` (+33 -0) 📝 `frontend/src/i18n/locales/de.ts` (+124 -0) _...and 8 more files_ </details> ### 📄 Description ## Description Adds a full Forecast & Reorder Intelligence tab to the Inventory page. The panel analyses historical filament consumption, computes statistical reorder points, projects stock runout dates, and integrates a shopping list workflow — all with permission controls and i18n coverage across 8 locales. ## Related Issue <!-- Link to the issue this PR addresses (if applicable) --> Fixes #1172 ## Documentation <!-- If this PR changes user-visible behavior, config keys, ports, CLI flags, URLs, or installation steps, link matching PRs in the docs repos below. Internal refactors, bug fixes with no observable change, and test-only changes are exempt — just check the "not required" box and say why. See CONTRIBUTING.md → Documentation Requirements for the full rules. --> **Companion docs PRs** (delete lines that don't apply): - Wiki: maziggy/bambuddy-wiki#22 **Pick one**: - [x] Docs PR(s) linked above - [ ] No docs update required — reason: ___ ## Type of Change <!-- Mark the relevant option with an "x" --> - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [x] Test addition or update ## Changes Made ## 1. Stock Consumption Forecast Engine **Daily rate calculation — two-tier:** - **History-based** (preferred, requires ≥2 print events): iterates consecutive print-event pairs, computing `g/day` per interval. Each observation is weighted by `exp(−λ × age_days)` where `λ = ln(2)/30`, giving a 30-day half-life so recent prints dominate. Computes weighted mean and weighted variance → std dev. - **Delta-based** (fallback): `total_grams_used / days_since_spool_added` — used when only one print event exists. **Per-SKU forecast object computed for every material/brand/subtype group:** ``` effective_lead_time = max(global_lead_time, sku_lead_time) [min: 1 day] statistical_safety_stock = Z_95 (1.645) × std_dev × √(effective_lead_time) safety_margin = user_margin_days × daily_rate OR user_margin_g directly safety_stock = statistical_safety_stock + safety_margin reorder_point = daily_rate × effective_lead_time + safety_stock days_remaining = current_stock_g / daily_rate days_until_ROP = (current_stock_g − reorder_point) / daily_rate ``` **Two alert levels:** | Level | Condition | Meaning | |---|---|---| | `reorderAlert` | `days_until_ROP ≤ 0` | Stock has dropped below reorder point — order now | | `stockBreakAlert` | `days_remaining ≤ effective_lead_time` | Stock will run out before reorder arrives | --- ## 2. Forecast Table UI Sortable table with columns: material, stock, rate, days remaining, empty-by date, reorder-by date. Each row expands to show: - **Logistics strip**: effective lead time, safety stock, reorder point — each with contextual hints - **Per-SKU editable settings**: lead time override and safety margin (value + unit: days or grams), saved on confirm via `POST /inventory/sku-settings` - **Individual spool breakdown**: list of each physical spool in the group with remaining weight and usage - **Projected stock chart**: 60-day area chart (recharts) for the top 5 materials showing stock trajectory, reorder point reference line, and safety stock band --- ## 3. Alert Banner Collapsible banner above the table listing all active alerts (both tiers). Colour-coded: red for stock-break risk, yellow for reorder-now. Hidden automatically when no alerts are present. --- ## 4. Alert Snooze per SKU A `BellOff` toggle on each row (write-permission gated) mutes alerts for a specific SKU — useful for materials being intentionally wound down or kept at low stock. When snoozed: - Row renders at 50% opacity - Alert background tint suppressed - Status icon hidden - Days-remaining text stays neutral grey - SKU excluded from the alert banner - Reorder-by date loses yellow highlight Persisted as `alerts_snoozed: bool` on `FilamentSkuSettings`. The toggle fires an upsert immediately, carrying the current lead time and safety margin values through unchanged. --- ## 5. Shopping List A slide-out panel (cart icon in toolbar) for managing filament purchase intent: - **Add to cart modal** — two modes: - *By Quantity*: explicit spool count - *By Duration*: days of stock desired → auto-calculates quantity from daily rate (falls back to 1 if no usage data) - Optional note field per item - Item list with status badges (`pending` / `ordered` / `received`), inline status cycling, and individual delete - **Clear all** button with confirmation - Badge on cart button showing pending item count - Per-item urgency indicator: stock-break-before-replenishment warning shown inline on cart rows matching an active `stockBreakAlert` **Backend endpoints:** | Method | Path | Description | |---|---|---| | `GET` | `/inventory/shopping-list` | List all items | | `POST` | `/inventory/shopping-list` | Add item | | `PATCH` | `/inventory/shopping-list/{id}/status` | Cycle item status | | `DELETE` | `/inventory/shopping-list/{id}` | Remove single item | | `DELETE` | `/inventory/shopping-list` | Clear all items | --- ## 6. Global Lead Time Setting Editable field in the panel toolbar (write-permission gated) that sets `forecast_global_lead_time_days` via `PUT /settings/`. Acts as a floor — per-SKU overrides only apply when they exceed the global value. --- ## 7. Permission System **Two new permissions:** | Permission | Controls | |---|---| | `inventory:forecast_read` | View the forecast tab (tab button disabled without it) | | `inventory:forecast_write` | Edit SKU settings, lead times, safety margins, shopping list, snooze | **Backward compatibility via `RequireAnyPermissionIfAuthEnabled`:** A new dependency factory accepts a list of permissions and passes if the user holds *any* of them. All forecast write routes accept either `inventory:forecast_write` **or** the pre-existing `inventory:update`, so existing users are not locked out after upgrade. **Frontend:** `ForecastPanel` uses `hasAnyPermission('inventory:forecast_write', 'inventory:update')` for `canWrite`, mirroring the backend logic exactly. **Startup migration:** on every startup, groups that have `inventory:read` automatically receive `inventory:forecast_read`, and groups with `inventory:update` automatically receive `inventory:forecast_write`. Idempotent — only adds if not already present. --- ## 8. Database Schema & Migrations ### `filament_sku_settings` | Column | Type | Default | |---|---|---| | `id` | INTEGER PK | — | | `material` | VARCHAR(50) | — | | `subtype` | VARCHAR(50) | NULL | | `brand` | VARCHAR(100) | NULL | | `lead_time_days` | INTEGER | `0` | | `safety_margin_value` | INTEGER | `14` | | `safety_margin_unit` | VARCHAR(10) | `'days'` | | `alerts_snoozed` | BOOLEAN | `false` | Unique constraint on `(material, subtype, brand)`. ### `filament_shopping_list` | Column | Type | Default | |---|---|---| | `id` | INTEGER PK | — | | `material` | VARCHAR(50) | — | | `subtype` | VARCHAR(50) | NULL | | `brand` | VARCHAR(100) | NULL | | `quantity_spools` | INTEGER | `1` | | `status` | VARCHAR(20) | `'pending'` | | `note` | VARCHAR(500) | NULL | | `created_at` | DATETIME | `now()` | ### Startup migrations (idempotent) - Table creation for both tables - `safety_margin_value` / `safety_margin_unit` column additions - Legacy `safety_margin_days` column removal (SQLite table rebuild — `ALTER TABLE DROP COLUMN` not supported pre-3.35) - `alerts_snoozed` column addition - Permission backfill for existing groups --- ## 9. i18n ~50 new keys added to the `forecast:` namespace across all 8 locales (en, de, fr, it, ja, pt-BR, zh-CN, zh-TW): - Column headers, alert text, setting labels, modal copy - Pluralised counts: `alertCount_one/other`, `spoolCount_one/other`, `moreSpools_one/other`, `addNSpools_one/other` - Snooze tooltip pair: `alertsSnoozed` / `alertsEnabled` --- ## 10. Tests New test file `ForecastPanelPermissions.test.tsx`: - Forecast tab button disabled without `inventory:forecast_read` - Forecast tab button enabled with `inventory:forecast_read` - Lock screen shown when user lacks read permission - Forecast table renders correctly with read permission - Cart button visible when auth is disabled (all-pass baseline) - Clicking a disabled tab does not enter forecast view ## Screenshots <!-- If applicable, add screenshots to demonstrate your changes --> <img width="1640" height="990" alt="forecast_main" src="https://github.com/user-attachments/assets/25d3b9c8-6e7f-41f7-a8ed-f1b0ada1895b" /> <img width="1625" height="524" alt="forecast_cart" src="https://github.com/user-attachments/assets/4b12e61e-2857-4e73-a14a-4852c52ea5fd" /> <img width="1635" height="259" alt="Screenshot 2026-04-30 111648" src="https://github.com/user-attachments/assets/dc4f5f2c-8863-44d0-a437-82b467adc824" /> ## Testing <!-- Describe how you tested your changes --> - [x] I have tested this on my local machine - [x] I have tested with my printer model: H2C <!-- e.g., X1C, P1S, A1 --> ## Checklist - [x] My code follows the project's coding style - [x] I have commented my code where necessary - [x] My changes generate no new warnings - [x] I have tested my changes thoroughly ## Additional Notes <!-- Add any additional information that reviewers should know --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 12:35:28 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/bambuddy#1168
No description provided.