"""市场物料库 CRUD""" from __future__ import annotations from datetime import datetime from decimal import Decimal from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import func, or_ from sqlalchemy.orm import Session from app.core.utils import generate_uuid from app.models.material import BizMaterial, BizMaterialStockLog def _dec(v: Any) -> float: if v is None: return 0.0 return float(v) def _fmt_dt(t: Optional[datetime]) -> str: return t.strftime("%Y-%m-%d %H:%M:%S") if t else "" def _row_to_dict(r: BizMaterial) -> Dict[str, Any]: return { "id": r.id, "materialCode": r.material_code, "materialName": r.material_name, "category": r.category or "", "specModel": r.spec_model or "", "unit": r.unit or "", "stockQty": _dec(r.stock_qty), "status": r.status or "1", "imageUrls": r.image_urls or "", "orderNum": r.order_num if r.order_num is not None else 0, "remark": r.remark or "", "createTime": _fmt_dt(r.create_time), "updateTime": _fmt_dt(r.update_time), } def _code_exists( db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None ) -> bool: c = (code or "").strip() if not c: return False q = db.query(BizMaterial).filter( BizMaterial.material_code == c, BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, ) if exclude_id: q = q.filter(BizMaterial.id != exclude_id) return q.first() is not None def list_materials( db: Session, tenant_no: str, *, page: int = 1, page_size: int = 10, keyword: Optional[str] = None, category: Optional[str] = None, status: Optional[str] = None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): q0 = db.query(BizMaterial).filter( BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, ) kw = (keyword or "").strip() if kw: like = f"%{kw}%" q0 = q0.filter( or_( BizMaterial.material_code.ilike(like), BizMaterial.material_name.ilike(like), ) ) cat = (category or "").strip() if cat: q0 = q0.filter(BizMaterial.category == cat) st = (status or "").strip() if st in ("0", "1"): q0 = q0.filter(BizMaterial.status == st) return q0 total = _filtered().order_by(None).with_entities(func.count(BizMaterial.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) rows = ( _filtered() .order_by(BizMaterial.create_time.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() ) return [_row_to_dict(x) for x in rows], int(total) def get_material( db: Session, material_id: str, tenant_no: str ) -> Optional[Dict[str, Any]]: obj = ( db.query(BizMaterial) .filter( BizMaterial.id == material_id, BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, ) .first() ) if not obj: return None return _row_to_dict(obj) def create_material( db: Session, data: Dict[str, Any], *, tenant_no: str, user_id: Optional[str] = None, dept_id: str = "", ) -> Dict[str, Any]: code = (data.get("materialCode") or "").strip() if not code: raise ValueError("物料编码不能为空") if _code_exists(db, code, tenant_no): raise ValueError("物料编码已存在") name = (data.get("materialName") or "").strip() if not name: raise ValueError("物料名称不能为空") status = (data.get("status") or "1").strip() if status not in ("0", "1"): raise ValueError("状态无效") obj = BizMaterial( id=generate_uuid(), material_code=code, material_name=name, category=(data.get("category") or "").strip() or None, spec_model=(data.get("specModel") or "").strip() or None, unit=(data.get("unit") or "").strip() or None, stock_qty=0, status=status, image_urls=(data.get("imageUrls") or "").strip() or None, tenant_no=tenant_no, order_num=data.get("orderNum") if data.get("orderNum") is not None else 0, remark=(data.get("remark") or "").strip() or None, create_by=user_id, create_dept_id=dept_id or "", create_time=datetime.now(), update_by=user_id, update_time=datetime.now(), del_flag=0, ) db.add(obj) db.commit() db.refresh(obj) return _row_to_dict(obj) def update_material( db: Session, material_id: str, data: Dict[str, Any], *, tenant_no: str, user_id: Optional[str] = None, ) -> Dict[str, Any]: obj = ( db.query(BizMaterial) .filter( BizMaterial.id == material_id, BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, ) .first() ) if not obj: raise ValueError("物料不存在") if "materialCode" in data and data["materialCode"] is not None: code = data["materialCode"].strip() if not code: raise ValueError("物料编码不能为空") if code != obj.material_code and _code_exists(db, code, tenant_no, exclude_id=material_id): raise ValueError("物料编码已存在") obj.material_code = code if "materialName" in data and data["materialName"] is not None: name = data["materialName"].strip() if not name: raise ValueError("物料名称不能为空") obj.material_name = name if "category" in data: obj.category = (data["category"] or "").strip() or None if "specModel" in data: obj.spec_model = (data["specModel"] or "").strip() or None if "unit" in data: obj.unit = (data["unit"] or "").strip() or None if "status" in data and data["status"] is not None: st = data["status"].strip() if st not in ("0", "1"): raise ValueError("状态无效") obj.status = st if "imageUrls" in data: obj.image_urls = (data["imageUrls"] or "").strip() or None if "orderNum" in data: obj.order_num = data["orderNum"] if "remark" in data: obj.remark = (data["remark"] or "").strip() or None obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) db.commit() db.refresh(obj) return _row_to_dict(obj) def soft_delete_material( db: Session, material_id: str, tenant_no: str, user_id: Optional[str] = None, ) -> None: obj = ( db.query(BizMaterial) .filter( BizMaterial.id == material_id, BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, ) .first() ) if not obj: raise ValueError("物料不存在") if obj.stock_qty is not None and _dec(obj.stock_qty) > 0: raise ValueError("该物料仍有库存,无法删除") obj.del_flag = 1 obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) db.commit() def stock_in( db: Session, material_id: str, *, qty: float, remark: Optional[str] = None, tenant_no: str, user_id: Optional[str] = None, dept_id: str = "", ) -> Dict[str, Any]: if qty is None or qty <= 0: raise ValueError("入库数量必须大于0") qty_dec = Decimal(str(qty)) obj = ( db.query(BizMaterial) .filter( BizMaterial.id == material_id, BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, ) .with_for_update() .first() ) if not obj: raise ValueError("物料不存在") stock_before = obj.stock_qty if obj.stock_qty is not None else Decimal("0") stock_after = stock_before + qty_dec obj.stock_qty = stock_after obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) log = BizMaterialStockLog( id=generate_uuid(), material_id=obj.id, material_code=obj.material_code, material_name=obj.material_name, change_type="rk", change_qty=qty_dec, stock_before=stock_before, stock_after=stock_after, tenant_no=tenant_no, remark=(remark or "").strip() or None, create_by=user_id, create_dept_id=dept_id or "", create_time=datetime.now(), update_by=user_id, update_time=datetime.now(), del_flag=0, ) db.add(log) db.commit() db.refresh(obj) return _row_to_dict(obj) def stock_out( db: Session, material_id: str, *, qty: float, receiver: Optional[str] = None, purpose: Optional[str] = None, remark: Optional[str] = None, tenant_no: str, user_id: Optional[str] = None, dept_id: str = "", ) -> Dict[str, Any]: if qty is None or qty <= 0: raise ValueError("出库数量必须大于0") qty_dec = Decimal(str(qty)) obj = ( db.query(BizMaterial) .filter( BizMaterial.id == material_id, BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, ) .with_for_update() .first() ) if not obj: raise ValueError("物料不存在") stock_before = obj.stock_qty if obj.stock_qty is not None else Decimal("0") if stock_before < qty_dec: raise ValueError( f"物料「{obj.material_name}」库存不足,当前库存 {stock_before},需出库 {qty_dec}" ) stock_after = stock_before - qty_dec obj.stock_qty = stock_after obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) log = BizMaterialStockLog( id=generate_uuid(), material_id=obj.id, material_code=obj.material_code, material_name=obj.material_name, change_type="ck", change_qty=qty_dec, stock_before=stock_before, stock_after=stock_after, receiver=(receiver or "").strip() or None, purpose=(purpose or "").strip() or None, tenant_no=tenant_no, remark=(remark or "").strip() or None, create_by=user_id, create_dept_id=dept_id or "", create_time=datetime.now(), update_by=user_id, update_time=datetime.now(), del_flag=0, ) db.add(log) db.commit() db.refresh(obj) return _row_to_dict(obj) def options_list(db: Session, tenant_no: str) -> List[Dict[str, Any]]: rows = ( db.query(BizMaterial) .filter( BizMaterial.tenant_no == tenant_no, BizMaterial.del_flag == 0, BizMaterial.status == "1", ) .order_by(BizMaterial.create_time.desc()) .all() ) return [ { "id": r.id, "label": r.material_name, "materialCode": r.material_code, "materialName": r.material_name, "specModel": r.spec_model or "", "unit": r.unit or "", "stockQty": _dec(r.stock_qty), } for r in rows ]