Files
zkxw/backend/app/crud/material_usage.py
T
2026-07-01 16:37:33 +08:00

423 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""市场物料变动记录 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()