1336 lines
48 KiB
Python
1336 lines
48 KiB
Python
"""生产物料库 API"""
|
|
from typing import List, Optional
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.database import get_db
|
|
from app.core.menu_guard import require_any_menu_code, require_menu_code
|
|
from app.core.response import error_response, success_response
|
|
from app.core.utils import parse_datetime
|
|
from app.crud import production_material as mat_svc
|
|
from app.crud import production_material_stock_batch as batch_svc
|
|
from app.crud import production_material_stock_check as stock_check_svc
|
|
from app.crud import production_material_usage as mu_svc
|
|
from app.crud import production_material_formula as formula_svc
|
|
from app.crud import production_material_prep as prep_svc
|
|
from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload
|
|
|
|
router = APIRouter(tags=["生产物料库"])
|
|
|
|
# ---------- 生产物料库 ----------
|
|
|
|
|
|
class ProductionMaterialBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_type: str = Field(..., alias="materialType")
|
|
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 ProductionMaterialUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_type: Optional[str] = Field(None, alias="materialType")
|
|
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)
|
|
stock_in_no: str = Field(..., alias="stockInNo")
|
|
stock_in_date: str = Field(..., alias="stockInDate")
|
|
expire_date: str = Field(..., alias="expireDate")
|
|
production_date: Optional[str] = Field(None, alias="productionDate")
|
|
catalog_no: str = Field(..., alias="catalogNo")
|
|
batch_no: str = Field(..., alias="batchNo")
|
|
manufacturer: str
|
|
storage_location: Optional[str] = Field(None, alias="storageLocation")
|
|
unit_price: float = Field(..., alias="unitPrice")
|
|
amount: float
|
|
qty: float = Field(..., gt=0)
|
|
remark: Optional[str] = None
|
|
stock_in_time: Optional[str] = Field(None, alias="stockInTime")
|
|
|
|
|
|
class BatchStockInItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_id: str = Field(..., alias="materialId")
|
|
stock_in_no: str = Field(..., alias="stockInNo")
|
|
stock_in_date: str = Field(..., alias="stockInDate")
|
|
expire_date: str = Field(..., alias="expireDate")
|
|
production_date: Optional[str] = Field(None, alias="productionDate")
|
|
catalog_no: str = Field(..., alias="catalogNo")
|
|
batch_no: str = Field(..., alias="batchNo")
|
|
manufacturer: str
|
|
storage_location: Optional[str] = Field(None, alias="storageLocation")
|
|
unit_price: float = Field(..., alias="unitPrice")
|
|
amount: float
|
|
qty: float = Field(..., gt=0)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class BatchStockInBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[BatchStockInItem] = Field(..., min_length=1)
|
|
remark: Optional[str] = None
|
|
stock_in_time: Optional[str] = Field(None, alias="stockInTime")
|
|
|
|
|
|
class BatchStockOutItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
batch_id: str = Field(..., alias="batchId")
|
|
qty: float = Field(..., gt=0)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class BatchStockOutBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[BatchStockOutItem] = Field(..., min_length=1)
|
|
receiver: Optional[str] = None
|
|
purpose: Optional[str] = None
|
|
remark: Optional[str] = None
|
|
stock_out_time: Optional[str] = Field(None, alias="stockOutTime")
|
|
|
|
|
|
class BatchDiscardItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
batch_id: str = Field(..., alias="batchId")
|
|
qty: float = Field(..., gt=0)
|
|
usage_unit: Optional[str] = Field(None, alias="usageUnit")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class BatchDiscardBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[BatchDiscardItem] = Field(..., min_length=1)
|
|
discard_reason: str = Field(..., alias="discardReason")
|
|
remark: Optional[str] = None
|
|
discard_time: Optional[str] = Field(None, alias="discardTime")
|
|
|
|
|
|
class ProductionMaterialBatchUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
stock_in_no: Optional[str] = Field(None, alias="stockInNo")
|
|
stock_in_date: Optional[str] = Field(None, alias="stockInDate")
|
|
expire_date: Optional[str] = Field(None, alias="expireDate")
|
|
production_date: Optional[str] = Field(None, alias="productionDate")
|
|
catalog_no: Optional[str] = Field(None, alias="catalogNo")
|
|
batch_no: Optional[str] = Field(None, alias="batchNo")
|
|
manufacturer: Optional[str] = None
|
|
storage_location: Optional[str] = Field(None, alias="storageLocation")
|
|
unit_price: Optional[float] = Field(None, alias="unitPrice")
|
|
amount: Optional[float] = None
|
|
remark: Optional[str] = None
|
|
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
|
|
|
|
|
|
@router.get("/production-material-batches")
|
|
async def list_production_material_batches(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
materialId: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = batch_svc.list_batches(
|
|
db,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
material_id=materialId,
|
|
material_type=materialType,
|
|
status=status,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-material-batches/options")
|
|
async def production_material_batch_options(
|
|
keyword: Optional[str] = None,
|
|
materialId: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
onlyAvailable: bool = Query(True, alias="onlyAvailable"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(
|
|
batch_svc.batch_options(
|
|
db,
|
|
tenant_no,
|
|
keyword=keyword,
|
|
material_id=materialId,
|
|
material_type=materialType,
|
|
only_available=onlyAvailable,
|
|
)
|
|
)
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/production-material-batches/{batch_id}")
|
|
async def update_production_material_batch(
|
|
batch_id: str,
|
|
body: ProductionMaterialBatchUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
data = body.model_dump(exclude_none=True)
|
|
obj = batch_svc.update_batch(
|
|
db,
|
|
batch_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
**data,
|
|
)
|
|
material = mat_svc.get_material(db, obj.material_id, tenant_no=tenant_no)
|
|
return success_response(
|
|
batch_svc._batch_row_to_dict(obj, material),
|
|
message="更新成功",
|
|
)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/production-material-batches/{batch_id}")
|
|
async def delete_production_material_batch(
|
|
batch_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
batch_svc.delete_batch(db, batch_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-materials")
|
|
async def list_production_materials(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mat_svc.list_materials(
|
|
db,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
material_type=materialType,
|
|
status=status,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-materials/options")
|
|
async def production_material_options(
|
|
materialType: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(
|
|
mat_svc.options_list(db, tenant_no=tenant_no, material_type=materialType)
|
|
)
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-materials")
|
|
async def create_production_material(
|
|
body: ProductionMaterialBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
obj = mat_svc.create_material(
|
|
db,
|
|
material_type=body.material_type,
|
|
material_code=body.material_code,
|
|
material_name=body.material_name,
|
|
spec_model=body.spec_model,
|
|
unit=body.unit,
|
|
warning_qty=Decimal(str(body.warning_qty))
|
|
if body.warning_qty is not None
|
|
else None,
|
|
status=mat_svc._norm_status(body.status),
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
)
|
|
return success_response(mat_svc._row_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/production-materials/{material_id}")
|
|
async def update_production_material(
|
|
material_id: str,
|
|
body: ProductionMaterialUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
data = body.model_dump(exclude_none=True)
|
|
if "warning_qty" in data:
|
|
data["warning_qty"] = Decimal(str(data["warning_qty"]))
|
|
if "status" in data:
|
|
data["status"] = mat_svc._norm_status(data["status"])
|
|
obj = mat_svc.update_material(
|
|
db,
|
|
material_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
**data,
|
|
)
|
|
return success_response(mat_svc._row_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/production-materials/{material_id}")
|
|
async def delete_production_material(
|
|
material_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material: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)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-materials/batch-stock-in")
|
|
async def batch_stock_in_production_material(
|
|
body: BatchStockInBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.stock_in_time:
|
|
rt = parse_datetime(body.stock_in_time)
|
|
if not rt:
|
|
return error_response(400, "入库时间格式无效")
|
|
operate_time = rt
|
|
items = [
|
|
{
|
|
"material_id": x.material_id,
|
|
"materialId": x.material_id,
|
|
"stock_in_no": x.stock_in_no,
|
|
"stockInNo": x.stock_in_no,
|
|
"stock_in_date": x.stock_in_date,
|
|
"stockInDate": x.stock_in_date,
|
|
"expire_date": x.expire_date,
|
|
"expireDate": x.expire_date,
|
|
"catalog_no": x.catalog_no,
|
|
"catalogNo": x.catalog_no,
|
|
"batch_no": x.batch_no,
|
|
"batchNo": x.batch_no,
|
|
"manufacturer": x.manufacturer,
|
|
"storage_location": x.storage_location,
|
|
"storageLocation": x.storage_location,
|
|
"unit_price": x.unit_price,
|
|
"unitPrice": x.unit_price,
|
|
"amount": x.amount,
|
|
"qty": x.qty,
|
|
"remark": x.remark,
|
|
}
|
|
for x in body.items
|
|
]
|
|
count = batch_svc.batch_stock_in(
|
|
db,
|
|
items=items,
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response({"count": count}, message=f"批量入库成功,共 {count} 项")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-materials/batch-stock-out")
|
|
async def batch_stock_out_production_material(
|
|
body: BatchStockOutBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.stock_out_time:
|
|
rt = parse_datetime(body.stock_out_time)
|
|
if not rt:
|
|
return error_response(400, "出库时间格式无效")
|
|
operate_time = rt
|
|
items = [
|
|
{"batch_id": x.batch_id, "batchId": x.batch_id, "qty": x.qty, "remark": x.remark}
|
|
for x in body.items
|
|
]
|
|
count = batch_svc.batch_stock_out(
|
|
db,
|
|
items=items,
|
|
receiver=body.receiver,
|
|
purpose=body.purpose,
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response({"count": count}, message=f"批量出库成功,共 {count} 项")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-materials/batch-discard")
|
|
async def batch_discard_production_material(
|
|
body: BatchDiscardBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.discard_time:
|
|
rt = parse_datetime(body.discard_time)
|
|
if not rt:
|
|
return error_response(400, "废弃时间格式无效")
|
|
operate_time = rt
|
|
items = [
|
|
{
|
|
"batch_id": x.batch_id,
|
|
"batchId": x.batch_id,
|
|
"qty": x.qty,
|
|
"usageUnit": x.usage_unit,
|
|
"usage_unit": x.usage_unit,
|
|
"remark": x.remark,
|
|
}
|
|
for x in body.items
|
|
]
|
|
count = batch_svc.batch_discard(
|
|
db,
|
|
items=items,
|
|
discard_reason=body.discard_reason,
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response({"count": count}, message=f"批量废弃成功,共 {count} 项")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-materials/{material_id}/stock-in")
|
|
async def stock_in_production_material(
|
|
material_id: str,
|
|
body: StockInBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.stock_in_time:
|
|
rt = parse_datetime(body.stock_in_time)
|
|
if not rt:
|
|
return error_response(400, "入库时间格式无效")
|
|
operate_time = rt
|
|
from decimal import Decimal
|
|
|
|
obj = batch_svc.stock_in_one(
|
|
db,
|
|
material_id,
|
|
stock_in_no=body.stock_in_no,
|
|
stock_in_date=body.stock_in_date,
|
|
expire_date=body.expire_date,
|
|
catalog_no=body.catalog_no,
|
|
batch_no=body.batch_no,
|
|
manufacturer=body.manufacturer,
|
|
storage_location=body.storage_location,
|
|
unit_price=body.unit_price,
|
|
amount=body.amount,
|
|
qty=Decimal(str(body.qty)),
|
|
remark=body.remark,
|
|
production_date=body.production_date,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response(batch_svc._batch_row_to_dict(obj), message="入库成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-batches/batch-stock-in")
|
|
async def batch_stock_in_production_material_batches(
|
|
body: BatchStockInBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""批量入库(与 /production-materials/batch-stock-in 等价)。"""
|
|
return await batch_stock_in_production_material(body, db, user)
|
|
|
|
|
|
@router.post("/production-material-batches/batch-stock-out")
|
|
async def batch_stock_out_production_material_batches(
|
|
body: BatchStockOutBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""批量出库(与 /production-materials/batch-stock-out 等价)。"""
|
|
return await batch_stock_out_production_material(body, db, user)
|
|
|
|
|
|
@router.post("/production-material-batches/batch-discard")
|
|
async def batch_discard_production_material_batches(
|
|
body: BatchDiscardBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""批量废弃(与 /production-materials/batch-discard 等价)。"""
|
|
return await batch_discard_production_material(body, db, user)
|
|
|
|
|
|
# ---------- 物料变动记录 ----------
|
|
|
|
|
|
@router.get("/production-material-requisition-options")
|
|
async def list_production_material_requisition_options(
|
|
materialType: Optional[str] = None,
|
|
keyword: Optional[str] = None,
|
|
limit: int = Query(50, ge=1, le=100),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(
|
|
require_any_menu_code(
|
|
"production:work-order:list",
|
|
"production:my-task:list",
|
|
"production:material:list",
|
|
)
|
|
),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = mu_svc.list_requisition_options(
|
|
db,
|
|
tenant_no,
|
|
keyword=keyword,
|
|
material_type=materialType,
|
|
limit=limit,
|
|
user_info=user,
|
|
)
|
|
return success_response(items)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-material-usages")
|
|
async def list_production_material_usages(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
materialId: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
changeType: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
keyword: Optional[str] = None,
|
|
stockInNo: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material: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, material_type=materialType, change_type=changeType,
|
|
start_time=startTime, end_time=endTime,
|
|
keyword=keyword, stock_in_no=stockInNo,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-usages/{usage_id}/mark-fully-used")
|
|
async def mark_production_material_usage_fully_used(
|
|
usage_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""标记领用记录已使用完(盘点登记 / 变动记录)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
mu_svc.mark_requisition_fully_used(
|
|
db,
|
|
usage_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
user_info=user,
|
|
)
|
|
return success_response(None, message="已标记为使用完")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/my-production-material-usages")
|
|
async def list_my_production_material_usages(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
materialId: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
changeType: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
keyword: Optional[str] = None,
|
|
stockInNo: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:my-material-usage:list")),
|
|
):
|
|
"""当前登录人的物料领用记录(手动出库领用人 / 任务使用填报人)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mu_svc.list_my_usages(
|
|
db,
|
|
tenant_no,
|
|
user_id=_get_current_user_id(user),
|
|
nick_name=(user.get("real_name") or "").strip(),
|
|
page=page,
|
|
page_size=pageSize,
|
|
material_id=materialId,
|
|
material_type=materialType,
|
|
change_type=changeType,
|
|
start_time=startTime,
|
|
end_time=endTime,
|
|
keyword=keyword,
|
|
stock_in_no=stockInNo,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-material-usages/{usage_id}/linked-step-usages")
|
|
async def list_production_material_linked_step_usages(
|
|
usage_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(
|
|
require_any_menu_code("production:material:list", "production:my-material-usage:list")
|
|
),
|
|
):
|
|
"""领用记录下关联的消耗明细(生产任务 step_save / 配制 prep_consume)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = mu_svc.list_linked_step_usages(
|
|
db, usage_id, tenant_no, user_info=user
|
|
)
|
|
return success_response(items)
|
|
except HTTPException as e:
|
|
detail = e.detail
|
|
msg = detail if isinstance(detail, str) else str(detail)
|
|
return error_response(e.status_code, msg)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 库存盘点 ----------
|
|
|
|
|
|
class StockCheckCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
title: str
|
|
check_time: Optional[str] = Field(None, alias="checkTime")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StockCheckUnusedLineItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
usage_id: Optional[str] = Field(None, alias="usageId")
|
|
batch_id: Optional[str] = Field(None, alias="batchId")
|
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
|
material_name: str = Field(..., alias="materialName")
|
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
stock_in_no: Optional[str] = Field(None, alias="stockInNo")
|
|
stock_in_date: Optional[str] = Field(None, alias="stockInDate")
|
|
expire_date: Optional[str] = Field(None, alias="expireDate")
|
|
catalog_no: Optional[str] = Field(None, alias="catalogNo")
|
|
batch_no: Optional[str] = Field(None, alias="batchNo")
|
|
manufacturer: Optional[str] = None
|
|
unit_price: Optional[float] = Field(None, alias="unitPrice")
|
|
holder: Optional[str] = None
|
|
unused_qty: float = Field(..., alias="unusedQty", gt=0)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StockCheckUnusedLinesBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[StockCheckUnusedLineItem] = Field(default_factory=list)
|
|
|
|
|
|
@router.get("/production-material-stock-check-requisitions")
|
|
async def list_production_material_stock_check_requisitions(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(50, ge=1, le=200),
|
|
keyword: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""盘点登记:待处理领用记录(未标记已使用完)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mu_svc.list_stock_check_requisitions(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-material-stock-checks")
|
|
async def list_production_material_stock_checks(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = stock_check_svc.list_checks(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
status=status,
|
|
start_time=startTime,
|
|
end_time=endTime,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-stock-checks")
|
|
async def create_production_material_stock_check(
|
|
body: StockCheckCreateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
res = stock_check_svc.create_check(
|
|
db,
|
|
title=body.title,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
check_time=body.check_time,
|
|
remark=body.remark,
|
|
)
|
|
return success_response(res, message="盘点单创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-material-stock-checks/{check_id}")
|
|
async def get_production_material_stock_check(
|
|
check_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
data = stock_check_svc.get_check_detail(db, check_id, tenant_no)
|
|
return success_response(data)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/production-material-stock-checks/{check_id}/unused-lines")
|
|
async def save_production_material_stock_check_unused_lines(
|
|
check_id: str,
|
|
body: StockCheckUnusedLinesBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
count = stock_check_svc.save_unused_lines(
|
|
db,
|
|
check_id,
|
|
items=[x.model_dump(by_alias=False) for x in body.items],
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
)
|
|
return success_response({"count": count}, message="登记保存成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-stock-checks/{check_id}/complete")
|
|
async def complete_production_material_stock_check(
|
|
check_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
stock_check_svc.complete_check(db, check_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="盘点已完成")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-stock-checks/{check_id}/cancel")
|
|
async def cancel_production_material_stock_check(
|
|
check_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
stock_check_svc.cancel_check(db, check_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="盘点已作废")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 配置试剂配方 ----------
|
|
|
|
|
|
class FormulaLineBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
component_material_id: str = Field(..., alias="componentMaterialId")
|
|
component_material_type: str = Field(..., alias="componentMaterialType")
|
|
standard_qty: float = Field(..., gt=0, alias="standardQty")
|
|
usage_unit: Optional[str] = Field(None, alias="usageUnit")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class FormulaReplaceBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
lines: List[FormulaLineBody] = Field(default_factory=list)
|
|
|
|
|
|
@router.get("/production-materials/{material_id}/formula")
|
|
async def get_production_material_formula(
|
|
material_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = formula_svc.get_formula(db, material_id, tenant_no)
|
|
return success_response(items)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/production-materials/{material_id}/formula")
|
|
async def replace_production_material_formula(
|
|
material_id: str,
|
|
body: FormulaReplaceBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
lines = [ln.model_dump(by_alias=True) for ln in body.lines]
|
|
items = formula_svc.replace_formula(
|
|
db,
|
|
material_id,
|
|
lines=lines,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
)
|
|
return success_response(items, message="配方已保存")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 配置试剂配制单 ----------
|
|
|
|
|
|
class PrepLineBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
component_material_id: str = Field(..., alias="componentMaterialId")
|
|
component_material_type: str = Field(..., alias="componentMaterialType")
|
|
requisition_usage_id: str = Field(..., alias="requisitionUsageId")
|
|
actual_qty: float = Field(..., gt=0, alias="actualQty")
|
|
usage_unit: Optional[str] = Field(None, alias="usageUnit")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class PrepBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
configured_material_id: str = Field(..., alias="configuredMaterialId")
|
|
prep_time: Optional[str] = Field(None, alias="prepTime")
|
|
output_qty: float = Field(..., gt=0, alias="outputQty")
|
|
output_unit: Optional[str] = Field(None, alias="outputUnit")
|
|
batch_no: str = Field(..., alias="batchNo")
|
|
storage_location: Optional[str] = Field(None, alias="storageLocation")
|
|
expire_date: str = Field(..., alias="expireDate")
|
|
remark: Optional[str] = None
|
|
lines: List[PrepLineBody] = Field(..., min_length=1)
|
|
|
|
|
|
class PrepUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
prep_time: Optional[str] = Field(None, alias="prepTime")
|
|
output_qty: Optional[float] = Field(None, gt=0, alias="outputQty")
|
|
output_unit: Optional[str] = Field(None, alias="outputUnit")
|
|
batch_no: Optional[str] = Field(None, alias="batchNo")
|
|
storage_location: Optional[str] = Field(None, alias="storageLocation")
|
|
expire_date: Optional[str] = Field(None, alias="expireDate")
|
|
remark: Optional[str] = None
|
|
lines: Optional[List[PrepLineBody]] = None
|
|
|
|
|
|
@router.get("/production-material-preps")
|
|
async def list_production_material_preps(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
configuredMaterialId: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = prep_svc.list_preps(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
status=status,
|
|
configured_material_id=configuredMaterialId,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
class AvailableMaterialBatch(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
requisitionUsageId: str = Field(..., alias="requisitionUsageId")
|
|
batchId: str = Field(..., alias="batchId")
|
|
materialId: str = Field(..., alias="materialId")
|
|
materialType: str = Field(..., alias="materialType")
|
|
materialCode: str = Field(..., alias="materialCode")
|
|
materialName: str = Field(..., alias="materialName")
|
|
specModel: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
stockInNo: str = Field(..., alias="stockInNo")
|
|
stockInDate: Optional[str] = Field(None, alias="stockInDate")
|
|
expireDate: Optional[str] = Field(None, alias="expireDate")
|
|
catalogNo: Optional[str] = Field(None, alias="catalogNo")
|
|
batchNo: str = Field(..., alias="batchNo")
|
|
manufacturer: str
|
|
storageLocation: Optional[str] = Field(None, alias="storageLocation")
|
|
requisitionQty: float = Field(..., alias="requisitionQty")
|
|
unitPrice: Optional[float] = Field(None, alias="unitPrice")
|
|
receiver: Optional[str] = None
|
|
receiverName: Optional[str] = Field(None, alias="receiverName")
|
|
useTime: Optional[str] = Field(None, alias="useTime")
|
|
|
|
|
|
@router.get("/production-material-preps/available-materials")
|
|
async def list_available_materials_for_prep(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(50, ge=1, le=200),
|
|
keyword: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""获取可用于配制的领用记录(有剩余、未标记已用完,须注册在 {prep_id} 路由之前)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = prep_svc.list_available_material_batches(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
material_type=materialType,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-material-preps/{prep_id}")
|
|
async def get_production_material_prep(
|
|
prep_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
data = prep_svc.get_prep_detail(db, prep_id, tenant_no)
|
|
return success_response(data)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-preps")
|
|
async def create_production_material_prep(
|
|
body: PrepBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
data = prep_svc.create_prep(
|
|
db,
|
|
data=body.model_dump(by_alias=True),
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
)
|
|
return success_response(data, message="配制单已创建")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/production-material-preps/{prep_id}")
|
|
async def update_production_material_prep(
|
|
prep_id: str,
|
|
body: PrepUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
data = prep_svc.update_prep(
|
|
db,
|
|
prep_id,
|
|
data=body.model_dump(by_alias=True, exclude_none=True),
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
)
|
|
return success_response(data, message="配制单已更新")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-preps/{prep_id}/complete")
|
|
async def complete_production_material_prep(
|
|
prep_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
data = prep_svc.complete_prep(
|
|
db,
|
|
prep_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
)
|
|
return success_response(data, message="配制已完成")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/production-material-preps/{prep_id}/cancel")
|
|
async def cancel_production_material_prep(
|
|
prep_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
prep_svc.cancel_prep(db, prep_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="配制单已作废")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/production-material-preps/{prep_id}")
|
|
async def delete_production_material_prep(
|
|
prep_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
prep_svc.delete_prep(db, prep_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="配制单已删除")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 一次性配制(简化流程)----------
|
|
|
|
|
|
class QuickPrepLineBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
requisitionUsageId: str = Field(..., alias="requisitionUsageId")
|
|
componentMaterialId: str = Field(..., alias="componentMaterialId")
|
|
componentMaterialType: str = Field(..., alias="componentMaterialType")
|
|
batchId: Optional[str] = Field(None, alias="batchId")
|
|
consumptionQty: float = Field(..., gt=0, alias="consumptionQty")
|
|
usageUnit: Optional[str] = Field(None, alias="usageUnit")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class QuickPrepBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
configuredMaterialName: str = Field(..., max_length=128, alias="configuredMaterialName")
|
|
outputQty: float = Field(..., gt=0, alias="outputQty")
|
|
lines: List[QuickPrepLineBody] = Field(..., min_length=1)
|
|
specModel: Optional[str] = Field(None, alias="specModel")
|
|
outputUnit: Optional[str] = Field(None, alias="outputUnit")
|
|
stockInNo: Optional[str] = Field(None, alias="stockInNo")
|
|
stockInDate: Optional[str] = Field(None, alias="stockInDate")
|
|
catalogNo: Optional[str] = Field(None, alias="catalogNo")
|
|
batchNo: Optional[str] = Field(None, alias="batchNo")
|
|
manufacturer: Optional[str] = Field(None, alias="manufacturer")
|
|
storageLocation: Optional[str] = Field(None, alias="storageLocation")
|
|
expireDate: Optional[str] = Field(None, alias="expireDate")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.post("/production-material-preps/quick")
|
|
async def quick_production_material_prep(
|
|
body: QuickPrepBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""一次性配制:提交即完成(子物料扣减 → 配置试剂入库 → 自动出库)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
data = prep_svc.create_quick_prep(
|
|
db,
|
|
data=body.model_dump(by_alias=True),
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
)
|
|
return success_response(data, message="配置试剂配制完成")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/production-material-consumptions")
|
|
async def list_production_material_consumptions(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:material:list")),
|
|
):
|
|
"""查询配制消耗明细(子物料消耗记录)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mu_svc.list_prep_consumptions(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
material_type=materialType,
|
|
start_time=startTime,
|
|
end_time=endTime,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/my-production-material-consumptions")
|
|
async def list_my_production_material_consumptions(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
materialType: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("production:my-material-usage:list")),
|
|
):
|
|
"""当前登录人的配制消耗明细(我的物料领用记录 - 消耗明细 Tab)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mu_svc.list_my_prep_consumptions(
|
|
db,
|
|
tenant_no,
|
|
user_id=_get_current_user_id(user),
|
|
nick_name=(user.get("real_name") or "").strip(),
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
material_type=materialType,
|
|
start_time=startTime,
|
|
end_time=endTime,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|