| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| README.md | 2026-05-07 | 3.0 kB | |
| v2.9.18 -- 3-way invoice match + risk owner picker + Gantt resize source code.tar.gz | 2026-05-07 | 458.7 MB | |
| v2.9.18 -- 3-way invoice match + risk owner picker + Gantt resize source code.zip | 2026-05-07 | 460.4 MB | |
| Totals: 3 Items | 919.1 MB | 0 | |
Three independent improvements landed in one batch.
Procurement — 3-way invoice match
create_invoice_from_po previously copied PO line items verbatim into Invoices with no link to confirmed Goods Receipts — overbilling unblocked. Now procurement/service._validate_3way_match sums quantity_received over confirmed-status GRs per po_item_id; for each proposed Invoice line it compares against received_qty and reports per-line violations. The route raises HTTP 422 with structured {message, errors: [...]} when invoice qty exceeds received qty.
force=true query param overrides for service-only POs (no goods to physically receive). When set, the invoice persists with metadata_['force_3way_match'] = True and a structured logger.warning is emitted with {po_id, user_id, violations} for audit aggregation.
A no_confirmed_grs sentinel reason is returned when a PO has zero confirmed GRs and the invoice has any positive qty — lets the FE render the precise "service-only PO? pass force=true" message.
All 19 existing procurement tests still pass.
Risk — owner picker + assignment notification
Risk.owner_user_id (nullable FK to oe_users_user, indexed, ON DELETE SET NULL) replaces the free-text owner_name for new rows; legacy data still renders via the fallback. Migration v2918_risk_owner_user_id.py is inspector-guarded (uses batch_alter_table for SQLite compatibility).
risk.assigned event now fires on new or changed owner. New subscriber _on_risk_assigned in notifications/events.py produces an in-app notification "You were assigned risk {{code}}: {{title}}".
Frontend swapped the free-text input for the existing UserSearchInput (avatar + name + role + email search). On selection, owner_user_id is sent as UUID and the resolved display name mirrored into owner_name so the list-row column keeps working uniformly across picker-sourced and legacy rows.
Schedule — Gantt drag-to-resize
Two transparent 7 px ew-resize zones (left + right) on every standard task bar; left handle moves start_date, right handle moves end_date. Snap-to-day via the existing pxToDate; clamped to keep end_date >= start_date + 1 day. Group and milestone bars are unaffected. Esc cancels the in-flight resize and reverts the preview.
onActivityResize?: (id, newStart, newEnd) => void prop added to GanttChart. Existing onActivityMove (drag-to-translate) untouched; resize is purely additive.
SchedulePage wires it through React Query: new resizeActivity mutation calls scheduleApi.updateActivity(id, {start_date, end_date}), optimistically updates the cached ['gantt', schedule.id] payload, snapshots and reverts on error.
See [CHANGELOG.md](https://github.com/datadrivenconstruction/OpenConstructionERP/blob/v2.9.18/CHANGELOG.md#2918 for the full list.