pl_inventory_service.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. """计划归档按产品+品牌写入库存台账与流水。
  2. 直销单 (direct) 首期不记账;入库增加、出库扣减(允许负库存,不做不足拦截)。
  3. 调整归档时先按 plan_id 冲销旧流水再按新量重记。
  4. """
  5. from decimal import Decimal
  6. from typing import Optional
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import generate_uuid
  9. from app.models.pl_models import (
  10. PlInventory,
  11. PlInventoryTransaction,
  12. PlPlan,
  13. PlPlanProduct,
  14. PlPlanProductBrandQty,
  15. )
  16. EMPTY_BRAND_ID = ""
  17. def qty_delta_for_plan(plan_type: str, qty: Decimal) -> Decimal:
  18. if plan_type == "inbound":
  19. return Decimal(str(qty))
  20. if plan_type == "outbound":
  21. return -Decimal(str(qty))
  22. return Decimal("0") # direct: 首期不记账
  23. def get_or_create_inventory(
  24. db: Session,
  25. *,
  26. tenant_no: str,
  27. product_id: str,
  28. product_no: str,
  29. product_name: str,
  30. brand_id: Optional[str],
  31. brand_name: Optional[str],
  32. unit: Optional[str],
  33. model: Optional[str],
  34. user_id: str,
  35. dept_id: str,
  36. ) -> PlInventory:
  37. brand_id = brand_id or EMPTY_BRAND_ID
  38. row = (
  39. db.query(PlInventory)
  40. .filter(
  41. PlInventory.tenant_no == tenant_no,
  42. PlInventory.product_id == product_id,
  43. PlInventory.brand_id == brand_id,
  44. PlInventory.del_flag == 0,
  45. )
  46. .first()
  47. )
  48. if row:
  49. return row
  50. row = PlInventory(
  51. id=generate_uuid(),
  52. tenant_no=tenant_no,
  53. product_id=product_id,
  54. brand_id=brand_id,
  55. brand_name=brand_name,
  56. product_no=product_no or "",
  57. product_name=product_name or "",
  58. model=model,
  59. unit=unit or "吨",
  60. stock_qty=Decimal("0"),
  61. stock_amount=Decimal("0"),
  62. create_by=user_id,
  63. create_dept_id=dept_id or "",
  64. update_by=user_id,
  65. )
  66. db.add(row)
  67. db.flush()
  68. return row
  69. def apply_brand_stock_change(
  70. db: Session,
  71. *,
  72. tenant_no: str,
  73. plan: PlPlan,
  74. plan_product: PlPlanProduct,
  75. brand_id: Optional[str],
  76. brand_name: Optional[str],
  77. qty: Decimal,
  78. user_id: str,
  79. dept_id: str,
  80. ) -> None:
  81. delta = qty_delta_for_plan(plan.plan_type, Decimal(str(qty)))
  82. if delta == 0:
  83. return
  84. inv = get_or_create_inventory(
  85. db,
  86. tenant_no=tenant_no,
  87. product_id=plan_product.product_id,
  88. product_no=plan_product.product_no,
  89. product_name=plan_product.product_name,
  90. brand_id=brand_id or EMPTY_BRAND_ID,
  91. brand_name=brand_name,
  92. unit=plan_product.unit,
  93. model=plan_product.model,
  94. user_id=user_id,
  95. dept_id=dept_id,
  96. )
  97. unit_price = (
  98. plan_product.inbound_price
  99. or plan_product.outbound_price
  100. or plan_product.tax_price
  101. or Decimal("0")
  102. )
  103. unit_price = Decimal(str(unit_price))
  104. inv.stock_qty = Decimal(str(inv.stock_qty or 0)) + delta
  105. inv.stock_amount = Decimal(str(inv.stock_qty or 0)) * unit_price
  106. inv.update_by = user_id
  107. db.add(
  108. PlInventoryTransaction(
  109. id=generate_uuid(),
  110. tenant_no=tenant_no,
  111. product_id=plan_product.product_id,
  112. brand_id=brand_id or EMPTY_BRAND_ID,
  113. brand_name=brand_name,
  114. product_name=plan_product.product_name or "",
  115. transaction_type="inbound" if delta > 0 else "outbound",
  116. plan_id=plan.id,
  117. plan_no=plan.plan_no,
  118. qty=delta,
  119. unit_price=unit_price,
  120. amount=abs(delta) * unit_price,
  121. stock_after=inv.stock_qty,
  122. create_by=user_id,
  123. create_dept_id=dept_id or "",
  124. update_by=user_id,
  125. )
  126. )
  127. def reverse_plan_inventory_postings(
  128. db: Session,
  129. *,
  130. tenant_no: str,
  131. plan_id: str,
  132. user_id: str,
  133. ) -> None:
  134. """冲销该计划已产生的库存流水(调整归档前调用)。"""
  135. txns = (
  136. db.query(PlInventoryTransaction)
  137. .filter(
  138. PlInventoryTransaction.tenant_no == tenant_no,
  139. PlInventoryTransaction.plan_id == plan_id,
  140. PlInventoryTransaction.del_flag == 0,
  141. )
  142. .order_by(PlInventoryTransaction.create_time.asc())
  143. .all()
  144. )
  145. for txn in txns:
  146. brand_id = txn.brand_id or EMPTY_BRAND_ID
  147. inv = (
  148. db.query(PlInventory)
  149. .filter(
  150. PlInventory.tenant_no == tenant_no,
  151. PlInventory.product_id == txn.product_id,
  152. PlInventory.brand_id == brand_id,
  153. PlInventory.del_flag == 0,
  154. )
  155. .first()
  156. )
  157. if inv:
  158. inv.stock_qty = Decimal(str(inv.stock_qty or 0)) - Decimal(str(txn.qty or 0))
  159. unit_price = Decimal(str(txn.unit_price or 0))
  160. inv.stock_amount = Decimal(str(inv.stock_qty or 0)) * unit_price
  161. inv.update_by = user_id
  162. txn.del_flag = 1
  163. txn.update_by = user_id
  164. def apply_archive_inventory_for_plan(
  165. db: Session,
  166. *,
  167. tenant_no: str,
  168. plan: PlPlan,
  169. user_id: str,
  170. dept_id: str = "",
  171. ) -> None:
  172. """归档后按品牌量(或无品牌 actual_qty)落账;先冲销旧流水再重记。"""
  173. reverse_plan_inventory_postings(
  174. db, tenant_no=tenant_no, plan_id=plan.id, user_id=user_id
  175. )
  176. if qty_delta_for_plan(plan.plan_type, Decimal("1")) == 0:
  177. return
  178. products = (
  179. db.query(PlPlanProduct)
  180. .filter(
  181. PlPlanProduct.plan_id == plan.id,
  182. PlPlanProduct.tenant_no == tenant_no,
  183. PlPlanProduct.del_flag == 0,
  184. )
  185. .order_by(PlPlanProduct.order_num.asc())
  186. .all()
  187. )
  188. for prod in products:
  189. brand_rows = (
  190. db.query(PlPlanProductBrandQty)
  191. .filter(
  192. PlPlanProductBrandQty.plan_product_id == prod.id,
  193. PlPlanProductBrandQty.tenant_no == tenant_no,
  194. PlPlanProductBrandQty.del_flag == 0,
  195. )
  196. .order_by(PlPlanProductBrandQty.order_num.asc())
  197. .all()
  198. )
  199. if prod.brand_mode and brand_rows:
  200. for bq in brand_rows:
  201. apply_brand_stock_change(
  202. db,
  203. tenant_no=tenant_no,
  204. plan=plan,
  205. plan_product=prod,
  206. brand_id=bq.brand_id or EMPTY_BRAND_ID,
  207. brand_name=bq.brand_name,
  208. qty=Decimal(str(bq.qty or 0)),
  209. user_id=user_id,
  210. dept_id=dept_id,
  211. )
  212. continue
  213. actual = Decimal(str(prod.actual_qty or 0))
  214. if actual <= 0:
  215. continue
  216. apply_brand_stock_change(
  217. db,
  218. tenant_no=tenant_no,
  219. plan=plan,
  220. plan_product=prod,
  221. brand_id=EMPTY_BRAND_ID,
  222. brand_name=None,
  223. qty=actual,
  224. user_id=user_id,
  225. dept_id=dept_id,
  226. )