113 files
This commit is contained in:
1 parent
0533eaca8d
commit
15c0e35c18
113 files changed
+12448
-1673
No files matched your search
@@ -823,6 +823,14 @@ async def ws_import_json(
|
||||
for pid, stock_val in part_stocks.items():
|
||||
store_new.set_part_stock(pid, stock_val)
|
||||
await store_new.async_save()
|
||||
# Restored stocks can sit below min_stock — reconcile buy
|
||||
# tasks like every other stock mutation does, or the
|
||||
# shopping list stays silent until the next unrelated
|
||||
# stock change (bug audit 2026-08-22).
|
||||
from ..parts_runtime import schedule_buy_task_reconcile
|
||||
|
||||
if new_entry is not None:
|
||||
schedule_buy_task_reconcile(hass, new_entry)
|
||||
|
||||
# (roadmap P6) recreate document metadata + web-links for the object
|
||||
# (blobs travel via the /config backup; a JSON-only import leaves
|
||||
|
||||
@@ -57,13 +57,18 @@ from . import (
|
||||
# the per-part DELTA against the entry's previous used_parts; None (or
|
||||
# []) clears the consumption and returns the old quantities to stock.
|
||||
vol.Optional("used_parts"): vol.Any(
|
||||
[
|
||||
{
|
||||
vol.Required("part_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
vol.Optional("quantity"): vol.All(vol.Coerce(float), vol.Range(min=0.01, max=999)),
|
||||
vol.Optional("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
}
|
||||
],
|
||||
vol.All(
|
||||
[
|
||||
{
|
||||
vol.Required("part_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
vol.Optional("quantity"): vol.All(vol.Coerce(float), vol.Range(min=0.01, max=999)),
|
||||
vol.Optional("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
|
||||
}
|
||||
],
|
||||
# Same cap as the live completion path (tasks_actions.py) —
|
||||
# the edit path must not accept what completion refuses.
|
||||
vol.Length(max=10),
|
||||
),
|
||||
None,
|
||||
),
|
||||
}
|
||||
@@ -94,18 +99,27 @@ async def ws_update_history_entry(
|
||||
|
||||
# Validate new timestamp format up front so we don't half-mutate
|
||||
if "timestamp" in msg:
|
||||
new_ts = msg["timestamp"]
|
||||
try:
|
||||
dt_util.parse_datetime(new_ts)
|
||||
if dt_util.parse_datetime(new_ts) is None:
|
||||
raise ValueError("not a datetime")
|
||||
except (ValueError, TypeError):
|
||||
parsed_ts = dt_util.parse_datetime(msg["timestamp"])
|
||||
if parsed_ts is None:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
"invalid_date",
|
||||
"timestamp must be an ISO datetime string",
|
||||
)
|
||||
return
|
||||
# Mirror the completed_at choke point (coordinator): a naive value
|
||||
# means local time, and a FUTURE moment is refused — the anchor
|
||||
# recompute below would otherwise push next_due into the future
|
||||
# (bug audit 2026-08-22; the backfill path already rejected this).
|
||||
if parsed_ts.tzinfo is None:
|
||||
parsed_ts = parsed_ts.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE)
|
||||
if parsed_ts > dt_util.now():
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
"invalid_date",
|
||||
"timestamp cannot be in the future",
|
||||
)
|
||||
return
|
||||
|
||||
# Locate the entry by its original timestamp — first match wins.
|
||||
target_index: int | None = None
|
||||
@@ -144,6 +158,11 @@ async def ws_update_history_entry(
|
||||
from . import _get_merged_tasks
|
||||
|
||||
old_used = patched.get("used_parts") or []
|
||||
# Deliberately NOT sanitize_consumes_parts here: an edited entry may
|
||||
# reference a part that has since been deleted, and that link must
|
||||
# stay RECORDED (stock math skips it) — dropping unknown ids would
|
||||
# rewrite history. Field validation (ids, quantity range, list cap)
|
||||
# is the schema's job above.
|
||||
new_used = msg["used_parts"] or []
|
||||
task_data = _get_merged_tasks(entry).get(task_id) or {}
|
||||
enriched = await async_apply_history_parts_edit(hass, entry, task_data, old_used, new_used)
|
||||
|
||||
Reference in new issue
Block a user