feat: 驳回图片改为选填,驳回原因/转交备注落库留痕
业务调整:车间驳回不必须拍照(编号错误、选错工序等场景无需照片举证)。 - schemas/task.py: TaskRejectRequest.images 由必填(min_length=1)改为 default_factory=list,保留 max_length=9 与 URL 总长保护;reason 增加 mode="before" 的 strip 校验器,堵住纯空格凑长度绕过 min_length 的口子 (顺带保证落库的 reject_reason 不带首尾空白) - task_service.py: 驳回时把原因+图片写入 TaskRecord 挂到被驳回任务下, 修复前端时间线看不到驳回详情的问题;转交时把交接备注也挂一条记录到 下家任务,补齐接手人的上下文视角 - task_service.py: create_task / receive_task 写入 overall_status 后调用 sync_product_status 对齐 status 字段
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
"""任务服务 — 核心业务逻辑:接收、驳回返工、裂变转交、无限嵌套子任务"""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import uuid
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select, delete
|
||||
@ -16,6 +17,7 @@ from app.core.lifecycle import (
|
||||
is_step_allowed,
|
||||
phase_label,
|
||||
resolve_phase_for_step,
|
||||
sync_product_status,
|
||||
)
|
||||
from app.models.product import Product
|
||||
from app.models.task_log import TaskLog
|
||||
@ -380,6 +382,9 @@ async def create_task(db: AsyncSession, data: TaskCreate) -> TaskResponse:
|
||||
_enforce_step_isolation(product, data.task_name, action="创建任务")
|
||||
if data.task_name and (not data.parent_task_id or data.task_type in ("TRANSFER", "RECOVERY")):
|
||||
product.overall_status = "已入库" if "virtual_warehouse" in data.task_name else data.task_name
|
||||
# 🔧 双字段同步:overall_status 改动后必须对齐 status,
|
||||
# 否则出库回流设备的 status 会永远停在 OUTBOUND,污染统计口径
|
||||
sync_product_status(product)
|
||||
# 派发给人 → 产品离开仓库
|
||||
if data.assignee_id and data.assignee_id != VIRTUAL_WAREHOUSE:
|
||||
product.current_location_id = data.assignee_id
|
||||
@ -681,6 +686,9 @@ async def receive_task(
|
||||
not task.parent_task_id or task.task_type in ("TRANSFER", "RECOVERY")
|
||||
):
|
||||
product.overall_status = task_name # 只主线任务同步宏观状态
|
||||
# 🔧 双字段同步:无条件对齐一次,顺带治愈历史残留
|
||||
# (如整体已是「发货测试」而 status 还停在 OUTBOUND)
|
||||
sync_product_status(product)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
@ -704,8 +712,9 @@ async def reject_task(
|
||||
2. 查找当前任务的父任务 (parent_task) 的负责人 (assignee_id)。
|
||||
- 若有父任务:返工任务分配给父任务的 assignee_id。
|
||||
- 若无父任务(顶层任务):返工任务分配给当前任务自己的 assignee_id。
|
||||
3. 为该负责人新建一个完全一样的任务,但 is_rework=True,status=PENDING。
|
||||
4. 新返工任务挂在同一个 parent_task_id 下(与原任务同级)。
|
||||
3. 把驳回原因 + 异常图片写入 TaskRecord,挂到被驳回的任务下(前端时间线可见)。
|
||||
4. 为该负责人新建一个完全一样的任务,但 is_rework=True,status=PENDING。
|
||||
5. 新返工任务挂在同一个 parent_task_id 下(与原任务同级)。
|
||||
"""
|
||||
task = await _get_task_or_404(db, task_id)
|
||||
|
||||
@ -761,7 +770,16 @@ async def reject_task(
|
||||
if not rework_assignee_id:
|
||||
rework_assignee_id = task.assignee_id
|
||||
|
||||
# --- 3. 创建返工任务 ---
|
||||
# --- 3. 写入驳回记录(原因 + 异常图片)→ 修复前端时间线缺失 ---
|
||||
# 图片为选填(编号错误等场景可不传),空数组落库为 "[]",前端时间线照常渲染
|
||||
db.add(TaskRecord(
|
||||
task_id=task.id,
|
||||
remark=f"[品质驳回] {request.reason}(返工任务已派发给 {rework_assignee_id or '未指派'})",
|
||||
images=json.dumps(request.images, ensure_ascii=False),
|
||||
created_at=now,
|
||||
))
|
||||
|
||||
# --- 4. 创建返工任务 ---
|
||||
rework_task = Task(
|
||||
product_id=task.product_id,
|
||||
parent_task_id=task.parent_task_id, # 与原任务同级
|
||||
@ -955,6 +973,16 @@ async def transfer_task(
|
||||
remark=request.note or f"由任务「{task.task_name}」裂变转交创建,分配给 {nt.assignee_id}",
|
||||
task_assignee_id=nt.assignee_id,
|
||||
)
|
||||
# 🔧 转交留言留痕:把上一工序的嘱咐也挂一条记录到**下家任务**上。
|
||||
# 原先留言只落在「已完工的上游任务记录」+「新任务的 Task.remark 字段」里,
|
||||
# 接手人打开自己任务的「操作日志」只能看到 [接收],看不到交接上下文,
|
||||
# 转交备注的协作意义就丢了。这里补齐下游视角。
|
||||
if request.note and request.note.strip():
|
||||
db.add(TaskRecord(
|
||||
task_id=nt.id,
|
||||
remark=f"[上一工序转交留言]:{request.note.strip()}",
|
||||
images="[]",
|
||||
))
|
||||
# 🔔 通知:新任务派发
|
||||
if nt.assignee_id:
|
||||
db.add(Notification(
|
||||
@ -970,12 +998,13 @@ async def transfer_task(
|
||||
if has_warehouse and not real_branches:
|
||||
product.current_location_id = VIRTUAL_WAREHOUSE
|
||||
product.overall_status = "待仓库收货"
|
||||
# 双字段同步:完工转交入库 → 产品标记为已完成(MOM 扫码实收后才变已入库)
|
||||
product.status = "COMPLETED"
|
||||
elif real_branches:
|
||||
product.current_location_id = real_branches[0][1]
|
||||
if not task.parent_task_id or task.task_type in ("TRANSFER", "RECOVERY"):
|
||||
product.overall_status = real_branches[0][0]
|
||||
# 🔧 双字段同步:两个分支统一在这里对齐一次(原先只有入库分支硬编码
|
||||
# product.status="COMPLETED",普通转交分支完全没同步 → 残留 OUTBOUND)
|
||||
sync_product_status(product)
|
||||
|
||||
# 🔧 位置回溯:如果有新任务创建,优先新任务负责人;否则回溯到父任务
|
||||
if not real_branches and not has_warehouse:
|
||||
@ -1057,6 +1086,17 @@ async def complete_task(
|
||||
remark=request.remark or f"完成任务: {task.task_name}",
|
||||
task_assignee_id=task.assignee_id,
|
||||
)
|
||||
# 🔧 完工留痕(与 transfer_task 同口径):备注要写成 TaskRecord,
|
||||
# 否则「操作日志」时间线只读 TaskRecord、看不到 TaskLog,交接内容就丢了。
|
||||
_will_transfer = bool(request.next_task_name and request.next_assignee_id)
|
||||
db.add(TaskRecord(
|
||||
task_id=task.id,
|
||||
remark=(
|
||||
request.remark
|
||||
or ("[完工转交] 移交下一工序" if _will_transfer else "[完工] 任务已完成")
|
||||
) + _admin_proxy_note(request.operator_id, task.assignee_id),
|
||||
images="[]",
|
||||
))
|
||||
|
||||
# --- 4. 可选:创建下一步任务(转交) ---
|
||||
next_task = None
|
||||
@ -1091,6 +1131,15 @@ async def complete_task(
|
||||
remark=f"由任务「{task.task_name}」完成后转交创建",
|
||||
task_assignee_id=next_task.assignee_id,
|
||||
)
|
||||
# 🔧 转交留言留痕:把上一工序的嘱咐也挂一条记录到**下家任务**上,
|
||||
# 让接手人打开自己的「操作日志」就能看到交接上下文
|
||||
# (与 transfer_task 同口径,保证两条转交路径行为一致)
|
||||
if request.remark and request.remark.strip():
|
||||
db.add(TaskRecord(
|
||||
task_id=next_task.id,
|
||||
remark=f"[上一工序转交留言]:{request.remark.strip()}",
|
||||
images="[]",
|
||||
))
|
||||
|
||||
# 🔧 位置回溯:老接口也触发(父任务优先)
|
||||
await _recalc_product_location(db, task.product_id, task_id)
|
||||
|
||||
Reference in New Issue
Block a user