wrolpi/modules/inventory/common.py

316 lines
14 KiB
Python
Raw Normal View History

Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
"""Config-only inventory backend.
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
Each inventory is a single YAML file at ``config/inventory/<slug>.yaml`` and is the source-of-truth (no database).
All files are loaded into one shared, cross-worker dict keyed by slug. Unit math/aggregation happens in the
frontend (mathjs), so this module treats item field values as opaque strings/numbers.
"""
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional
from wrolpi.common import logger, MultiFileConfig, read_config_data
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
from wrolpi.errors import ValidationError
from .defaults import (DEFAULT_INVENTORIES, FIELD_TYPES, default_fields, slugify)
MY_DIR: Path = Path(__file__).parent
logger = logger.getChild(__name__)
@dataclass
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
class InventoryConfigValidator:
version: int = None
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
slug: str = None
name: str = None
type: str = None
created_at: str = None
viewed_at: str = None
fields: list = field(default_factory=list)
items: list = field(default_factory=list)
def _validate_fields(fields_: list):
"""Validate a field schema list. Raises ValidationError on bad input."""
if not isinstance(fields_, list):
raise ValidationError('fields must be a list')
seen = set()
for f in fields_:
if not isinstance(f, dict) or not f.get('key') or not f.get('type'):
raise ValidationError(f'Invalid field definition: {f}')
if f['type'] not in FIELD_TYPES:
raise ValidationError(f'Unknown field type: {f["type"]}')
if f['key'] in seen:
raise ValidationError(f'Duplicate field key: {f["key"]}')
seen.add(f['key'])
def _allowed_item_keys(fields_: list) -> set:
"""Keys an item may contain, derived from the field schema (quantity fields add a companion `<key>_unit`)."""
keys = set()
for f in fields_:
keys.add(f['key'])
if f.get('type') == 'quantity':
keys.add(f'{f["key"]}_unit')
return keys
def _normalize_items(items: list, fields_: list) -> list:
"""Filter each item to the schema's allowed keys, strip whitespace from string values, and assign stable ids (fresh id for any missing/duplicate)."""
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
if not isinstance(items, list):
raise ValidationError('items must be a list')
allowed = _allowed_item_keys(fields_)
counter = max((i.get('id') for i in items if isinstance(i.get('id'), int)), default=0)
used = set()
normalized = []
for raw in items:
clean = {k: (v.strip() if isinstance(v, str) else v) for k, v in raw.items() if k in allowed}
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
item_id = raw.get('id')
if not isinstance(item_id, int) or item_id in used:
counter += 1
item_id = counter
used.add(item_id)
clean['id'] = item_id
normalized.append(clean)
return normalized
# Slugs reserved in the config/inventory/ directory that are NOT inventories. The shared food catalog lives at
# config/inventory/catalog.yaml, so it must be excluded from inventory discovery (and from new-inventory slugs).
RESERVED_SLUGS = {'catalog'}
class InventoriesConfig(MultiFileConfig):
subdirectory = 'inventory'
name = 'inventory'
validator = InventoryConfigValidator
default_entity = dict(version=0, slug=None, name=None, type='food', created_at=None, viewed_at=None,
fields=[], items=[])
def discover(self) -> List[str]:
# Exclude reserved files (e.g. catalog.yaml) that share the directory but are not inventories.
return [slug for slug in super().discover() if slug not in RESERVED_SLUGS]
def all_inventories(self) -> List[dict]:
"""Return every inventory in full (fields + items), sorted by name.
The frontend loads this once and derives everything (selected items, summary, locations, ration data)
client-side, then persists whole inventories via `save_inventory` — so there are no granular item/field
endpoints.
"""
return sorted(self.all(), key=lambda i: (i.get('name') or '').lower())
def get_inventory(self, slug: str) -> Optional[dict]:
return self.get(slug)
def create_inventory(self, name: str, inventory_type: str) -> dict:
from wrolpi.dates import now
name = (name or '').strip()
if not name:
raise ValidationError('Inventory name is required')
slug = slugify(name)
# Slug is a stable identifier; dedupe it so two same-named inventories don't collide on disk, and never
# let a user-created inventory claim a reserved slug (e.g. naming an inventory "Catalog").
taken = lambda s: s in self._configs or s in RESERVED_SLUGS
if taken(slug):
suffix = 2
while taken(f'{slug}-{suffix}'):
suffix += 1
slug = f'{slug}-{suffix}'
created = now().isoformat()
self.create(slug, dict(
slug=slug,
name=name,
type=inventory_type,
created_at=created,
viewed_at=created,
fields=default_fields(inventory_type),
items=[],
))
return self.get(slug)
def delete_inventory(self, slug: str):
if slug not in self._configs:
raise ValidationError(f'No inventory: {slug}')
self.delete(slug)
def save_inventory(self, slug: str, data: dict, expected_version: int = None) -> dict:
"""Replace an inventory's editable parts (name / fields / items) in one write.
`slug` is a stable id and is never changed by a save (only the display `name` changes). Item ids are
normalized server-side (any item missing/duplicating an id gets a fresh one). When `expected_version` is
given and does not match the stored version, an `InventoryConflict` is raised so a stale client (e.g. a
second browser tab) cannot silently clobber a newer save.
"""
from wrolpi.dates import now
from .errors import InventoryConflict
if slug not in self._configs:
raise ValidationError(f'No inventory: {slug}')
# Hold the save lock for the whole read-check-mutate-persist cycle so two concurrent saves of the same
# inventory can't both pass the version check and clobber each other (lost write).
with self.save_lock():
current = dict(self._configs[slug])
stored_version = current.get('version')
if expected_version is not None and stored_version is not None and stored_version != expected_version:
raise InventoryConflict(
f'Inventory {slug} changed since it was loaded (stored {stored_version} != {expected_version})')
if 'name' in data:
name = (data['name'] or '').strip()
if not name:
raise ValidationError('Inventory name is required')
current['name'] = name
if data.get('type'):
current['type'] = data['type']
if 'fields' in data:
fields_ = data['fields']
_validate_fields(fields_)
for index, f in enumerate(fields_):
f['order'] = index
current['fields'] = fields_
if 'items' in data:
current['items'] = _normalize_items(data['items'], current.get('fields') or [])
current['viewed_at'] = now().isoformat()
self._configs[slug] = current
# Persist synchronously (lock already held) so the version bump is immediate and returned to the client.
self._persist_slug(slug)
return self.get(slug)
def _load_backup(self, slug: str, backup_date: str) -> dict:
"""Read and validate a dated backup file for an inventory."""
file = self._get_backup_file(slug, backup_date)
if not file.is_file():
raise ValidationError(f'No backup for {slug} on {backup_date}')
data = read_config_data(file)
if not self.is_valid(data):
raise ValidationError(f'Backup is not a valid inventory config: {file}')
return data
def preview_restore(self, slug: str, backup_date: str, mode: str) -> dict:
"""Describe what restoring a backup would change, without applying it.
`overwrite` replaces the whole inventory (items present only in the backup are added, items present only in
the current inventory are removed, and the field schema may change). `merge` only adds backup items whose
id isn't already present (current fields and items are kept).
"""
if mode not in ('merge', 'overwrite'):
raise ValidationError('mode must be "merge" or "overwrite"')
if slug not in self._configs:
raise ValidationError(f'No inventory: {slug}')
backup = self._load_backup(slug, backup_date)
current = self.get(slug)
current_items = current.get('items') or []
backup_items = backup.get('items') or []
current_ids = {i.get('id') for i in current_items}
backup_ids = {i.get('id') for i in backup_items}
add = [i for i in backup_items if i.get('id') not in current_ids]
if mode == 'overwrite':
remove = [i for i in current_items if i.get('id') not in backup_ids]
unchanged = sum(1 for i in current_items if i.get('id') in backup_ids)
fields_change = (current.get('fields') or []) != (backup.get('fields') or [])
else:
remove = []
unchanged = len(current_items)
fields_change = False
return dict(
mode=mode,
add=add,
remove=remove,
unchanged=unchanged,
fields_change=fields_change,
current_count=len(current_items),
backup_count=len(backup_items),
backup_name=backup.get('name'),
)
def apply_restore(self, slug: str, backup_date: str, mode: str) -> dict:
"""Restore an inventory from a dated backup (see `preview_restore` for `merge`/`overwrite` semantics).
The pre-restore state is backed up by `_persist_slug`, so a restore can itself be undone."""
from wrolpi.dates import now
if mode not in ('merge', 'overwrite'):
raise ValidationError('mode must be "merge" or "overwrite"')
if slug not in self._configs:
raise ValidationError(f'No inventory: {slug}')
backup = self._load_backup(slug, backup_date)
with self.save_lock():
current = dict(self._configs[slug])
if mode == 'overwrite':
# Replace the editable parts with the backup; `slug`/`version` stay (version bumps on persist).
current['name'] = backup.get('name') or current.get('name')
current['type'] = backup.get('type') or current.get('type')
current['fields'] = backup.get('fields') or []
# Validate the field dicts (a corrupt backup field without a 'key' would otherwise raise a 500
# inside _normalize_items); raises a 400 ValidationError instead.
_validate_fields(current['fields'])
current['items'] = _normalize_items(backup.get('items') or [], current['fields'])
else:
current_items = current.get('items') or []
current_ids = {i.get('id') for i in current_items}
add = [i for i in (backup.get('items') or []) if i.get('id') not in current_ids]
current['items'] = _normalize_items(current_items + add, current.get('fields') or [])
current['viewed_at'] = now().isoformat()
self._configs[slug] = current
self._persist_slug(slug)
return self.get(slug)
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
def seed_defaults(self):
"""Create the default example inventories if the directory is empty."""
if self._configs:
return
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
for default in DEFAULT_INVENTORIES:
try:
Add a recommended Food Storage example, a Ration tab, and a supply planner Build on the rebuilt config-only Inventory with three related additions. Recommended example inventory: - A fresh install now seeds the "Food Storage" example with the recommended one-adult / one-year (~3,000 kcal/day) emergency food supply (60 items, ~1.12M kcal), instead of an empty inventory. - The counts are layered on top of the shipped food catalog (single source of per-package nutrition) via RECOMMENDED_ONE_YEAR_FOOD_COUNTS, joined at seed time by recommended_food_storage_items(). - Genericized the two remaining brand names in the catalog defaults (Velveeta -> Processed Cheese, Spam -> Canned Lunch Meat) so both the catalog and the seeded inventory stay brand-free. Ration tab (split out of Summary): - Moved the Ration Estimate (People / Daily Calories / estimate table) out of the increasingly long Summary tab into its own "Ration" tab, shown only for inventories that have a calories field. Supply planner ("Plan More Supply"): - A slider picks a target duration longer than the current estimate; the whole inventory is scaled up proportionally (every item by the same factor, mix kept balanced) into a sortable shopping list of what to buy. - Any split package rounds UP to a whole one (ceil), matching the "1.6 buckets -> buy 1 more" rule. - The shopping list is exportable as CSV (reusing toCSV) and PDF (reusing the print-to-PDF path, scoped via body.printing-shopping so only the list prints -- no inventory or summary). Search filter: - Moved the items search out of the top toolbar (which implied it filtered every tab) into the Items tab, so it clearly narrows Items only; Summary/Ration/Export/print always use the full inventory. Aggregation/role helpers (findNameKey, planSupplyPurchase) live in summarize.js, the single source of truth shared by Summary, export, and the ration views. All new pure functions and UI flows are covered by tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:39:58 -06:00
inventory = self.create_inventory(default['name'], default['type'])
# Optionally populate the example with seed items (e.g. the recommended one-year food supply).
items_factory = default.get('items_factory')
items = items_factory() if items_factory else None
if items:
self.save_inventory(inventory['slug'], dict(items=items))
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
except Exception as e:
logger.warning(f'Failed to seed default inventory {default}: {e}')
2022-02-16 21:57:27 -07:00
INVENTORIES_CONFIG: InventoriesConfig = InventoriesConfig()
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
TEST_INVENTORIES_CONFIG: Optional[InventoriesConfig] = None
2022-02-16 21:57:27 -07:00
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
def get_inventory_configs() -> InventoriesConfig:
if isinstance(TEST_INVENTORIES_CONFIG, InventoriesConfig):
2022-02-16 21:57:27 -07:00
return TEST_INVENTORIES_CONFIG
return INVENTORIES_CONFIG
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
# Backwards-compatible alias used by some call sites.
get_inventories_config = get_inventory_configs
2022-02-16 21:57:27 -07:00
def set_test_inventories_config(enabled: bool):
global TEST_INVENTORIES_CONFIG
if enabled:
TEST_INVENTORIES_CONFIG = InventoriesConfig()
else:
TEST_INVENTORIES_CONFIG = None
def import_inventories_config():
Rebuild Inventory as a config-only system with food catalog and calculators Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:20:10 -06:00
"""Migrate any legacy inventories.yaml, load per-inventory files, seed defaults, and load the food catalog."""
from .migrate import migrate_legacy_inventory
config = get_inventory_configs()
try:
migrate_legacy_inventory(config)
except Exception as e:
logger.error('Failed to migrate legacy inventories', exc_info=e)
config.import_all()
config.seed_defaults()
# Load the shared food catalog and merge in any new shipped defaults.
try:
from .catalog import import_catalog_config
import_catalog_config()
except Exception as e:
logger.error('Failed to import food catalog', exc_info=e)