qc_material_month_template.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. """质检物料月初入库模板 CRUD / preview / confirm。"""
  2. from __future__ import annotations
  3. from datetime import date, datetime
  4. from decimal import Decimal
  5. from typing import Any, Dict, List, Optional
  6. from sqlalchemy.orm import Session
  7. from app.core.utils import generate_uuid
  8. from app.crud import qc_material as mat_svc
  9. from app.crud import qc_material_stock_batch as batch_svc
  10. from app.crud.qc_material import _is_enabled_status
  11. from app.crud.qc_material_stock_batch import _fmt_date, _parse_date, _require_str
  12. from app.crud.qc_material_stock_in_no import allocate_stock_in_nos
  13. from app.models.qc_material import BizQcMaterialMonthTemplate
  14. def _optional_str(v: Any) -> str:
  15. return str(v or "").strip()
  16. def _optional_decimal(v: Any, *, default: Decimal = Decimal("0")) -> Decimal:
  17. if v is None or str(v).strip() == "":
  18. return default
  19. try:
  20. d = Decimal(str(v))
  21. except Exception as e:
  22. raise ValueError("数值格式无效") from e
  23. if d < 0:
  24. raise ValueError("数值不能为负数")
  25. return d
  26. def _optional_expire_date(v: Any) -> Optional[date]:
  27. s = str(v or "").strip()
  28. if not s:
  29. return None
  30. return _parse_date(v, label="过期日期")
  31. def _require_template_fields(payload: dict) -> dict:
  32. """规范化模板写入字段;模板行字段均可空,确认入库时再校验必填。"""
  33. if not isinstance(payload, dict):
  34. raise ValueError("模板数据无效")
  35. material_id = (payload.get("material_id") or payload.get("materialId") or "").strip() or None
  36. material_code = _optional_str(payload.get("material_code") or payload.get("materialCode"))
  37. material_name = _optional_str(payload.get("material_name") or payload.get("materialName"))
  38. catalog_no = _optional_str(payload.get("catalog_no") or payload.get("catalogNo"))
  39. batch_no = _optional_str(payload.get("batch_no") or payload.get("batchNo"))
  40. manufacturer = _optional_str(payload.get("manufacturer"))
  41. unit_price = _optional_decimal(payload.get("unit_price") or payload.get("unitPrice"))
  42. amount = _optional_decimal(payload.get("amount"))
  43. in_qty_raw = payload.get("in_qty") if payload.get("in_qty") is not None else payload.get("inQty")
  44. in_qty = _optional_decimal(in_qty_raw)
  45. expire_date = _optional_expire_date(payload.get("expire_date") or payload.get("expireDate"))
  46. spec_model = _optional_str(payload.get("spec_model") or payload.get("specModel")) or None
  47. unit = _optional_str(payload.get("unit")) or None
  48. remark = _optional_str(payload.get("remark")) or None
  49. order_num = payload.get("order_num")
  50. if order_num is None:
  51. order_num = payload.get("orderNum")
  52. if order_num is None:
  53. order_num = 0
  54. try:
  55. order_num = int(order_num)
  56. except (TypeError, ValueError) as e:
  57. raise ValueError("排序号格式无效") from e
  58. return {
  59. "material_id": material_id,
  60. "material_code": material_code,
  61. "material_name": material_name,
  62. "spec_model": spec_model,
  63. "unit": unit,
  64. "catalog_no": catalog_no,
  65. "batch_no": batch_no,
  66. "manufacturer": manufacturer,
  67. "unit_price": unit_price,
  68. "amount": amount,
  69. "in_qty": in_qty,
  70. "expire_date": expire_date,
  71. "order_num": order_num,
  72. "remark": remark,
  73. }
  74. def _row_to_dict(row: BizQcMaterialMonthTemplate) -> Dict[str, Any]:
  75. return {
  76. "id": row.id,
  77. "materialId": row.material_id or "",
  78. "materialCode": row.material_code or "",
  79. "materialName": row.material_name or "",
  80. "specModel": row.spec_model or "",
  81. "unit": row.unit or "",
  82. "catalogNo": row.catalog_no or "",
  83. "batchNo": row.batch_no or "",
  84. "manufacturer": row.manufacturer or "",
  85. "unitPrice": float(row.unit_price) if row.unit_price is not None else 0,
  86. "amount": float(row.amount) if row.amount is not None else 0,
  87. "inQty": float(row.in_qty) if row.in_qty is not None else 0,
  88. "expireDate": _fmt_date(row.expire_date),
  89. "orderNum": row.order_num if row.order_num is not None else 0,
  90. "remark": row.remark or "",
  91. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  92. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  93. }
  94. def list_templates(db: Session, *, tenant_no: str) -> List[Dict[str, Any]]:
  95. rows = (
  96. db.query(BizQcMaterialMonthTemplate)
  97. .filter(
  98. BizQcMaterialMonthTemplate.tenant_no == tenant_no,
  99. BizQcMaterialMonthTemplate.del_flag == 0,
  100. )
  101. .order_by(
  102. BizQcMaterialMonthTemplate.order_num.asc(),
  103. BizQcMaterialMonthTemplate.create_time.asc(),
  104. )
  105. .all()
  106. )
  107. return [_row_to_dict(r) for r in rows]
  108. def create_template(
  109. db: Session,
  110. *,
  111. data: dict,
  112. tenant_no: str,
  113. user_id: Optional[str],
  114. create_dept_id: str = "",
  115. ) -> Dict[str, Any]:
  116. fields = _require_template_fields(data)
  117. if fields["material_id"]:
  118. mat = mat_svc.get_material(db, fields["material_id"], tenant_no=tenant_no)
  119. if not mat:
  120. raise ValueError("关联物资不存在")
  121. now = datetime.now()
  122. row = BizQcMaterialMonthTemplate(
  123. id=generate_uuid(),
  124. material_id=fields["material_id"],
  125. material_code=fields["material_code"],
  126. material_name=fields["material_name"],
  127. spec_model=fields["spec_model"],
  128. unit=fields["unit"],
  129. catalog_no=fields["catalog_no"],
  130. batch_no=fields["batch_no"],
  131. manufacturer=fields["manufacturer"],
  132. unit_price=fields["unit_price"],
  133. amount=fields["amount"],
  134. in_qty=fields["in_qty"],
  135. expire_date=fields["expire_date"],
  136. tenant_no=tenant_no,
  137. order_num=fields["order_num"],
  138. remark=fields["remark"],
  139. create_by=user_id,
  140. create_dept_id=create_dept_id or "",
  141. create_time=now,
  142. update_by=user_id,
  143. update_time=now,
  144. del_flag=0,
  145. )
  146. db.add(row)
  147. db.commit()
  148. db.refresh(row)
  149. return _row_to_dict(row)
  150. def update_template(
  151. db: Session,
  152. row_id: str,
  153. *,
  154. data: dict,
  155. tenant_no: str,
  156. user_id: Optional[str],
  157. ) -> Dict[str, Any]:
  158. row = (
  159. db.query(BizQcMaterialMonthTemplate)
  160. .filter(
  161. BizQcMaterialMonthTemplate.id == row_id,
  162. BizQcMaterialMonthTemplate.tenant_no == tenant_no,
  163. BizQcMaterialMonthTemplate.del_flag == 0,
  164. )
  165. .first()
  166. )
  167. if not row:
  168. raise ValueError("模板行不存在")
  169. fields = _require_template_fields(data)
  170. if fields["material_id"]:
  171. mat = mat_svc.get_material(db, fields["material_id"], tenant_no=tenant_no)
  172. if not mat:
  173. raise ValueError("关联物资不存在")
  174. row.material_id = fields["material_id"]
  175. row.material_code = fields["material_code"]
  176. row.material_name = fields["material_name"]
  177. row.spec_model = fields["spec_model"]
  178. row.unit = fields["unit"]
  179. row.catalog_no = fields["catalog_no"]
  180. row.batch_no = fields["batch_no"]
  181. row.manufacturer = fields["manufacturer"]
  182. row.unit_price = fields["unit_price"]
  183. row.amount = fields["amount"]
  184. row.in_qty = fields["in_qty"]
  185. row.expire_date = fields["expire_date"]
  186. row.order_num = fields["order_num"]
  187. row.remark = fields["remark"]
  188. row.update_by = user_id
  189. row.update_time = datetime.now()
  190. db.commit()
  191. db.refresh(row)
  192. return _row_to_dict(row)
  193. def soft_delete_template(db: Session, row_id: str, *, tenant_no: str) -> None:
  194. row = (
  195. db.query(BizQcMaterialMonthTemplate)
  196. .filter(
  197. BizQcMaterialMonthTemplate.id == row_id,
  198. BizQcMaterialMonthTemplate.tenant_no == tenant_no,
  199. BizQcMaterialMonthTemplate.del_flag == 0,
  200. )
  201. .first()
  202. )
  203. if not row:
  204. raise ValueError("模板行不存在")
  205. row.del_flag = 1
  206. row.update_time = datetime.now()
  207. db.commit()
  208. def preview_templates(
  209. db: Session, *, tenant_no: str, stock_in_date: date
  210. ) -> List[Dict[str, Any]]:
  211. rows = (
  212. db.query(BizQcMaterialMonthTemplate)
  213. .filter(
  214. BizQcMaterialMonthTemplate.tenant_no == tenant_no,
  215. BizQcMaterialMonthTemplate.del_flag == 0,
  216. )
  217. .order_by(
  218. BizQcMaterialMonthTemplate.order_num.asc(),
  219. BizQcMaterialMonthTemplate.create_time.asc(),
  220. )
  221. .all()
  222. )
  223. if not rows:
  224. raise ValueError("请先维护月初模板")
  225. nos = allocate_stock_in_nos(
  226. db, tenant_no=tenant_no, stock_in_date=stock_in_date, count=len(rows)
  227. )
  228. out: List[Dict[str, Any]] = []
  229. for i, row in enumerate(rows):
  230. out.append(
  231. {
  232. "templateId": row.id,
  233. "materialId": row.material_id or "",
  234. "materialCode": row.material_code or "",
  235. "materialName": row.material_name or "",
  236. "specModel": row.spec_model or "",
  237. "unit": row.unit or "",
  238. "stockInNo": nos[i],
  239. "stockInDate": _fmt_date(stock_in_date),
  240. "expireDate": _fmt_date(row.expire_date),
  241. "catalogNo": row.catalog_no or "",
  242. "batchNo": row.batch_no or "",
  243. "manufacturer": row.manufacturer or "",
  244. "unitPrice": float(row.unit_price) if row.unit_price is not None else 0,
  245. "amount": float(row.amount) if row.amount is not None else 0,
  246. "qty": float(row.in_qty) if row.in_qty is not None else 0,
  247. "remark": row.remark or "",
  248. }
  249. )
  250. return out
  251. def ensure_material_id(
  252. db: Session,
  253. *,
  254. tenant_no: str,
  255. user_id: Optional[str],
  256. material_id: Optional[str],
  257. material_code: str,
  258. material_name: str,
  259. spec_model: Optional[str],
  260. unit: Optional[str],
  261. ) -> str:
  262. mid = (material_id or "").strip()
  263. if mid:
  264. mat = mat_svc.get_material(db, mid, tenant_no=tenant_no)
  265. if not mat:
  266. raise ValueError("关联物资不存在")
  267. if not _is_enabled_status(mat.status):
  268. raise ValueError(f"物资「{mat.material_name or mid}」已停用")
  269. return mat.id
  270. code = _require_str(material_code, "物料编码")
  271. name = _require_str(material_name, "物料名称")
  272. existing = mat_svc.get_material_by_code(db, code, tenant_no=tenant_no)
  273. if existing:
  274. if not _is_enabled_status(existing.status):
  275. raise ValueError(f"物资「{existing.material_name or code}」已停用")
  276. return existing.id
  277. created = mat_svc.create_material(
  278. db,
  279. material_code=code,
  280. material_name=name,
  281. spec_model=spec_model,
  282. unit=unit,
  283. stock_qty=Decimal("0"),
  284. status="1",
  285. tenant_no=tenant_no,
  286. user_id=user_id,
  287. commit=False,
  288. )
  289. return created.id
  290. def confirm_month_import(
  291. db: Session,
  292. *,
  293. tenant_no: str,
  294. user_id: Optional[str],
  295. stock_in_date: date,
  296. items: list,
  297. remark: Optional[str] = None,
  298. operate_time: Optional[datetime] = None,
  299. ) -> int:
  300. if not items:
  301. raise ValueError("请至少添加一条入库明细")
  302. stock_in_items: List[Dict[str, Any]] = []
  303. for it in items:
  304. if not isinstance(it, dict):
  305. raise ValueError("入库明细格式无效")
  306. mid = ensure_material_id(
  307. db,
  308. tenant_no=tenant_no,
  309. user_id=user_id,
  310. material_id=it.get("material_id") or it.get("materialId"),
  311. material_code=it.get("material_code") or it.get("materialCode") or "",
  312. material_name=it.get("material_name") or it.get("materialName") or "",
  313. spec_model=it.get("spec_model") or it.get("specModel"),
  314. unit=it.get("unit"),
  315. )
  316. stock_in_items.append(
  317. {
  318. "material_id": mid,
  319. "materialId": mid,
  320. "stock_in_no": it.get("stock_in_no") or it.get("stockInNo"),
  321. "stockInNo": it.get("stock_in_no") or it.get("stockInNo"),
  322. "stock_in_date": _fmt_date(stock_in_date),
  323. "stockInDate": _fmt_date(stock_in_date),
  324. "expire_date": it.get("expire_date") or it.get("expireDate"),
  325. "expireDate": it.get("expire_date") or it.get("expireDate"),
  326. "catalog_no": it.get("catalog_no") or it.get("catalogNo"),
  327. "catalogNo": it.get("catalog_no") or it.get("catalogNo"),
  328. "batch_no": it.get("batch_no") or it.get("batchNo"),
  329. "batchNo": it.get("batch_no") or it.get("batchNo"),
  330. "manufacturer": it.get("manufacturer"),
  331. "unit_price": it.get("unit_price") if it.get("unit_price") is not None else it.get("unitPrice"),
  332. "unitPrice": it.get("unit_price") if it.get("unit_price") is not None else it.get("unitPrice"),
  333. "amount": it.get("amount"),
  334. "qty": it.get("qty"),
  335. "remark": it.get("remark"),
  336. }
  337. )
  338. return batch_svc.batch_stock_in(
  339. db,
  340. items=stock_in_items,
  341. remark=remark,
  342. tenant_no=tenant_no,
  343. user_id=user_id,
  344. operate_time=operate_time,
  345. )