Files
shanxia2.0/backend/app/crud/qc_material_usage.py
T
2026-05-21 11:09:29 +08:00

288 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""物料变动记录 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)