| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- """市场物料变动记录 CRUD"""
- from __future__ import annotations
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Tuple
- from sqlalchemy import func, or_
- from sqlalchemy.orm import Session
- from app.models.material import BizMaterialStockBatch, BizMaterialUsage
- from app.models.models import SysUser
- def _parse_receiver_purpose_from_remark(remark: Optional[str]) -> Tuple[str, str]:
- receiver, purpose = "", ""
- for part in (remark or "").split(";"):
- seg = part.strip()
- if seg.startswith("领用人:"):
- receiver = seg[len("领用人:") :].strip()
- elif seg.startswith("用途:"):
- purpose = seg[len("用途:") :].strip()
- return receiver, purpose
- def _resolve_receiver_purpose(r: BizMaterialUsage) -> Tuple[str, str]:
- receiver = (getattr(r, "receiver", None) or "").strip()
- purpose = (getattr(r, "purpose", None) or "").strip()
- if receiver or purpose:
- return receiver, purpose
- if (r.source_type or "") == "manual_out":
- return _parse_receiver_purpose_from_remark(r.remark)
- return "", ""
- def _display_remark(r: BizMaterialUsage) -> str:
- remark = (r.remark or "").strip()
- if (r.source_type or "") != "manual_out" or not remark:
- return remark
- parts: List[str] = []
- for part in remark.split(";"):
- seg = part.strip()
- if not seg or seg.startswith("领用人:") or seg.startswith("用途:"):
- continue
- if seg.startswith("备注"):
- seg = seg[2:].strip() or seg
- parts.append(seg)
- return ";".join(parts) if parts else ""
- def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
- ids = list({x for x in user_ids if x})
- if not ids:
- return {}
- rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
- return {u.id: (u.nick_name or "").strip() for u in rows}
- def _receiver_display_map(db: Session, receivers: List[str]) -> Dict[str, str]:
- keys = list({(x or "").strip() for x in receivers if (x or "").strip()})
- if not keys:
- return {}
- by_id = _user_nick_name_map(db, keys)
- out: Dict[str, str] = {}
- for k in keys:
- out[k] = by_id.get(k) or k
- return out
- def _usage_unit_label(r: BizMaterialUsage, batch: Optional[BizMaterialStockBatch]) -> str:
- u = (getattr(r, "usage_unit", None) or "").strip()
- if u:
- return u
- if batch and (batch.unit or "").strip():
- return (batch.unit or "").strip()
- return ""
- def _stock_in_date_for_row(r: BizMaterialUsage, batch: Optional[BizMaterialStockBatch]) -> str:
- if batch and batch.stock_in_date:
- return batch.stock_in_date.strftime("%Y-%m-%d")
- return ""
- def _unit_price_for_row(r: BizMaterialUsage, batch: Optional[BizMaterialStockBatch]) -> Optional[float]:
- if r.unit_price is not None:
- return float(r.unit_price)
- if batch and batch.unit_price is not None:
- return float(batch.unit_price)
- return None
- def _amount_for_row(r: BizMaterialUsage, batch: Optional[BizMaterialStockBatch]) -> Optional[float]:
- is_in = (r.change_type or "") == "in"
- if is_in:
- if r.amount is not None:
- return float(r.amount)
- if batch and batch.amount is not None:
- return float(batch.amount)
- return None
- unit_price = _unit_price_for_row(r, batch)
- change_qty = float(r.change_qty) if r.change_qty is not None else 0
- qty = abs(change_qty)
- if unit_price is None or qty <= 0:
- return None
- return round(unit_price * qty, 2)
- def _row_to_dict(
- r: BizMaterialUsage,
- *,
- create_by_name: str = "",
- batch: Optional[BizMaterialStockBatch] = None,
- receiver_name: str = "",
- ) -> Dict[str, Any]:
- create_by = r.create_by or ""
- receiver, purpose = _resolve_receiver_purpose(r)
- create_time = r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else ""
- is_manual_out = (r.source_type or "") == "manual_out"
- stock_after = float(r.stock_after) if r.stock_after is not None else 0
- change_qty = float(r.change_qty) if r.change_qty is not None else 0
- stock_before = (
- float(r.stock_before)
- if r.stock_before is not None
- else stock_after - change_qty
- )
- usage_unit = _usage_unit_label(r, batch)
- return {
- "id": r.id,
- "materialId": r.material_id,
- "materialCode": r.material_code or "",
- "materialName": r.material_name or "",
- "batchId": r.batch_id or "",
- "stockInNo": r.stock_in_no or "",
- "stockInDate": _stock_in_date_for_row(r, batch),
- "catalogNo": r.catalog_no or "",
- "batchNo": r.batch_no or "",
- "manufacturer": r.manufacturer or "",
- "unitPrice": _unit_price_for_row(r, batch),
- "amount": _amount_for_row(r, batch),
- "changeType": r.change_type,
- "changeQty": change_qty,
- "usageUnit": usage_unit,
- "stockBefore": stock_before,
- "stockAfter": stock_after,
- "receiver": receiver,
- "receiverName": receiver_name or receiver,
- "operatorName": (getattr(r, "operator_name", None) or "").strip(),
- "purpose": purpose,
- "sourceType": r.source_type or "",
- "requisitionStatus": (getattr(r, "requisition_status", None) or "open").strip() or "open",
- "useTime": create_time if is_manual_out else "",
- "remark": _display_remark(r),
- "createBy": create_by,
- "createByName": create_by_name or create_by,
- "createTime": create_time,
- "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
- }
- def _batch_map(db: Session, batch_ids: List[str]) -> Dict[str, BizMaterialStockBatch]:
- ids = [x for x in batch_ids if x]
- if not ids:
- return {}
- rows = (
- db.query(BizMaterialStockBatch)
- .filter(BizMaterialStockBatch.id.in_(ids), BizMaterialStockBatch.del_flag == 0)
- .all()
- )
- return {r.id: r for r in rows}
- def _list_usages_query(
- db: Session,
- tenant_no: str,
- *,
- material_id: Optional[str] = None,
- change_type: Optional[str] = None,
- start_time: Optional[str] = None,
- end_time: Optional[str] = None,
- keyword: Optional[str] = None,
- stock_in_no: Optional[str] = None,
- user_info=None,
- ):
- q0 = db.query(BizMaterialUsage).filter(
- BizMaterialUsage.tenant_no == tenant_no,
- BizMaterialUsage.del_flag == 0,
- )
- if material_id:
- q0 = q0.filter(BizMaterialUsage.material_id == material_id)
- ct = (change_type or "").strip()
- if ct == "out":
- q0 = q0.filter(
- BizMaterialUsage.change_type == "use",
- BizMaterialUsage.source_type == "manual_out",
- )
- elif ct == "in":
- q0 = q0.filter(BizMaterialUsage.change_type == "in")
- elif ct == "discard":
- q0 = q0.filter(BizMaterialUsage.change_type == "discard")
- sno = (stock_in_no or "").strip()
- if sno:
- q0 = q0.filter(BizMaterialUsage.stock_in_no.like(f"%{sno}%"))
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizMaterialUsage.material_code.like(like),
- BizMaterialUsage.material_name.like(like),
- BizMaterialUsage.stock_in_no.like(like),
- BizMaterialUsage.batch_no.like(like),
- BizMaterialUsage.catalog_no.like(like),
- )
- )
- if start_time:
- q0 = q0.filter(BizMaterialUsage.create_time >= start_time)
- if end_time:
- q0 = q0.filter(BizMaterialUsage.create_time <= end_time + " 23:59:59")
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizMaterialUsage, user_info, db, tenant_no)
- return q0
- def _paginate_usages(db: Session, q0, *, page: int, page_size: int) -> Tuple[List[Dict[str, Any]], int]:
- total = q0.order_by(None).with_entities(func.count(BizMaterialUsage.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = (
- q0.order_by(BizMaterialUsage.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- bmap = _batch_map(db, [x.batch_id for x in rows if x.batch_id])
- name_map = _user_nick_name_map(db, [x.create_by for x in rows if x.create_by])
- receivers = []
- for x in rows:
- rcv, _ = _resolve_receiver_purpose(x)
- if rcv:
- receivers.append(rcv)
- receiver_map = _receiver_display_map(db, receivers)
- return [
- _row_to_dict(
- x,
- create_by_name=name_map.get(x.create_by or "", ""),
- batch=bmap.get(x.batch_id or ""),
- receiver_name=receiver_map.get(
- (_resolve_receiver_purpose(x)[0] or ""), _resolve_receiver_purpose(x)[0] or ""
- ),
- )
- for x in rows
- ], int(total)
- def list_usages(
- db: Session,
- tenant_no: str,
- *,
- page: int = 1,
- page_size: int = 10,
- material_id: Optional[str] = None,
- change_type: Optional[str] = None,
- start_time: Optional[str] = None,
- end_time: Optional[str] = None,
- keyword: Optional[str] = None,
- stock_in_no: Optional[str] = None,
- user_info=None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- q0 = _list_usages_query(
- db,
- tenant_no,
- material_id=material_id,
- change_type=change_type,
- start_time=start_time,
- end_time=end_time,
- keyword=keyword,
- stock_in_no=stock_in_no,
- user_info=user_info,
- )
- return _paginate_usages(db, q0, page=page, page_size=page_size)
- def _fmt_date_val(v) -> str:
- if v is None:
- return ""
- if hasattr(v, "strftime"):
- return v.strftime("%Y-%m-%d")
- return str(v)[:10]
- def list_stock_check_requisitions(
- db: Session,
- tenant_no: str,
- *,
- page: int = 1,
- page_size: int = 50,
- keyword: Optional[str] = None,
- user_info=None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- """盘点登记弹窗:待处理领用记录(未标记已使用完)。"""
- q0 = db.query(BizMaterialUsage).filter(
- BizMaterialUsage.tenant_no == tenant_no,
- BizMaterialUsage.del_flag == 0,
- BizMaterialUsage.change_type == "use",
- BizMaterialUsage.source_type == "manual_out",
- BizMaterialUsage.requisition_status == "open",
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizMaterialUsage, user_info, db, tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizMaterialUsage.material_code.like(like),
- BizMaterialUsage.material_name.like(like),
- BizMaterialUsage.stock_in_no.like(like),
- BizMaterialUsage.batch_no.like(like),
- BizMaterialUsage.catalog_no.like(like),
- BizMaterialUsage.receiver.like(like),
- )
- )
- total = q0.order_by(None).with_entities(func.count(BizMaterialUsage.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 200)
- rows = (
- q0.order_by(BizMaterialUsage.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- bmap = _batch_map(db, [x.batch_id for x in rows if x.batch_id])
- name_map = _user_nick_name_map(db, [x.create_by for x in rows if x.create_by])
- receivers = []
- for r in rows:
- rcv, _ = _resolve_receiver_purpose(r)
- if rcv:
- receivers.append(rcv)
- receiver_map = _receiver_display_map(db, receivers)
- out: List[Dict[str, Any]] = []
- for r in rows:
- batch = bmap.get(r.batch_id or "")
- rcv, purpose = _resolve_receiver_purpose(r)
- rcv_name = receiver_map.get(rcv, rcv) if rcv else ""
- qty_abs = abs(float(r.change_qty or 0))
- unit = _usage_unit_label(r, batch)
- use_time = r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else ""
- out.append(
- {
- "id": r.id,
- "materialId": r.material_id,
- "materialCode": r.material_code or "",
- "materialName": r.material_name or "",
- "specModel": (batch.spec_model if batch else "") or "",
- "batchId": r.batch_id or "",
- "stockInNo": r.stock_in_no or "",
- "stockInDate": _stock_in_date_for_row(r, batch),
- "expireDate": _fmt_date_val(batch.expire_date if batch else None),
- "catalogNo": r.catalog_no or "",
- "batchNo": r.batch_no or "",
- "manufacturer": r.manufacturer or "",
- "unitPrice": _unit_price_for_row(r, batch),
- "unit": unit,
- "requisitionQty": qty_abs,
- "taskConsumedQty": 0,
- "receiver": rcv,
- "receiverName": rcv_name,
- "operatorName": (getattr(r, "operator_name", None) or "").strip(),
- "purpose": purpose,
- "useTime": use_time,
- "requisitionStatus": (r.requisition_status or "open").strip() or "open",
- "createByName": name_map.get(r.create_by or "", r.create_by or ""),
- }
- )
- return out, int(total)
- def mark_requisition_fully_used(
- db: Session,
- usage_id: str,
- *,
- tenant_no: str,
- user_id: Optional[str] = None,
- user_info=None,
- ) -> None:
- row = (
- db.query(BizMaterialUsage)
- .filter(
- BizMaterialUsage.id == usage_id,
- BizMaterialUsage.tenant_no == tenant_no,
- BizMaterialUsage.del_flag == 0,
- BizMaterialUsage.change_type == "use",
- BizMaterialUsage.source_type == "manual_out",
- )
- .first()
- )
- if not row:
- raise ValueError("领用记录不存在或不可标记")
- if user_info:
- from app.core.data_scope import apply_data_scope
- scoped = apply_data_scope(
- db.query(BizMaterialUsage).filter(BizMaterialUsage.id == usage_id),
- BizMaterialUsage,
- user_info,
- db,
- tenant_no,
- )
- if not scoped.first():
- raise PermissionError("无权操作该领用记录")
- if (row.requisition_status or "open") == "closed":
- return
- now = datetime.now()
- row.requisition_status = "closed"
- row.update_by = user_id
- row.update_time = now
- db.add(row)
- db.commit()
|