288 lines
10 KiB
Python
288 lines
10 KiB
Python
"""物料变动记录 CRUD"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
from fastapi import HTTPException
|
||
from sqlalchemy import func
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.utils import generate_uuid
|
||
from app.models.qc_material import BizQcMaterial, BizQcMaterialUsage
|
||
from app.models.models import SysUser
|
||
|
||
|
||
def _parse_receiver_purpose_from_remark(remark: Optional[str]) -> Tuple[str, str]:
|
||
"""兼容旧数据:领用人/用途曾拼接在 remark 中。"""
|
||
receiver, purpose = "", ""
|
||
for part in (remark or "").split(";"):
|
||
seg = part.strip()
|
||
if seg.startswith("领用人:"):
|
||
receiver = seg[len("领用人:") :].strip()
|
||
elif seg.startswith("用途:"):
|
||
purpose = seg[len("用途:") :].strip()
|
||
return receiver, purpose
|
||
|
||
|
||
def _resolve_receiver_purpose(r: BizQcMaterialUsage) -> Tuple[str, str]:
|
||
receiver = (getattr(r, "receiver", None) or "").strip()
|
||
purpose = (getattr(r, "purpose", None) or "").strip()
|
||
if receiver or purpose:
|
||
return receiver, purpose
|
||
if (r.source_type or "") == "manual_out":
|
||
return _parse_receiver_purpose_from_remark(r.remark)
|
||
return "", ""
|
||
|
||
|
||
def _display_remark(r: BizQcMaterialUsage) -> str:
|
||
"""手动出库时备注仅展示用户填写内容,不含领用人/用途拼接段。"""
|
||
remark = (r.remark or "").strip()
|
||
if (r.source_type or "") != "manual_out" or not remark:
|
||
return remark
|
||
parts: List[str] = []
|
||
for part in remark.split(";"):
|
||
seg = part.strip()
|
||
if not seg or seg.startswith("领用人:") or seg.startswith("用途:"):
|
||
continue
|
||
if seg.startswith("备注"):
|
||
seg = seg[2:].strip() or seg
|
||
parts.append(seg)
|
||
return ";".join(parts) if parts else ""
|
||
|
||
|
||
def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
|
||
ids = list({x for x in user_ids if x})
|
||
if not ids:
|
||
return {}
|
||
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
|
||
return {u.id: (u.nick_name or "").strip() for u in rows}
|
||
|
||
|
||
def _row_to_dict(r: BizQcMaterialUsage, *, create_by_name: str = "") -> Dict[str, Any]:
|
||
create_by = r.create_by or ""
|
||
receiver, purpose = _resolve_receiver_purpose(r)
|
||
create_time = r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else ""
|
||
is_manual_out = (r.source_type or "") == "manual_out"
|
||
return {
|
||
"id": r.id,
|
||
"materialId": r.material_id,
|
||
"materialCode": r.material_code or "",
|
||
"materialName": r.material_name or "",
|
||
"changeType": r.change_type,
|
||
"changeQty": float(r.change_qty) if r.change_qty is not None else 0,
|
||
"stockAfter": float(r.stock_after) if r.stock_after is not None else 0,
|
||
"qcTaskId": r.qc_task_id or "",
|
||
"qcTaskNo": r.qc_task_no or "",
|
||
"receiver": receiver,
|
||
"purpose": purpose,
|
||
"sourceType": r.source_type or "",
|
||
"useTime": create_time if is_manual_out else "",
|
||
"remark": _display_remark(r),
|
||
"createBy": create_by,
|
||
"createByName": create_by_name or create_by,
|
||
"createTime": create_time,
|
||
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
|
||
}
|
||
|
||
|
||
def list_usages(
|
||
db: Session,
|
||
tenant_no: str,
|
||
*,
|
||
page: int = 1,
|
||
page_size: int = 10,
|
||
material_id: Optional[str] = None,
|
||
change_type: Optional[str] = None,
|
||
start_time: Optional[str] = None,
|
||
end_time: Optional[str] = None,
|
||
user_info=None,
|
||
) -> Tuple[List[Dict[str, Any]], int]:
|
||
def _filtered():
|
||
q0 = db.query(BizQcMaterialUsage).filter(
|
||
BizQcMaterialUsage.tenant_no == tenant_no,
|
||
BizQcMaterialUsage.del_flag == 0,
|
||
)
|
||
if material_id:
|
||
q0 = q0.filter(BizQcMaterialUsage.material_id == material_id)
|
||
ct = (change_type or "").strip()
|
||
if ct == "out":
|
||
q0 = q0.filter(
|
||
BizQcMaterialUsage.change_type == "use",
|
||
BizQcMaterialUsage.source_type == "manual_out",
|
||
)
|
||
elif ct == "use":
|
||
q0 = q0.filter(
|
||
BizQcMaterialUsage.change_type == "use",
|
||
BizQcMaterialUsage.source_type == "task_complete",
|
||
)
|
||
elif ct == "in":
|
||
q0 = q0.filter(BizQcMaterialUsage.change_type == "in")
|
||
if start_time:
|
||
q0 = q0.filter(BizQcMaterialUsage.create_time >= start_time)
|
||
if end_time:
|
||
q0 = q0.filter(BizQcMaterialUsage.create_time <= end_time + " 23:59:59")
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizQcMaterialUsage, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizQcMaterialUsage.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = (
|
||
_filtered()
|
||
.order_by(BizQcMaterialUsage.create_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
name_map = _user_nick_name_map(db, [x.create_by for x in rows if x.create_by])
|
||
return [
|
||
_row_to_dict(x, create_by_name=name_map.get(x.create_by or "", ""))
|
||
for x in rows
|
||
], int(total)
|
||
|
||
|
||
def list_usages_by_task(
|
||
db: Session,
|
||
task_id: str,
|
||
tenant_no: str,
|
||
*,
|
||
user_info=None,
|
||
) -> List[Dict[str, Any]]:
|
||
"""查询质检任务关联的物料使用记录(task_complete)。"""
|
||
q = db.query(BizQcMaterialUsage).filter(
|
||
BizQcMaterialUsage.tenant_no == tenant_no,
|
||
BizQcMaterialUsage.del_flag == 0,
|
||
BizQcMaterialUsage.qc_task_id == task_id,
|
||
BizQcMaterialUsage.change_type == "use",
|
||
BizQcMaterialUsage.source_type == "task_complete",
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
|
||
q = apply_data_scope(q, BizQcMaterialUsage, user_info, db, tenant_no)
|
||
rows = q.order_by(BizQcMaterialUsage.create_time.asc()).all()
|
||
return [_row_to_dict(x) for x in rows]
|
||
|
||
|
||
def revert_task_material_usages(
|
||
db: Session,
|
||
task_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> None:
|
||
"""撤销任务此前 task_complete 扣减,便于重新填报物料使用。"""
|
||
rows = (
|
||
db.query(BizQcMaterialUsage)
|
||
.filter(
|
||
BizQcMaterialUsage.qc_task_id == task_id,
|
||
BizQcMaterialUsage.tenant_no == tenant_no,
|
||
BizQcMaterialUsage.del_flag == 0,
|
||
BizQcMaterialUsage.source_type == "task_complete",
|
||
BizQcMaterialUsage.change_type == "use",
|
||
)
|
||
.all()
|
||
)
|
||
if not rows:
|
||
return
|
||
now = datetime.now()
|
||
for r in rows:
|
||
restore_qty = abs(r.change_qty) if r.change_qty is not None else Decimal("0")
|
||
if restore_qty <= 0:
|
||
r.del_flag = 1
|
||
r.update_by = user_id
|
||
r.update_time = now
|
||
continue
|
||
material = (
|
||
db.query(BizQcMaterial)
|
||
.with_for_update()
|
||
.filter(
|
||
BizQcMaterial.id == r.material_id,
|
||
BizQcMaterial.tenant_no == tenant_no,
|
||
BizQcMaterial.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if material:
|
||
material.stock_qty = (material.stock_qty or Decimal("0")) + restore_qty
|
||
material.update_by = user_id
|
||
material.update_time = now
|
||
r.del_flag = 1
|
||
r.update_by = user_id
|
||
r.update_time = now
|
||
|
||
|
||
def deduct_for_task(
|
||
db: Session,
|
||
task_id: str,
|
||
task_no: str,
|
||
material_usages: List[Dict[str, Any]],
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
dept_id: str = "",
|
||
) -> None:
|
||
"""扣减物料库存并生成变动记录。调用方控制事务,本函数不 commit。"""
|
||
revert_task_material_usages(db, task_id, tenant_no, user_id=user_id)
|
||
for item in material_usages:
|
||
mid = (item.get("materialId") or item.get("material_id") or "").strip()
|
||
qty = item.get("qty")
|
||
if not mid:
|
||
raise HTTPException(status_code=400, detail="物料ID不能为空")
|
||
if qty is None or Decimal(str(qty)) <= 0:
|
||
raise HTTPException(status_code=400, detail="物料使用数量必须大于0")
|
||
|
||
qty_dec = Decimal(str(qty))
|
||
|
||
material = (
|
||
db.query(BizQcMaterial)
|
||
.with_for_update()
|
||
.filter(
|
||
BizQcMaterial.id == mid,
|
||
BizQcMaterial.tenant_no == tenant_no,
|
||
BizQcMaterial.del_flag == 0,
|
||
BizQcMaterial.status == "1",
|
||
)
|
||
.first()
|
||
)
|
||
if not material:
|
||
raise HTTPException(status_code=400, detail=f"物料不存在或已停用(ID: {mid})")
|
||
|
||
stock_qty = material.stock_qty if material.stock_qty is not None else Decimal("0")
|
||
if stock_qty < qty_dec:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"物料「{material.material_name}」库存不足,当前库存 {stock_qty},需使用 {qty_dec}",
|
||
)
|
||
|
||
new_stock = stock_qty - qty_dec
|
||
material.stock_qty = new_stock
|
||
material.update_by = user_id
|
||
material.update_time = datetime.now()
|
||
|
||
usage_remark = str(item.get("remark") or "").strip()[:512]
|
||
|
||
usage = BizQcMaterialUsage(
|
||
id=generate_uuid(),
|
||
material_id=mid,
|
||
material_code=material.material_code,
|
||
material_name=material.material_name,
|
||
change_type="use",
|
||
change_qty=-qty_dec,
|
||
stock_after=new_stock,
|
||
qc_task_id=task_id,
|
||
qc_task_no=task_no,
|
||
source_type="task_complete",
|
||
tenant_no=tenant_no,
|
||
remark=usage_remark,
|
||
create_by=user_id,
|
||
create_dept_id=dept_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(usage)
|