| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337 |
- """生产物料库 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)
- id: Optional[str] = None
- 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)
- id: Optional[str] = None
- 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))
|