| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394 |
- """物料变动记录 CRUD"""
- from __future__ import annotations
- from datetime import datetime
- from decimal import Decimal
- from typing import Any, Dict, List, Optional, Tuple
- from fastapi import HTTPException
- from sqlalchemy import and_, func, or_
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.crud.production_material import _filter_material_type
- from app.models.production_material import BizProductionMaterial, BizProductionMaterialPrep, BizProductionMaterialStockBatch, BizProductionMaterialUsage
- from app.models.models import SysUser
- from app.crud.production_material_stock_batch import _usage_batch_snapshots
- def _parse_receiver_purpose_from_remark(remark: Optional[str]) -> Tuple[str, str]:
- """兼容旧数据:领用人/用途曾拼接在 remark 中。"""
- 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: BizProductionMaterialUsage) -> 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: BizProductionMaterialUsage) -> 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]:
- """领用人字段可能是用户 ID 或昵称。"""
- 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: BizProductionMaterialUsage, batch: Optional[BizProductionMaterialStockBatch]) -> 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: BizProductionMaterialUsage, batch: Optional[BizProductionMaterialStockBatch]) -> str:
- if batch and batch.stock_in_date:
- return batch.stock_in_date.strftime("%Y-%m-%d")
- return ""
- def _unit_price_for_row(r: BizProductionMaterialUsage, batch: Optional[BizProductionMaterialStockBatch]) -> 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: BizProductionMaterialUsage, batch: Optional[BizProductionMaterialStockBatch]) -> 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: BizProductionMaterialUsage,
- *,
- create_by_name: str = "",
- batch: Optional[BizProductionMaterialStockBatch] = None,
- receiver_name: str = "",
- requisition_receiver: str = "",
- requisition_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)
- req_receiver = requisition_receiver or receiver
- req_receiver_name = requisition_receiver_name or receiver_name or req_receiver
- return {
- "id": r.id,
- "materialId": r.material_id,
- "materialType": r.material_type or "",
- "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 "",
- "storageLocation": (r.storage_location or "").strip() or (batch.storage_location if batch else "") 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,
- "stepId": r.step_id or "",
- "stepName": r.step_name or "",
- "taskId": r.task_id or "",
- "taskNo": r.task_no or "",
- "parentUsageId": (r.parent_usage_id or "").strip(),
- "requisitionUsageId": (r.parent_usage_id or "").strip(),
- "prepId": (r.prep_id or "").strip(),
- "receiver": receiver,
- "receiverName": receiver_name or receiver,
- "requisitionReceiver": req_receiver,
- "requisitionReceiverName": req_receiver_name,
- "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, BizProductionMaterialStockBatch]:
- ids = [x for x in batch_ids if x]
- if not ids:
- return {}
- rows = (
- db.query(BizProductionMaterialStockBatch)
- .filter(BizProductionMaterialStockBatch.id.in_(ids), BizProductionMaterialStockBatch.del_flag == 0)
- .all()
- )
- return {r.id: r for r in rows}
- def _mine_usage_filter(user_id: str, nick_name: str):
- """当前登录人的领用记录:手动出库(领用人)或任务使用(填报人)。"""
- uid = (user_id or "").strip()
- nn = (nick_name or "").strip()
- manual_conds = [BizProductionMaterialUsage.source_type == "manual_out"]
- receiver_match: List[Any] = []
- if nn:
- receiver_match.append(BizProductionMaterialUsage.receiver == nn)
- receiver_match.append(BizProductionMaterialUsage.remark.like(f"%领用人:{nn}%"))
- if uid:
- receiver_match.append(BizProductionMaterialUsage.receiver == uid)
- if receiver_match:
- manual_conds.append(or_(*receiver_match))
- else:
- manual_conds.append(BizProductionMaterialUsage.id == "")
- task_conds = [
- BizProductionMaterialUsage.source_type == "step_save",
- BizProductionMaterialUsage.create_by == uid,
- ]
- return and_(
- BizProductionMaterialUsage.change_type == "use",
- or_(and_(*manual_conds), and_(*task_conds)),
- )
- def _list_usages_query(
- db: Session,
- tenant_no: str,
- *,
- material_id: Optional[str] = None,
- material_type: 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,
- mine_user_id: Optional[str] = None,
- mine_nick_name: Optional[str] = None,
- ):
- q0 = db.query(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- or_(
- BizProductionMaterialUsage.source_type.is_(None),
- BizProductionMaterialUsage.source_type != "prep_consume",
- ),
- )
- if mine_user_id is not None:
- q0 = q0.filter(_mine_usage_filter(mine_user_id, mine_nick_name or ""))
- if material_id:
- q0 = q0.filter(BizProductionMaterialUsage.material_id == material_id)
- mt = _filter_material_type(material_type)
- if mt:
- q0 = q0.filter(BizProductionMaterialUsage.material_type == mt)
- ct = (change_type or "").strip()
- if ct == "out":
- q0 = q0.filter(
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "manual_out",
- )
- elif ct == "use":
- q0 = q0.filter(
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "step_save",
- )
- elif ct == "in":
- q0 = q0.filter(BizProductionMaterialUsage.change_type == "in")
- elif ct == "discard":
- q0 = q0.filter(BizProductionMaterialUsage.change_type == "discard")
- sno = (stock_in_no or "").strip()
- if sno:
- q0 = q0.filter(BizProductionMaterialUsage.stock_in_no.like(f"%{sno}%"))
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizProductionMaterialUsage.material_code.like(like),
- BizProductionMaterialUsage.material_name.like(like),
- BizProductionMaterialUsage.stock_in_no.like(like),
- BizProductionMaterialUsage.batch_no.like(like),
- BizProductionMaterialUsage.catalog_no.like(like),
- )
- )
- if start_time:
- q0 = q0.filter(BizProductionMaterialUsage.create_time >= start_time)
- if end_time:
- q0 = q0.filter(BizProductionMaterialUsage.create_time <= end_time + " 23:59:59")
- if user_info and mine_user_id is None:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizProductionMaterialUsage, 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(BizProductionMaterialUsage.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = (
- q0.order_by(BizProductionMaterialUsage.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,
- material_type: 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,
- material_type=material_type,
- 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 list_my_usages(
- db: Session,
- tenant_no: str,
- *,
- user_id: str,
- nick_name: str = "",
- page: int = 1,
- page_size: int = 10,
- material_id: Optional[str] = None,
- material_type: 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,
- ) -> Tuple[List[Dict[str, Any]], int]:
- """当前登录人的物料领用记录(手动出库 + 任务使用)。"""
- q0 = _list_usages_query(
- db,
- tenant_no,
- material_id=material_id,
- material_type=material_type,
- change_type=change_type,
- start_time=start_time,
- end_time=end_time,
- keyword=keyword,
- stock_in_no=stock_in_no,
- mine_user_id=user_id,
- mine_nick_name=nick_name,
- )
- return _paginate_usages(db, q0, page=page, page_size=page_size)
- def list_usages_by_step(
- db: Session,
- step_id: str,
- tenant_no: str,
- *,
- user_info=None,
- ) -> List[Dict[str, Any]]:
- """查询生产步骤关联的物料使用记录(step_save)。"""
- q = db.query(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.step_id == step_id,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "step_save",
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizProductionMaterialUsage, user_info, db, tenant_no)
- rows = q.order_by(BizProductionMaterialUsage.create_time.asc()).all()
- bmap = _batch_map(db, [x.batch_id for x in rows if x.batch_id])
- parent_ids = [x.parent_usage_id for x in rows if (x.parent_usage_id or "").strip()]
- parent_map: Dict[str, BizProductionMaterialUsage] = {}
- if parent_ids:
- parents = (
- db.query(BizProductionMaterialUsage)
- .filter(
- BizProductionMaterialUsage.id.in_(parent_ids),
- BizProductionMaterialUsage.del_flag == 0,
- )
- .all()
- )
- parent_map = {p.id: p for p in parents}
- parent_receivers = []
- for p in parent_map.values():
- rcv, _ = _resolve_receiver_purpose(p)
- if rcv:
- parent_receivers.append(rcv)
- parent_receiver_map = _receiver_display_map(db, parent_receivers)
- out: List[Dict[str, Any]] = []
- for x in rows:
- parent = parent_map.get((x.parent_usage_id or "").strip())
- req_rcv, _ = _resolve_receiver_purpose(parent) if parent else ("", "")
- req_rcv_name = parent_receiver_map.get(req_rcv, req_rcv) if req_rcv else ""
- rcv, _ = _resolve_receiver_purpose(x)
- out.append(
- _row_to_dict(
- x,
- batch=bmap.get(x.batch_id or ""),
- receiver_name=parent_receiver_map.get(rcv, rcv) if not parent else req_rcv_name,
- requisition_receiver=req_rcv,
- requisition_receiver_name=req_rcv_name,
- )
- )
- return out
- def list_linked_step_usages(
- db: Session,
- parent_usage_id: str,
- tenant_no: str,
- *,
- user_info=None,
- ) -> List[Dict[str, Any]]:
- """查询领用记录关联的消耗明细(parent_usage_id → step_save / prep_consume)。"""
- pid = (parent_usage_id or "").strip()
- if not pid:
- return []
- parent = (
- db.query(BizProductionMaterialUsage)
- .filter(
- BizProductionMaterialUsage.id == pid,
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- )
- .first()
- )
- if not parent:
- raise HTTPException(status_code=404, detail="领用记录不存在")
- if (parent.source_type or "") != "manual_out":
- return []
- q = db.query(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.parent_usage_id == pid,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type.in_(["step_save", "prep_consume"]),
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizProductionMaterialUsage, user_info, db, tenant_no)
- rows = q.order_by(BizProductionMaterialUsage.create_time.desc()).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])
- prep_ids = list({(x.prep_id or "").strip() for x in rows if (x.prep_id or "").strip()})
- prep_map: Dict[str, Dict[str, str]] = {}
- if prep_ids:
- preps = (
- db.query(BizProductionMaterialPrep)
- .filter(
- BizProductionMaterialPrep.id.in_(prep_ids),
- BizProductionMaterialPrep.tenant_no == tenant_no,
- BizProductionMaterialPrep.del_flag == 0,
- )
- .all()
- )
- prep_map = {
- p.id: {
- "prepNo": p.prep_no or "",
- "configuredMaterialName": p.configured_material_name or "",
- }
- for p in preps
- }
- out: List[Dict[str, Any]] = []
- for x in rows:
- item = _row_to_dict(
- x,
- create_by_name=name_map.get(x.create_by or "", ""),
- batch=bmap.get(x.batch_id or ""),
- requisition_receiver_name="",
- )
- item["productionTaskId"] = item.get("taskId") or ""
- item["productionTaskNo"] = item.get("taskNo") or ""
- prep_info = prep_map.get((x.prep_id or "").strip(), {})
- item["prepNo"] = prep_info.get("prepNo", "")
- item["configuredMaterialName"] = prep_info.get("configuredMaterialName", "")
- out.append(item)
- return out
- def revert_step_material_usages(
- db: Session,
- step_id: str,
- tenant_no: str,
- user_id: Optional[str] = None,
- ) -> None:
- """撤销步骤此前 step_save 扣减,便于重新填报物料使用。"""
- rows = (
- db.query(BizProductionMaterialUsage)
- .filter(
- BizProductionMaterialUsage.step_id == step_id,
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.source_type == "step_save",
- BizProductionMaterialUsage.change_type == "use",
- )
- .all()
- )
- if not rows:
- return
- now = datetime.now()
- for r in rows:
- if (r.parent_usage_id or "").strip():
- r.del_flag = 1
- r.update_by = user_id
- r.update_time = now
- continue
- restore_qty = abs(r.change_qty) if r.change_qty is not None else Decimal("0")
- if restore_qty <= 0:
- r.del_flag = 1
- r.update_by = user_id
- r.update_time = now
- continue
- batch_id = (r.batch_id or "").strip()
- if batch_id:
- batch = (
- db.query(BizProductionMaterialStockBatch)
- .with_for_update()
- .filter(
- BizProductionMaterialStockBatch.id == batch_id,
- BizProductionMaterialStockBatch.tenant_no == tenant_no,
- BizProductionMaterialStockBatch.del_flag == 0,
- )
- .first()
- )
- if batch:
- batch.remaining_qty = (batch.remaining_qty or Decimal("0")) + restore_qty
- batch.update_by = user_id
- batch.update_time = now
- material = (
- db.query(BizProductionMaterial)
- .with_for_update()
- .filter(
- BizProductionMaterial.id == r.material_id,
- BizProductionMaterial.tenant_no == tenant_no,
- BizProductionMaterial.del_flag == 0,
- )
- .first()
- )
- if material:
- material.stock_qty = (material.stock_qty or Decimal("0")) + restore_qty
- material.update_by = user_id
- material.update_time = now
- r.del_flag = 1
- r.update_by = user_id
- r.update_time = now
- def list_requisition_options(
- db: Session,
- tenant_no: str,
- *,
- keyword: Optional[str] = None,
- material_type: Optional[str] = None,
- limit: int = 50,
- user_info=None,
- ) -> List[Dict[str, Any]]:
- """领用记录选项(manual_out),供生产步骤关联物料使用。"""
- q0 = db.query(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "manual_out",
- )
- mt = _filter_material_type(material_type)
- if mt:
- q0 = q0.filter(BizProductionMaterialUsage.material_type == mt)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizProductionMaterialUsage, user_info, db, tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizProductionMaterialUsage.material_code.like(like),
- BizProductionMaterialUsage.material_name.like(like),
- BizProductionMaterialUsage.stock_in_no.like(like),
- BizProductionMaterialUsage.batch_no.like(like),
- BizProductionMaterialUsage.catalog_no.like(like),
- BizProductionMaterialUsage.receiver.like(like),
- )
- )
- limit = min(max(1, limit), 100)
- rows = q0.order_by(BizProductionMaterialUsage.create_time.desc()).limit(limit).all()
- bmap = _batch_map(db, [x.batch_id for x in rows if x.batch_id])
- receivers = []
- for r in rows:
- rcv, _ = _resolve_receiver_purpose(r)
- if rcv:
- receivers.append(rcv)
- receiver_map = _receiver_display_map(db, receivers)
- options: List[Dict[str, Any]] = []
- for r in rows:
- batch = bmap.get(r.batch_id or "")
- rcv, _ = _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 ""
- unit_suffix = unit or ""
- label = (
- f"{use_time} {r.material_name or ''} / {r.stock_in_no or ''} "
- f"/ 批号:{r.batch_no or '—'} / 领用:{qty_abs}{unit_suffix} / 领用人:{rcv_name or '—'}"
- ).strip()
- options.append(
- {
- "id": r.id,
- "label": label,
- "materialId": r.material_id,
- "materialType": r.material_type or "",
- "materialName": r.material_name or "",
- "stockInNo": r.stock_in_no or "",
- "batchNo": r.batch_no or "",
- "batchId": r.batch_id or "",
- "receiver": rcv,
- "receiverName": rcv_name,
- "requisitionQty": qty_abs,
- "usageUnit": unit,
- "useTime": use_time,
- }
- )
- return options
- def _record_step_usage_from_requisition(
- db: Session,
- step_id: str,
- step_name: str,
- task_id: str,
- task_no: str,
- item: Dict[str, Any],
- tenant_no: str,
- user_id: Optional[str] = None,
- dept_id: str = "",
- ) -> None:
- req_id = (
- item.get("requisitionUsageId")
- or item.get("requisition_usage_id")
- or item.get("parentUsageId")
- or item.get("parent_usage_id")
- or ""
- ).strip()
- if not req_id:
- raise HTTPException(status_code=400, detail="请选择领用记录")
- qty = item.get("qty")
- if qty is None or Decimal(str(qty)) <= 0:
- raise HTTPException(status_code=400, detail="物料使用数量必须大于0")
- qty_dec = Decimal(str(qty))
- req = (
- db.query(BizProductionMaterialUsage)
- .filter(
- BizProductionMaterialUsage.id == req_id,
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.source_type == "manual_out",
- BizProductionMaterialUsage.change_type == "use",
- )
- .first()
- )
- if not req:
- raise HTTPException(status_code=400, detail="领用记录不存在或已失效")
- material = (
- db.query(BizProductionMaterial)
- .filter(
- BizProductionMaterial.id == req.material_id,
- BizProductionMaterial.tenant_no == tenant_no,
- BizProductionMaterial.del_flag == 0,
- )
- .first()
- )
- if not material:
- raise HTTPException(status_code=400, detail="关联物料不存在")
- usage_unit = (
- str(item.get("usageUnit") or item.get("usage_unit") or "").strip()
- or _usage_unit_label(req, None)
- or ""
- )[:32]
- stock_qty = material.stock_qty if material.stock_qty is not None else Decimal("0")
- usage_remark = str(item.get("remark") or "").strip()[:512]
- now = datetime.now()
- usage = BizProductionMaterialUsage(
- id=generate_uuid(),
- material_id=req.material_id,
- material_type=req.material_type or material.material_type,
- material_code=req.material_code or material.material_code,
- material_name=req.material_name or material.material_name,
- batch_id=req.batch_id,
- stock_in_no=req.stock_in_no,
- catalog_no=req.catalog_no,
- batch_no=req.batch_no,
- manufacturer=req.manufacturer,
- storage_location=req.storage_location,
- unit_price=req.unit_price,
- amount=req.amount,
- change_type="use",
- change_qty=-qty_dec,
- usage_unit=usage_unit or None,
- stock_before=stock_qty,
- stock_after=stock_qty,
- step_id=step_id,
- step_name=step_name,
- task_id=task_id,
- task_no=task_no,
- parent_usage_id=req_id,
- source_type="step_save",
- tenant_no=tenant_no,
- remark=usage_remark,
- create_by=user_id,
- create_dept_id=dept_id,
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(usage)
- def _deduct_step_usage_legacy_batch(
- db: Session,
- step_id: str,
- step_name: str,
- task_id: str,
- task_no: str,
- item: Dict[str, Any],
- tenant_no: str,
- user_id: Optional[str] = None,
- dept_id: str = "",
- ) -> None:
- """旧版:按入库批次扣减库存(兼容历史数据)。"""
- batch_id = (item.get("batchId") or item.get("batch_id") or "").strip()
- qty = item.get("qty")
- if not batch_id:
- raise HTTPException(status_code=400, detail="入库批次不能为空")
- if qty is None or Decimal(str(qty)) <= 0:
- raise HTTPException(status_code=400, detail="物料使用数量必须大于0")
- qty_dec = Decimal(str(qty))
- batch = (
- db.query(BizProductionMaterialStockBatch)
- .with_for_update()
- .filter(
- BizProductionMaterialStockBatch.id == batch_id,
- BizProductionMaterialStockBatch.tenant_no == tenant_no,
- BizProductionMaterialStockBatch.del_flag == 0,
- )
- .first()
- )
- if not batch:
- raise HTTPException(status_code=400, detail=f"入库批次不存在(ID: {batch_id})")
- rem = batch.remaining_qty or Decimal("0")
- if rem < qty_dec:
- raise HTTPException(
- status_code=400,
- detail=f"批次「{batch.stock_in_no}」库存不足,当前剩余 {rem},需使用 {qty_dec}",
- )
- material = (
- db.query(BizProductionMaterial)
- .with_for_update()
- .filter(
- BizProductionMaterial.id == batch.material_id,
- BizProductionMaterial.tenant_no == tenant_no,
- BizProductionMaterial.del_flag == 0,
- BizProductionMaterial.status == "1",
- )
- .first()
- )
- if not material:
- raise HTTPException(status_code=400, detail="物料不存在或已停用")
- batch.remaining_qty = rem - qty_dec
- batch.update_by = user_id
- batch.update_time = datetime.now()
- db.add(batch)
- stock_qty = material.stock_qty if material.stock_qty is not None else Decimal("0")
- if stock_qty < qty_dec:
- raise HTTPException(
- status_code=400,
- detail=f"物料「{material.material_name}」汇总库存不足",
- )
- new_stock = stock_qty - qty_dec
- material.stock_qty = new_stock
- material.update_by = user_id
- material.update_time = datetime.now()
- db.add(material)
- usage_remark = str(item.get("remark") or "").strip()[:512]
- usage_unit = str(item.get("usageUnit") or item.get("usage_unit") or "").strip()[:32] or None
- snaps = _usage_batch_snapshots(batch, out_qty=qty_dec)
- usage = BizProductionMaterialUsage(
- id=generate_uuid(),
- material_id=material.id,
- material_type=material.material_type,
- material_code=material.material_code,
- material_name=material.material_name,
- change_type="use",
- change_qty=-qty_dec,
- usage_unit=usage_unit,
- stock_before=stock_qty,
- stock_after=new_stock,
- step_id=step_id,
- step_name=step_name,
- task_id=task_id,
- task_no=task_no,
- source_type="step_save",
- tenant_no=tenant_no,
- remark=usage_remark,
- create_by=user_id,
- create_dept_id=dept_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- **snaps,
- )
- db.add(usage)
- def register_step_usage_from_requisition(
- db: Session,
- step_id: str,
- step_name: str,
- task_id: str,
- task_no: str,
- material_usages: List[Dict[str, Any]],
- tenant_no: str,
- user_id: Optional[str] = None,
- dept_id: str = "",
- ) -> None:
- """登记步骤物料使用;关联领用记录时不扣减库存。调用方控制事务,本函数不 commit。"""
- revert_step_material_usages(db, step_id, tenant_no, user_id=user_id)
- seen_requisition: set = set()
- seen_batch: set = set()
- for item in material_usages:
- req_id = (
- item.get("requisitionUsageId")
- or item.get("requisition_usage_id")
- or item.get("parentUsageId")
- or ""
- ).strip()
- if req_id:
- if req_id in seen_requisition:
- raise HTTPException(status_code=400, detail="物料列表中存在重复领用记录,请检查")
- seen_requisition.add(req_id)
- _record_step_usage_from_requisition(
- db,
- step_id,
- step_name,
- task_id,
- task_no,
- item,
- tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- )
- continue
- batch_id = (item.get("batchId") or item.get("batch_id") or "").strip()
- if not batch_id:
- raise HTTPException(status_code=400, detail="请选择领用记录")
- if batch_id in seen_batch:
- raise HTTPException(status_code=400, detail="物料列表中存在重复入库批次,请检查")
- seen_batch.add(batch_id)
- _deduct_step_usage_legacy_batch(
- db,
- step_id,
- step_name,
- task_id,
- task_no,
- item,
- tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- )
- def list_available_requisitions_for_prep(
- db: Session,
- tenant_no: str,
- *,
- page: int = 1,
- page_size: int = 50,
- keyword: Optional[str] = None,
- material_type: Optional[str] = None,
- user_info=None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- """配制子物料来源:全部未标记已用完的领用记录。"""
- q0 = db.query(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "manual_out",
- BizProductionMaterialUsage.material_type.in_(["reagent", "consumable"]),
- BizProductionMaterialUsage.requisition_status != "closed",
- )
- mt = _filter_material_type(material_type)
- if mt:
- q0 = q0.filter(BizProductionMaterialUsage.material_type == mt)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizProductionMaterialUsage.material_code.like(like),
- BizProductionMaterialUsage.material_name.like(like),
- BizProductionMaterialUsage.stock_in_no.like(like),
- BizProductionMaterialUsage.batch_no.like(like),
- BizProductionMaterialUsage.catalog_no.like(like),
- BizProductionMaterialUsage.receiver.like(like),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizProductionMaterialUsage, user_info, db, tenant_no)
- total = q0.order_by(None).with_entities(func.count(BizProductionMaterialUsage.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 200)
- rows = (
- q0.order_by(BizProductionMaterialUsage.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])
- receivers = []
- for r in rows:
- rcv, _ = _resolve_receiver_purpose(r)
- if rcv:
- receivers.append(rcv)
- receiver_map = _receiver_display_map(db, receivers)
- items: List[Dict[str, Any]] = []
- for r in rows:
- batch = bmap.get(r.batch_id or "")
- rcv, _ = _resolve_receiver_purpose(r)
- rcv_name = receiver_map.get(rcv, rcv) if rcv else ""
- req_qty = 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 ""
- items.append(
- {
- "requisitionUsageId": r.id,
- "batchId": r.batch_id or "",
- "materialId": r.material_id,
- "materialType": r.material_type or "",
- "materialCode": r.material_code or "",
- "materialName": r.material_name or "",
- "specModel": (batch.spec_model if batch else "") or "",
- "unit": unit,
- "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 "",
- "storageLocation": (r.storage_location or "").strip() or (batch.storage_location if batch else "") or "",
- "requisitionQty": req_qty,
- "unitPrice": _unit_price_for_row(r, batch),
- "receiver": rcv,
- "receiverName": rcv_name,
- "useTime": use_time,
- }
- )
- return items, int(total)
- def _step_consumed_qty_map(
- db: Session,
- tenant_no: str,
- parent_usage_ids: List[str],
- ) -> Dict[str, float]:
- """统计领用记录关联的生产步骤消耗量。"""
- ids = [x for x in parent_usage_ids if x]
- if not ids:
- return {}
- rows = (
- db.query(
- BizProductionMaterialUsage.parent_usage_id,
- func.coalesce(func.sum(func.abs(BizProductionMaterialUsage.change_qty)), 0),
- )
- .filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.parent_usage_id.in_(ids),
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "step_save",
- )
- .group_by(BizProductionMaterialUsage.parent_usage_id)
- .all()
- )
- return {pid: float(qty or 0) for pid, qty in rows if pid}
- 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(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "manual_out",
- BizProductionMaterialUsage.requisition_status == "open",
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizProductionMaterialUsage, user_info, db, tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizProductionMaterialUsage.material_code.like(like),
- BizProductionMaterialUsage.material_name.like(like),
- BizProductionMaterialUsage.stock_in_no.like(like),
- BizProductionMaterialUsage.batch_no.like(like),
- BizProductionMaterialUsage.catalog_no.like(like),
- BizProductionMaterialUsage.receiver.like(like),
- )
- )
- total = q0.order_by(None).with_entities(func.count(BizProductionMaterialUsage.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 200)
- rows = (
- q0.order_by(BizProductionMaterialUsage.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)
- consumed_map = _step_consumed_qty_map(db, tenant_no, [x.id for x in rows])
- 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 ""
- task_consumed = consumed_map.get(r.id, 0)
- out.append(
- {
- "id": r.id,
- "materialId": r.material_id,
- "materialType": r.material_type or "",
- "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 "",
- "storageLocation": (r.storage_location or "").strip() or (batch.storage_location if batch else "") or "",
- "unitPrice": _unit_price_for_row(r, batch),
- "unit": unit,
- "requisitionQty": qty_abs,
- "taskConsumedQty": task_consumed,
- "receiver": rcv,
- "receiverName": rcv_name,
- "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(BizProductionMaterialUsage)
- .filter(
- BizProductionMaterialUsage.id == usage_id,
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.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(BizProductionMaterialUsage).filter(BizProductionMaterialUsage.id == usage_id),
- BizProductionMaterialUsage,
- 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()
- def _prep_consumption_to_dict(
- r: BizProductionMaterialUsage,
- *,
- prep_no: str = "",
- configured_material_name: str = "",
- create_by_name: str = "",
- ) -> Dict[str, Any]:
- """配制消耗明细行转字典。"""
- return {
- "id": r.id,
- "materialId": r.material_id,
- "materialType": r.material_type or "",
- "materialCode": r.material_code or "",
- "materialName": r.material_name or "",
- "batchId": r.batch_id or "",
- "stockInNo": r.stock_in_no or "",
- "batchNo": r.batch_no or "",
- "manufacturer": r.manufacturer or "",
- "unitPrice": float(r.unit_price) if r.unit_price is not None else None,
- "changeQty": float(r.change_qty) if r.change_qty is not None else 0,
- "usageUnit": r.usage_unit or "",
- "prepId": r.prep_id or "",
- "prepNo": prep_no,
- "configuredMaterialName": configured_material_name,
- "sourceType": r.source_type or "",
- "remark": r.remark or "",
- "createBy": r.create_by or "",
- "createByName": create_by_name or r.create_by or "",
- "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
- }
- def list_prep_consumptions(
- db: Session,
- tenant_no: str,
- *,
- page: int = 1,
- page_size: int = 10,
- keyword: Optional[str] = None,
- material_type: Optional[str] = None,
- start_time: Optional[str] = None,
- end_time: Optional[str] = None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- """查询配制消耗明细(sub_consume)。"""
- q0 = db.query(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "prep_consume",
- )
- if material_type:
- mt = material_type.strip().lower()
- if mt in ("reagent", "consumable"):
- q0 = q0.filter(BizProductionMaterialUsage.material_type == mt)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizProductionMaterialUsage.material_name.like(like),
- BizProductionMaterialUsage.material_code.like(like),
- BizProductionMaterialUsage.batch_no.like(like),
- )
- )
- if start_time:
- q0 = q0.filter(BizProductionMaterialUsage.create_time >= start_time)
- if end_time:
- q0 = q0.filter(BizProductionMaterialUsage.create_time <= end_time + " 23:59:59")
- total = q0.order_by(None).with_entities(func.count(BizProductionMaterialUsage.id)).scalar() or 0
- rows = (
- q0.order_by(BizProductionMaterialUsage.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- # 收集 prep_id 并查询配制单信息
- prep_ids = list({(r.prep_id or "").strip() for r in rows if (r.prep_id or "").strip()})
- prep_map: Dict[str, Dict[str, Any]] = {}
- if prep_ids:
- preps = (
- db.query(BizProductionMaterialPrep)
- .filter(
- BizProductionMaterialPrep.id.in_(prep_ids),
- BizProductionMaterialPrep.tenant_no == tenant_no,
- BizProductionMaterialPrep.del_flag == 0,
- )
- .all()
- )
- prep_map = {p.id: {"prepNo": p.prep_no, "configuredMaterialName": p.configured_material_name or ""} for p in preps}
- name_map = _user_nick_name_map(db, [r.create_by for r in rows if r.create_by])
- items = [
- _prep_consumption_to_dict(
- r,
- prep_no=prep_map.get((r.prep_id or "").strip(), {}).get("prepNo", ""),
- configured_material_name=prep_map.get((r.prep_id or "").strip(), {}).get("configuredMaterialName", ""),
- create_by_name=name_map.get(r.create_by or "", ""),
- )
- for r in rows
- ]
- return items, int(total)
- def list_my_prep_consumptions(
- db: Session,
- tenant_no: str,
- *,
- user_id: str,
- nick_name: str = "",
- page: int = 1,
- page_size: int = 10,
- keyword: Optional[str] = None,
- material_type: Optional[str] = None,
- start_time: Optional[str] = None,
- end_time: Optional[str] = None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- """当前登录人领用批次的配制消耗明细(按领用人,非配制操作人)。"""
- uid = (user_id or "").strip()
- nn = (nick_name or "").strip()
- parent_conds = [BizProductionMaterialUsage.source_type == "manual_out"]
- receiver_match: List[Any] = []
- if nn:
- receiver_match.append(BizProductionMaterialUsage.receiver == nn)
- receiver_match.append(BizProductionMaterialUsage.remark.like(f"%领用人:{nn}%"))
- if uid:
- receiver_match.append(BizProductionMaterialUsage.receiver == uid)
- if not receiver_match:
- return [], 0
- parent_conds.append(or_(*receiver_match))
- parent_id_q = (
- db.query(BizProductionMaterialUsage.id)
- .filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- and_(*parent_conds),
- )
- )
- q0 = db.query(BizProductionMaterialUsage).filter(
- BizProductionMaterialUsage.tenant_no == tenant_no,
- BizProductionMaterialUsage.del_flag == 0,
- BizProductionMaterialUsage.change_type == "use",
- BizProductionMaterialUsage.source_type == "prep_consume",
- BizProductionMaterialUsage.parent_usage_id.in_(parent_id_q),
- )
- if material_type:
- mt = material_type.strip().lower()
- if mt in ("reagent", "consumable"):
- q0 = q0.filter(BizProductionMaterialUsage.material_type == mt)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizProductionMaterialUsage.material_name.like(like),
- BizProductionMaterialUsage.material_code.like(like),
- BizProductionMaterialUsage.batch_no.like(like),
- )
- )
- if start_time:
- q0 = q0.filter(BizProductionMaterialUsage.create_time >= start_time)
- if end_time:
- q0 = q0.filter(BizProductionMaterialUsage.create_time <= end_time + " 23:59:59")
- total = q0.order_by(None).with_entities(func.count(BizProductionMaterialUsage.id)).scalar() or 0
- rows = (
- q0.order_by(BizProductionMaterialUsage.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- prep_ids = list({(r.prep_id or "").strip() for r in rows if (r.prep_id or "").strip()})
- prep_map: Dict[str, Dict[str, Any]] = {}
- if prep_ids:
- preps = (
- db.query(BizProductionMaterialPrep)
- .filter(
- BizProductionMaterialPrep.id.in_(prep_ids),
- BizProductionMaterialPrep.tenant_no == tenant_no,
- BizProductionMaterialPrep.del_flag == 0,
- )
- .all()
- )
- prep_map = {p.id: {"prepNo": p.prep_no, "configuredMaterialName": p.configured_material_name or ""} for p in preps}
- items = [
- _prep_consumption_to_dict(
- r,
- prep_no=prep_map.get((r.prep_id or "").strip(), {}).get("prepNo", ""),
- configured_material_name=prep_map.get((r.prep_id or "").strip(), {}).get("configuredMaterialName", ""),
- )
- for r in rows
- ]
- return items, int(total)
|