fix(quotes): derive decision.project_id from accept item_projects
Multi-project quotes: the manager picks one project per line item in the accept modal (item_projects), but accept_quote only auto-filled decision.project_id from existing line_item_projects (≤1). For items without a single existing line-project the decision stayed NULL, so / pm/on-the-way (filtered by ItemDecision.project_id) never showed those items to any project manager — they accumulated only in the global admin list. Now derivation order is: explicit decision.project_id → payload.item_projects (single) → existing line_item_projects (single) → NULL (office-only, never misattributed). Verified E2E: two items of one quote accepted to two projects appear in exactly their own PM's on-the-way.
This commit is contained in:
@@ -416,6 +416,9 @@ async def accept_quote(db: AsyncSession, quote_id: UUID, payload: AcceptQuoteIn,
|
|||||||
|
|
||||||
# --- process item decisions (UPSERT) ---
|
# --- process item decisions (UPSERT) ---
|
||||||
chosen_ts_ids: set[UUID] = set()
|
chosen_ts_ids: set[UUID] = set()
|
||||||
|
# پروژههای ارسالی در payload.item_projects — منبع اصلی برای
|
||||||
|
# decision.project_id (مدیر تدارکات برای هر قلم یک پروژه انتخاب میکند)
|
||||||
|
proj_by_line: dict[UUID, list[UUID]] = {e.line_item_id: list(e.project_ids) for e in payload.item_projects}
|
||||||
for decision_in in payload.item_decisions:
|
for decision_in in payload.item_decisions:
|
||||||
decision = await db.scalar(
|
decision = await db.scalar(
|
||||||
select(ItemDecision).where(ItemDecision.line_item_id == decision_in.line_item_id).with_for_update()
|
select(ItemDecision).where(ItemDecision.line_item_id == decision_in.line_item_id).with_for_update()
|
||||||
@@ -460,22 +463,31 @@ async def accept_quote(db: AsyncSession, quote_id: UUID, payload: AcceptQuoteIn,
|
|||||||
decision.adjusted_unit_price = decision_in.adjusted_unit_price
|
decision.adjusted_unit_price = decision_in.adjusted_unit_price
|
||||||
decision.adjusted_notes = decision_in.adjusted_notes
|
decision.adjusted_notes = decision_in.adjusted_notes
|
||||||
decision.decided_at = utcnow()
|
decision.decided_at = utcnow()
|
||||||
# v2.6: the SINGLE purchased project (proc manager picks it;
|
# v2.6: the SINGLE purchased project (proc manager picks it).
|
||||||
# auto-filled when the offer had exactly one project).
|
# Derivation order:
|
||||||
|
# 1) explicit decision_in.project_id
|
||||||
|
# 2) payload.item_projects for this line item (exactly one)
|
||||||
|
# 3) existing line_item_projects (exactly one)
|
||||||
|
# otherwise NULL — the item stays visible only to the office
|
||||||
|
# until a project is assigned (never wrongly visible to a PM).
|
||||||
if decision_in.project_id is not None:
|
if decision_in.project_id is not None:
|
||||||
decision.project_id = decision_in.project_id
|
decision.project_id = decision_in.project_id
|
||||||
else:
|
else:
|
||||||
line_projs = (
|
accept_projs = proj_by_line.get(decision_in.line_item_id) or []
|
||||||
(await db.execute(
|
if len(accept_projs) == 1:
|
||||||
select(LineItemProjects.project_id).where(
|
decision.project_id = accept_projs[0]
|
||||||
LineItemProjects.line_item_id == decision_in.line_item_id
|
else:
|
||||||
)
|
line_projs = (
|
||||||
))
|
(await db.execute(
|
||||||
.scalars()
|
select(LineItemProjects.project_id).where(
|
||||||
.all()
|
LineItemProjects.line_item_id == decision_in.line_item_id
|
||||||
)
|
)
|
||||||
if len(line_projs) == 1:
|
))
|
||||||
decision.project_id = line_projs[0]
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if len(line_projs) == 1:
|
||||||
|
decision.project_id = line_projs[0]
|
||||||
if decision.project_id is not None:
|
if decision.project_id is not None:
|
||||||
if await db.get(Project, decision.project_id) is None:
|
if await db.get(Project, decision.project_id) is None:
|
||||||
raise NotFoundError("پروژه یافت نشد", field="item_decisions.project_id")
|
raise NotFoundError("پروژه یافت نشد", field="item_decisions.project_id")
|
||||||
|
|||||||
Reference in New Issue
Block a user