T8.5 - CR-013/D6: the material request is structure, not features
The OneNote comparison from the meeting was "word vomit"; the structure that replaces it, built at the lightweight scope EXACTLY as approved Aug 14: - Line items (qty, unit, description) added, edited, removed. Descriptions offer the D6 project list through a datalist - which is also precisely what keeps free text working when no list is loaded, the state every project is in today. Picking a listed material fills its unit; nothing locks. - Needed-by date, requestor (the signed-in account), delivery location (T8.4's fields on this package, composed), and an explicit status set (Requested / Filled / Declined). The request rides on the package record (data.materialRequests) - server-persisted through the same upsert as everything else, never localStorage. - Submitting notifies the warehouse owner named on the package (CR-010) - the routing that replaces the funnel through one person - through the T7.6 gate, with the count, the needed-by, the delivery location and the deep link, in the house convention. material_requested lands in the audit history. - The dashboard grows a Material requests queue, filterable by status and by delivery location. - The block lives inside #material-card, so the CR-006 materials toggle governs it with no special casing. The whole flow is driven at 390px - requests originate in the field. - NO parts catalog, no inventory count, no warehouse integration - the probe greps the block for them. One infrastructure bug fixed in passing detection (not silently): T8.5's dashboard-panel insert matched the substring inside "async function dashIssue", splitting the async keyword from its function - the creator failed to parse and every boot died. Caught by the probe's first run; anchored fixes now restore both halves. Verification (each probe run alone): NEW tests/mreq_check.py 19/19 (request end-to-end at 390px against the SMTP sink, dashboard filters, fences). Regressions: frame_check 39/39, sections_check 95/95, kitting_check 26/26. Items: CR-013, D6 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1732,6 +1732,22 @@ def kitting_body(user: "models.User", wp: "models.WorkPackage", actor: "models.U
|
||||
)
|
||||
|
||||
|
||||
def material_request_body(user: "models.User", wp: "models.WorkPackage",
|
||||
actor: "models.User", n_lines: int, needed: str,
|
||||
delivery: str, link: str) -> str:
|
||||
who = actor.full_name or actor.username
|
||||
name = user.full_name or user.username
|
||||
needed_line = f" needed by {needed}" if needed else ""
|
||||
return (
|
||||
f"Hi {name},\n\n"
|
||||
f"{who} raised a material request on {wp.number or 'a work package'}: "
|
||||
f"{n_lines} line{'' if n_lines == 1 else 's'}{needed_line}.\n"
|
||||
f"Delivery location: {delivery}.\n\n"
|
||||
f"Open it here:\n{link}\n\n"
|
||||
f"— This is an automated message from the Work Package Suite."
|
||||
)
|
||||
|
||||
|
||||
def notify_kitting_change(db: Session, wp: "models.WorkPackage", actor: "models.User",
|
||||
old_status: str, new_status: str) -> list:
|
||||
"""CR-011: the package's distribution list plus its warehouse owner (CR-010's
|
||||
@@ -1929,6 +1945,33 @@ def upsert_wp(body: WpIn, background_tasks: BackgroundTasks, user: models.User =
|
||||
summary=(wp.number or wp.subject or wp.id),
|
||||
detail={"from": _kit_old, "to": _kit_new})
|
||||
notifs.extend(notify_kitting_change(db, wp, user, _kit_old, _kit_new))
|
||||
# CR-013 / T8.5: a new material request notifies the warehouse owner named
|
||||
# on the package (CR-010) - the routing that replaces the informal funnel
|
||||
# through one person. Same gate, same outbox, same link discipline.
|
||||
if not is_new:
|
||||
_mr_old = [r for r in ((old_data or {}).get("materialRequests") or []) if isinstance(r, dict)]
|
||||
_mr_new = [r for r in ((wp.data or {}).get("materialRequests") or []) if isinstance(r, dict)]
|
||||
if len(_mr_new) > len(_mr_old):
|
||||
fresh = _mr_new[len(_mr_old):]
|
||||
for req in fresh:
|
||||
log_event(db, user, "material_requested", "wp", wp.id,
|
||||
project_id=wp.project_id,
|
||||
summary=(wp.number or wp.subject or wp.id),
|
||||
detail={"lines": len(req.get("items") or []),
|
||||
"neededBy": str(req.get("neededBy") or "")[:40]})
|
||||
ko = (wp.data or {}).get("kitOwnerId")
|
||||
owner = db.get(models.User, ko) if isinstance(ko, str) and ko else None
|
||||
if owner and owner.is_active and owner.id != user.id:
|
||||
link = wp_link(db, wp)
|
||||
n_lines = sum(len(r.get("items") or []) for r in fresh)
|
||||
needed = str(fresh[-1].get("neededBy") or "").strip()
|
||||
deliv = str(fresh[-1].get("deliveryLoc") or "").strip() or "not set"
|
||||
notifs.append(notify.enqueue(
|
||||
db, user=owner, kind="material_requested",
|
||||
subject=f"Material request: {wp.number or 'work package'}",
|
||||
body=material_request_body(owner, wp, user, n_lines, needed, deliv, link),
|
||||
link=link, wp_id=wp.id, project_id=wp.project_id,
|
||||
))
|
||||
# Notify a newly-assigned owner (skip self-assignment).
|
||||
notif = None
|
||||
if new_assignee and new_assignee != old_assignee and new_assignee != user.id:
|
||||
|
||||
Reference in New Issue
Block a user