BE-PR-008: add Proposal Accept tracking fields (source_proposal_id, source_essential_id)
- Add source_proposal_id and source_essential_id FK columns to Task model - Populate tracking fields during Proposal Accept task generation - Add generated_tasks relationship on Proposal model for reverse lookup - Expose source_proposal_id/source_essential_id in TaskResponse schema - Add GeneratedTaskBrief schema and include generated_tasks in ProposalDetailResponse - Proposal detail endpoint now returns generated story tasks with status
This commit is contained in:
@@ -63,6 +63,26 @@ def _serialize_proposal(db: Session, proposal: Proposal, *, include_essentials:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
result["essentials"] = [_serialize_essential(e) for e in essentials]
|
result["essentials"] = [_serialize_essential(e) for e in essentials]
|
||||||
|
|
||||||
|
# BE-PR-008: include tasks generated from this Proposal via Accept
|
||||||
|
gen_tasks = (
|
||||||
|
db.query(Task)
|
||||||
|
.filter(Task.source_proposal_id == proposal.id)
|
||||||
|
.order_by(Task.id.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
result["generated_tasks"] = [
|
||||||
|
{
|
||||||
|
"task_id": t.id,
|
||||||
|
"task_code": t.task_code,
|
||||||
|
"task_type": t.task_type or "story",
|
||||||
|
"task_subtype": t.task_subtype,
|
||||||
|
"title": t.title,
|
||||||
|
"status": t.status.value if hasattr(t.status, "value") else t.status,
|
||||||
|
"source_essential_id": t.source_essential_id,
|
||||||
|
}
|
||||||
|
for t in gen_tasks
|
||||||
|
]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -327,6 +347,9 @@ def accept_proposal(
|
|||||||
reporter_id=proposal.created_by_id or current_user.id,
|
reporter_id=proposal.created_by_id or current_user.id,
|
||||||
created_by_id=proposal.created_by_id or current_user.id,
|
created_by_id=proposal.created_by_id or current_user.id,
|
||||||
task_code=task_code,
|
task_code=task_code,
|
||||||
|
# BE-PR-008: track which Proposal/Essential generated this task
|
||||||
|
source_proposal_id=proposal.id,
|
||||||
|
source_essential_id=essential.id,
|
||||||
)
|
)
|
||||||
db.add(task)
|
db.add(task)
|
||||||
db.flush() # materialise task.id
|
db.flush() # materialise task.id
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ class Proposal(Base):
|
|||||||
lazy="select",
|
lazy="select",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# BE-PR-008: reverse lookup — story tasks generated from this Proposal
|
||||||
|
generated_tasks = relationship(
|
||||||
|
"Task",
|
||||||
|
foreign_keys="Task.source_proposal_id",
|
||||||
|
lazy="select",
|
||||||
|
viewonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
# ---- convenience alias ------------------------------------------------
|
# ---- convenience alias ------------------------------------------------
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def proposal_code(self) -> str | None:
|
def proposal_code(self) -> str | None:
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ class Task(Base):
|
|||||||
assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
|
|
||||||
|
# Proposal Accept tracking (BE-PR-008)
|
||||||
|
# When a task is generated from Proposal Accept, these record the source.
|
||||||
|
source_proposal_id = Column(
|
||||||
|
Integer, ForeignKey("proposes.id"), nullable=True,
|
||||||
|
comment="Proposal that generated this task via accept (NULL if manually created)",
|
||||||
|
)
|
||||||
|
source_essential_id = Column(
|
||||||
|
Integer, ForeignKey("essentials.id"), nullable=True,
|
||||||
|
comment="Essential that generated this task via accept (NULL if manually created)",
|
||||||
|
)
|
||||||
|
|
||||||
# Tags (comma-separated)
|
# Tags (comma-separated)
|
||||||
tags = Column(String(500), nullable=True)
|
tags = Column(String(500), nullable=True)
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class TaskResponse(TaskBase):
|
|||||||
resolution_summary: Optional[str] = None
|
resolution_summary: Optional[str] = None
|
||||||
positions: Optional[str] = None
|
positions: Optional[str] = None
|
||||||
pending_matters: Optional[str] = None
|
pending_matters: Optional[str] = None
|
||||||
|
# BE-PR-008: Proposal Accept tracking
|
||||||
|
source_proposal_id: Optional[int] = None
|
||||||
|
source_essential_id: Optional[int] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
@@ -344,10 +347,22 @@ class EssentialResponse(EssentialBase):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratedTaskBrief(BaseModel):
|
||||||
|
"""Brief info about a story task generated from Proposal Accept."""
|
||||||
|
task_id: int
|
||||||
|
task_code: Optional[str] = None
|
||||||
|
task_type: str
|
||||||
|
task_subtype: Optional[str] = None
|
||||||
|
title: str
|
||||||
|
status: Optional[str] = None
|
||||||
|
source_essential_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ProposalDetailResponse(ProposalResponse):
|
class ProposalDetailResponse(ProposalResponse):
|
||||||
"""Extended Proposal response that embeds its Essential list."""
|
"""Extended Proposal response that embeds its Essential list and generated tasks."""
|
||||||
|
|
||||||
essentials: List[EssentialResponse] = []
|
essentials: List[EssentialResponse] = []
|
||||||
|
generated_tasks: List[GeneratedTaskBrief] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
Reference in New Issue
Block a user