feat(qc): implement material library with CRUD, stock-in, usage tracking and task deduction
This commit is contained in:
1 parent
4137c54588
commit
e6072457fd
9 files changed
+1638
-22
No files matched your search
@@ -14,6 +14,8 @@ from app.crud import qc_task_result as result_svc
|
||||
from app.crud import qc_task as task_svc
|
||||
from app.crud import qc_third_party_company as tpc_svc
|
||||
from app.crud import qc_third_party_item as tpi_svc
|
||||
from app.crud import qc_material as mat_svc
|
||||
from app.crud import qc_material_usage as mu_svc
|
||||
from app.crud import qc_work_order as wo_svc
|
||||
from app.crud.collection_protocol import total_pages
|
||||
from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload
|
||||
@@ -373,6 +375,7 @@ class TaskCompleteWithResultBody(BaseModel):
|
||||
results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
|
||||
remark: Optional[str] = Field(None, alias="remark")
|
||||
third_party_attachments: Optional[str] = Field(None, alias="thirdPartyAttachments")
|
||||
material_usages: Optional[List[Dict[str, Any]]] = Field(None, alias="materialUsages")
|
||||
|
||||
|
||||
@router.get("/qc-lab-test-items")
|
||||
@@ -745,6 +748,18 @@ async def complete_task_with_result(
|
||||
db, task_id, attachments=body.third_party_attachments,
|
||||
tenant_no=tenant_no, user_id=user_id,
|
||||
)
|
||||
# 物料扣减(可选,仅当传了 materialUsages 时)
|
||||
if body.material_usages:
|
||||
task_obj_pre = task_svc.get_task(db, task_id, tenant_no=tenant_no)
|
||||
task_no = task_obj_pre.task_no if task_obj_pre else ""
|
||||
mu_svc.deduct_for_task(
|
||||
db,
|
||||
task_id,
|
||||
task_no=task_no,
|
||||
material_usages=body.material_usages,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
)
|
||||
obj = task_svc.apply_task_result(
|
||||
db,
|
||||
task_id,
|
||||
@@ -788,3 +803,165 @@ async def update_qc_task_attachments(
|
||||
return error_response(str(e))
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
|
||||
|
||||
# ---------- 质检物料库 ----------
|
||||
|
||||
|
||||
class QcMaterialBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
material_code: str = Field(..., alias="materialCode")
|
||||
material_name: str = Field(..., alias="materialName")
|
||||
spec_model: Optional[str] = Field(None, alias="specModel")
|
||||
unit: Optional[str] = None
|
||||
warning_qty: Optional[float] = Field(0, alias="warningQty")
|
||||
status: str = "1"
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class QcMaterialUpdateBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
material_code: Optional[str] = Field(None, alias="materialCode")
|
||||
material_name: Optional[str] = Field(None, alias="materialName")
|
||||
spec_model: Optional[str] = Field(None, alias="specModel")
|
||||
unit: Optional[str] = None
|
||||
warning_qty: Optional[float] = Field(None, alias="warningQty")
|
||||
status: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class StockInBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
qty: float = Field(..., gt=0)
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/qc-materials")
|
||||
async def list_qc_materials(
|
||||
page: int = Query(1, ge=1),
|
||||
pageSize: int = Query(10, ge=1, le=100),
|
||||
keyword: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items, total = mat_svc.list_materials(
|
||||
db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
|
||||
)
|
||||
return success_response(_page_payload(items, total, page, pageSize))
|
||||
except PermissionError as e:
|
||||
return error_response(str(e), code=403)
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
|
||||
|
||||
@router.get("/qc-materials/options")
|
||||
async def qc_material_options(
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
return success_response(mat_svc.options_list(db, tenant_no=tenant_no))
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
|
||||
|
||||
@router.post("/qc-materials")
|
||||
async def create_qc_material(
|
||||
body: QcMaterialBody,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
user_id = _get_current_user_id(user)
|
||||
obj = mat_svc.create_material(db, body.model_dump(by_alias=True, exclude_none=True), tenant_no=tenant_no, user_id=user_id)
|
||||
return success_response(obj, message="创建成功")
|
||||
except ValueError as e:
|
||||
return error_response(str(e))
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
|
||||
|
||||
@router.put("/qc-materials/{material_id}")
|
||||
async def update_qc_material(
|
||||
material_id: str,
|
||||
body: QcMaterialUpdateBody,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
user_id = _get_current_user_id(user)
|
||||
obj = mat_svc.update_material(db, material_id, body.model_dump(by_alias=True, exclude_none=True), tenant_no=tenant_no, user_id=user_id)
|
||||
return success_response(obj, message="更新成功")
|
||||
except ValueError as e:
|
||||
return error_response(str(e))
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
|
||||
|
||||
@router.delete("/qc-materials/{material_id}")
|
||||
async def delete_qc_material(
|
||||
material_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
user_id = _get_current_user_id(user)
|
||||
mat_svc.soft_delete_material(db, material_id, tenant_no=tenant_no, user_id=user_id)
|
||||
return success_response(None, message="删除成功")
|
||||
except ValueError as e:
|
||||
return error_response(str(e))
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
|
||||
|
||||
@router.post("/qc-materials/{material_id}/stock-in")
|
||||
async def stock_in_qc_material(
|
||||
material_id: str,
|
||||
body: StockInBody,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
user_id = _get_current_user_id(user)
|
||||
obj = mat_svc.stock_in(db, material_id, qty=body.qty, remark=body.remark, tenant_no=tenant_no, user_id=user_id)
|
||||
return success_response(obj, message="入库成功")
|
||||
except ValueError as e:
|
||||
return error_response(str(e))
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
|
||||
|
||||
# ---------- 物料变动记录 ----------
|
||||
|
||||
|
||||
@router.get("/qc-material-usages")
|
||||
async def list_qc_material_usages(
|
||||
page: int = Query(1, ge=1),
|
||||
pageSize: int = Query(10, ge=1, le=100),
|
||||
materialId: Optional[str] = None,
|
||||
changeType: Optional[str] = None,
|
||||
startTime: Optional[str] = None,
|
||||
endTime: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:qcMaterialUsage:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items, total = mu_svc.list_usages(
|
||||
db, tenant_no, page=page, page_size=pageSize,
|
||||
material_id=materialId, change_type=changeType,
|
||||
start_time=startTime, end_time=endTime,
|
||||
)
|
||||
return success_response(_page_payload(items, total, page, pageSize))
|
||||
except PermissionError as e:
|
||||
return error_response(str(e), code=403)
|
||||
except Exception as e:
|
||||
return error_response(str(e))
|
||||
@@ -0,0 +1,313 @@
|
||||
"""物料库 CRUD"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from sqlalchemy import and_, func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.models import BizQcMaterial, BizQcMaterialUsage
|
||||
|
||||
|
||||
def _dec(v: Any) -> float:
|
||||
if v is None:
|
||||
return 0.0
|
||||
return float(v)
|
||||
|
||||
|
||||
def _fmt_dt(t: Optional[datetime]) -> str:
|
||||
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
|
||||
|
||||
|
||||
def _row_to_dict(r: BizQcMaterial) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": r.id,
|
||||
"materialCode": r.material_code,
|
||||
"materialName": r.material_name,
|
||||
"specModel": r.spec_model or "",
|
||||
"unit": r.unit or "",
|
||||
"stockQty": _dec(r.stock_qty),
|
||||
"warningQty": _dec(r.warning_qty),
|
||||
"status": r.status or "1",
|
||||
"orderNum": r.order_num if r.order_num is not None else 0,
|
||||
"remark": r.remark or "",
|
||||
"createTime": _fmt_dt(r.create_time),
|
||||
"updateTime": _fmt_dt(r.update_time),
|
||||
}
|
||||
|
||||
|
||||
def code_exists(
|
||||
db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None
|
||||
) -> bool:
|
||||
c = (code or "").strip()
|
||||
if not c:
|
||||
return False
|
||||
q = db.query(BizQcMaterial).filter(
|
||||
BizQcMaterial.material_code == c,
|
||||
BizQcMaterial.tenant_no == tenant_no,
|
||||
BizQcMaterial.del_flag == 0,
|
||||
)
|
||||
if exclude_id:
|
||||
q = q.filter(BizQcMaterial.id != exclude_id)
|
||||
return q.first() is not None
|
||||
|
||||
|
||||
def total_pages(db: Session, tenant_no: str, page_size: int = 10) -> int:
|
||||
cnt = (
|
||||
db.query(func.count(BizQcMaterial.id))
|
||||
.filter(
|
||||
BizQcMaterial.tenant_no == tenant_no,
|
||||
BizQcMaterial.del_flag == 0,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
ps = max(1, page_size)
|
||||
return (cnt + ps - 1) // ps
|
||||
|
||||
|
||||
def list_materials(
|
||||
db: Session,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
keyword: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
tenant_no: Optional[str] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
def _filtered():
|
||||
q0 = db.query(BizQcMaterial).filter(BizQcMaterial.del_flag == 0)
|
||||
if tenant_no:
|
||||
q0 = q0.filter(BizQcMaterial.tenant_no == tenant_no)
|
||||
kw = (keyword or "").strip()
|
||||
if kw:
|
||||
like = f"%{kw}%"
|
||||
q0 = q0.filter(
|
||||
or_(
|
||||
BizQcMaterial.material_code.like(like),
|
||||
BizQcMaterial.material_name.like(like),
|
||||
)
|
||||
)
|
||||
st = (status or "").strip()
|
||||
if st in ("0", "1"):
|
||||
q0 = q0.filter(BizQcMaterial.status == st)
|
||||
return q0
|
||||
|
||||
total = _filtered().order_by(None).with_entities(func.count(BizQcMaterial.id)).scalar() or 0
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
rows = (
|
||||
_filtered()
|
||||
.order_by(BizQcMaterial.create_time.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
return [_row_to_dict(x) for x in rows], int(total)
|
||||
|
||||
|
||||
def get_material(
|
||||
db: Session, row_id: str, tenant_no: Optional[str] = None
|
||||
) -> Optional[BizQcMaterial]:
|
||||
q = db.query(BizQcMaterial).filter(
|
||||
BizQcMaterial.id == row_id, BizQcMaterial.del_flag == 0
|
||||
)
|
||||
if tenant_no:
|
||||
q = q.filter(BizQcMaterial.tenant_no == tenant_no)
|
||||
return q.first()
|
||||
|
||||
|
||||
def create_material(
|
||||
db: Session,
|
||||
*,
|
||||
material_code: str,
|
||||
material_name: str,
|
||||
spec_model: Optional[str] = None,
|
||||
unit: Optional[str] = None,
|
||||
stock_qty: Optional[Decimal] = None,
|
||||
warning_qty: Optional[Decimal] = None,
|
||||
status: str = "1",
|
||||
tenant_no: str,
|
||||
order_num: Optional[int] = None,
|
||||
remark: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
create_dept_id: str = "",
|
||||
) -> BizQcMaterial:
|
||||
code = (material_code or "").strip()
|
||||
if not code:
|
||||
raise ValueError("物料编码不能为空")
|
||||
if code_exists(db, code, tenant_no):
|
||||
raise ValueError("物料编码已存在")
|
||||
name = (material_name or "").strip()
|
||||
if not name:
|
||||
raise ValueError("物料名称不能为空")
|
||||
st = (status or "1").strip()
|
||||
if st not in ("0", "1"):
|
||||
raise ValueError("状态无效")
|
||||
obj = BizQcMaterial(
|
||||
id=generate_uuid(),
|
||||
material_code=code,
|
||||
material_name=name,
|
||||
spec_model=(spec_model or "").strip() or None,
|
||||
unit=(unit or "").strip() or None,
|
||||
stock_qty=stock_qty if stock_qty is not None else 0,
|
||||
warning_qty=warning_qty if warning_qty is not None else 0,
|
||||
status=st,
|
||||
tenant_no=tenant_no,
|
||||
order_num=order_num if order_num is not None else 0,
|
||||
remark=(remark or "").strip() or None,
|
||||
create_by=user_id,
|
||||
create_dept_id=create_dept_id or "",
|
||||
create_time=datetime.now(),
|
||||
update_by=user_id,
|
||||
update_time=datetime.now(),
|
||||
del_flag=0,
|
||||
)
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def update_material(
|
||||
db: Session,
|
||||
row_id: str,
|
||||
*,
|
||||
material_code: Optional[str] = None,
|
||||
material_name: Optional[str] = None,
|
||||
spec_model: Optional[str] = None,
|
||||
unit: Optional[str] = None,
|
||||
warning_qty: Optional[Decimal] = None,
|
||||
status: Optional[str] = None,
|
||||
order_num: Optional[int] = None,
|
||||
remark: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
tenant_no: Optional[str] = None,
|
||||
) -> BizQcMaterial:
|
||||
obj = get_material(db, row_id, tenant_no=tenant_no)
|
||||
if not obj:
|
||||
raise ValueError("物料不存在")
|
||||
if material_code is not None:
|
||||
code = material_code.strip()
|
||||
if not code:
|
||||
raise ValueError("物料编码不能为空")
|
||||
if code != obj.material_code and code_exists(db, code, obj.tenant_no, exclude_id=row_id):
|
||||
raise ValueError("物料编码已存在")
|
||||
obj.material_code = code
|
||||
if material_name is not None:
|
||||
name = (material_name or "").strip()
|
||||
if not name:
|
||||
raise ValueError("物料名称不能为空")
|
||||
obj.material_name = name
|
||||
if spec_model is not None:
|
||||
obj.spec_model = (spec_model or "").strip() or None
|
||||
if unit is not None:
|
||||
obj.unit = (unit or "").strip() or None
|
||||
if warning_qty is not None:
|
||||
obj.warning_qty = warning_qty
|
||||
if status is not None:
|
||||
st = status.strip()
|
||||
if st not in ("0", "1"):
|
||||
raise ValueError("状态无效")
|
||||
obj.status = st
|
||||
if order_num is not None:
|
||||
obj.order_num = order_num
|
||||
if remark is not None:
|
||||
obj.remark = (remark or "").strip() or None
|
||||
obj.update_by = user_id
|
||||
obj.update_time = datetime.now()
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def soft_delete_material(db: Session, row_id: str, tenant_no: Optional[str] = None) -> None:
|
||||
obj = get_material(db, row_id, tenant_no=tenant_no)
|
||||
if not obj:
|
||||
raise ValueError("物料不存在")
|
||||
if obj.stock_qty is not None and _dec(obj.stock_qty) > 0:
|
||||
raise ValueError("该物料仍有库存,无法删除")
|
||||
obj.del_flag = 1
|
||||
obj.update_time = datetime.now()
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
|
||||
|
||||
def stock_in(
|
||||
db: Session,
|
||||
row_id: str,
|
||||
*,
|
||||
qty: Decimal,
|
||||
remark: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
tenant_no: Optional[str] = None,
|
||||
) -> BizQcMaterial:
|
||||
if qty is None or float(qty) <= 0:
|
||||
raise ValueError("入库数量必须大于0")
|
||||
# 行锁锁定物料行
|
||||
obj = (
|
||||
db.query(BizQcMaterial)
|
||||
.filter(BizQcMaterial.id == row_id, BizQcMaterial.del_flag == 0)
|
||||
.with_for_update()
|
||||
.first()
|
||||
)
|
||||
if not obj:
|
||||
raise ValueError("物料不存在")
|
||||
old_stock = obj.stock_qty or 0
|
||||
new_stock = old_stock + qty
|
||||
obj.stock_qty = new_stock
|
||||
obj.update_by = user_id
|
||||
obj.update_time = datetime.now()
|
||||
db.add(obj)
|
||||
# 写入使用记录
|
||||
usage = BizQcMaterialUsage(
|
||||
id=generate_uuid(),
|
||||
material_id=obj.id,
|
||||
material_code=obj.material_code,
|
||||
material_name=obj.material_name,
|
||||
change_type="in",
|
||||
change_qty=qty,
|
||||
stock_after=new_stock,
|
||||
source_type="manual_in",
|
||||
tenant_no=obj.tenant_no,
|
||||
remark=(remark or "").strip() or None,
|
||||
create_by=user_id,
|
||||
create_dept_id=obj.create_dept_id or "",
|
||||
create_time=datetime.now(),
|
||||
update_by=user_id,
|
||||
update_time=datetime.now(),
|
||||
del_flag=0,
|
||||
)
|
||||
db.add(usage)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def options_list(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
rows = (
|
||||
db.query(BizQcMaterial)
|
||||
.filter(
|
||||
BizQcMaterial.tenant_no == tenant_no,
|
||||
BizQcMaterial.del_flag == 0,
|
||||
BizQcMaterial.status == "1",
|
||||
)
|
||||
.order_by(BizQcMaterial.create_time.desc())
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"label": r.material_name,
|
||||
"materialCode": r.material_code,
|
||||
"materialName": r.material_name,
|
||||
"specModel": r.spec_model or "",
|
||||
"unit": r.unit or "",
|
||||
"stockQty": _dec(r.stock_qty),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
@@ -0,0 +1,140 @@
|
||||
"""物料变动记录 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
|
||||
|
||||
|
||||
def _row_to_dict(r: BizQcMaterialUsage) -> Dict[str, Any]:
|
||||
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 "",
|
||||
"sourceType": r.source_type,
|
||||
"remark": r.remark or "",
|
||||
"createBy": r.create_by or "",
|
||||
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
|
||||
"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,
|
||||
) -> 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)
|
||||
if change_type:
|
||||
q0 = q0.filter(BizQcMaterialUsage.change_type == change_type)
|
||||
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")
|
||||
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()
|
||||
)
|
||||
return [_row_to_dict(x) for x in rows], int(total)
|
||||
|
||||
|
||||
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。"""
|
||||
for item in material_usages:
|
||||
mid = item.get("materialId") or ""
|
||||
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 = 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="",
|
||||
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)
|
||||
Reference in new issue
Block a user