| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- """计划归档按产品+品牌写入库存台账与流水。
- 直销单 (direct) 首期不记账;入库增加、出库扣减(允许负库存,不做不足拦截)。
- 调整归档时先按 plan_id 冲销旧流水再按新量重记。
- """
- from decimal import Decimal
- from typing import Optional
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.models.pl_models import (
- PlInventory,
- PlInventoryTransaction,
- PlPlan,
- PlPlanProduct,
- PlPlanProductBrandQty,
- )
- EMPTY_BRAND_ID = ""
- def qty_delta_for_plan(plan_type: str, qty: Decimal) -> Decimal:
- if plan_type == "inbound":
- return Decimal(str(qty))
- if plan_type == "outbound":
- return -Decimal(str(qty))
- return Decimal("0") # direct: 首期不记账
- def get_or_create_inventory(
- db: Session,
- *,
- tenant_no: str,
- product_id: str,
- product_no: str,
- product_name: str,
- brand_id: Optional[str],
- brand_name: Optional[str],
- unit: Optional[str],
- model: Optional[str],
- user_id: str,
- dept_id: str,
- ) -> PlInventory:
- brand_id = brand_id or EMPTY_BRAND_ID
- row = (
- db.query(PlInventory)
- .filter(
- PlInventory.tenant_no == tenant_no,
- PlInventory.product_id == product_id,
- PlInventory.brand_id == brand_id,
- PlInventory.del_flag == 0,
- )
- .first()
- )
- if row:
- return row
- row = PlInventory(
- id=generate_uuid(),
- tenant_no=tenant_no,
- product_id=product_id,
- brand_id=brand_id,
- brand_name=brand_name,
- product_no=product_no or "",
- product_name=product_name or "",
- model=model,
- unit=unit or "吨",
- stock_qty=Decimal("0"),
- stock_amount=Decimal("0"),
- create_by=user_id,
- create_dept_id=dept_id or "",
- update_by=user_id,
- )
- db.add(row)
- db.flush()
- return row
- def apply_brand_stock_change(
- db: Session,
- *,
- tenant_no: str,
- plan: PlPlan,
- plan_product: PlPlanProduct,
- brand_id: Optional[str],
- brand_name: Optional[str],
- qty: Decimal,
- user_id: str,
- dept_id: str,
- qty_delta_override: Optional[Decimal] = None,
- unit_price_override: Optional[Decimal] = None,
- ) -> None:
- delta = (
- qty_delta_override
- if qty_delta_override is not None
- else qty_delta_for_plan(plan.plan_type, Decimal(str(qty)))
- )
- if delta == 0:
- return
- inv = get_or_create_inventory(
- db,
- tenant_no=tenant_no,
- product_id=plan_product.product_id,
- product_no=plan_product.product_no,
- product_name=plan_product.product_name,
- brand_id=brand_id or EMPTY_BRAND_ID,
- brand_name=brand_name,
- unit=plan_product.unit,
- model=plan_product.model,
- user_id=user_id,
- dept_id=dept_id,
- )
- unit_price = unit_price_override
- if unit_price is None:
- unit_price = (
- plan_product.inbound_price
- or plan_product.outbound_price
- or plan_product.tax_price
- or Decimal("0")
- )
- unit_price = Decimal(str(unit_price))
- inv.stock_qty = Decimal(str(inv.stock_qty or 0)) + delta
- inv.stock_amount = Decimal(str(inv.stock_qty or 0)) * unit_price
- inv.update_by = user_id
- db.add(
- PlInventoryTransaction(
- id=generate_uuid(),
- tenant_no=tenant_no,
- product_id=plan_product.product_id,
- brand_id=brand_id or EMPTY_BRAND_ID,
- brand_name=brand_name,
- product_name=plan_product.product_name or "",
- transaction_type="inbound" if delta > 0 else "outbound",
- plan_id=plan.id,
- plan_no=plan.plan_no,
- qty=delta,
- unit_price=unit_price,
- amount=abs(delta) * unit_price,
- stock_after=inv.stock_qty,
- create_by=user_id,
- create_dept_id=dept_id or "",
- update_by=user_id,
- )
- )
- def reverse_plan_inventory_postings(
- db: Session,
- *,
- tenant_no: str,
- plan_id: str,
- user_id: str,
- ) -> None:
- """冲销该计划已产生的库存流水(调整归档前调用)。"""
- txns = (
- db.query(PlInventoryTransaction)
- .filter(
- PlInventoryTransaction.tenant_no == tenant_no,
- PlInventoryTransaction.plan_id == plan_id,
- PlInventoryTransaction.del_flag == 0,
- )
- .order_by(PlInventoryTransaction.create_time.asc())
- .all()
- )
- for txn in txns:
- brand_id = txn.brand_id or EMPTY_BRAND_ID
- inv = (
- db.query(PlInventory)
- .filter(
- PlInventory.tenant_no == tenant_no,
- PlInventory.product_id == txn.product_id,
- PlInventory.brand_id == brand_id,
- PlInventory.del_flag == 0,
- )
- .first()
- )
- if inv:
- inv.stock_qty = Decimal(str(inv.stock_qty or 0)) - Decimal(str(txn.qty or 0))
- unit_price = Decimal(str(txn.unit_price or 0))
- inv.stock_amount = Decimal(str(inv.stock_qty or 0)) * unit_price
- inv.update_by = user_id
- txn.del_flag = 1
- txn.update_by = user_id
- def apply_archive_inventory_for_plan(
- db: Session,
- *,
- tenant_no: str,
- plan: PlPlan,
- user_id: str,
- dept_id: str = "",
- ) -> None:
- """归档后按品牌量(或无品牌 actual_qty)落账;先冲销旧流水再重记。"""
- reverse_plan_inventory_postings(
- db, tenant_no=tenant_no, plan_id=plan.id, user_id=user_id
- )
- if qty_delta_for_plan(plan.plan_type, Decimal("1")) == 0:
- return
- products = (
- db.query(PlPlanProduct)
- .filter(
- PlPlanProduct.plan_id == plan.id,
- PlPlanProduct.tenant_no == tenant_no,
- PlPlanProduct.del_flag == 0,
- )
- .order_by(PlPlanProduct.order_num.asc())
- .all()
- )
- for prod in products:
- brand_rows = (
- db.query(PlPlanProductBrandQty)
- .filter(
- PlPlanProductBrandQty.plan_product_id == prod.id,
- PlPlanProductBrandQty.tenant_no == tenant_no,
- PlPlanProductBrandQty.del_flag == 0,
- )
- .order_by(PlPlanProductBrandQty.order_num.asc())
- .all()
- )
- if prod.brand_mode and brand_rows:
- for bq in brand_rows:
- apply_brand_stock_change(
- db,
- tenant_no=tenant_no,
- plan=plan,
- plan_product=prod,
- brand_id=bq.brand_id or EMPTY_BRAND_ID,
- brand_name=bq.brand_name,
- qty=Decimal(str(bq.qty or 0)),
- user_id=user_id,
- dept_id=dept_id,
- )
- continue
- actual = Decimal(str(prod.actual_qty or 0))
- if actual <= 0:
- continue
- apply_brand_stock_change(
- db,
- tenant_no=tenant_no,
- plan=plan,
- plan_product=prod,
- brand_id=EMPTY_BRAND_ID,
- brand_name=None,
- qty=actual,
- user_id=user_id,
- dept_id=dept_id,
- )
- def apply_archive_inventory_for_plan_stops(
- db: Session,
- *,
- tenant_no: str,
- plan: PlPlan,
- user_id: str,
- dept_id: str = "",
- ) -> None:
- """一车多卸:内部装货出库、内部送货入库。"""
- from app.services.pl_plan_archive_stop_service import iter_stop_archive_inventory_lines
- reverse_plan_inventory_postings(
- db, tenant_no=tenant_no, plan_id=plan.id, user_id=user_id
- )
- product_map = {
- row.id: row
- for row in db.query(PlPlanProduct).filter(
- PlPlanProduct.plan_id == plan.id,
- PlPlanProduct.tenant_no == tenant_no,
- PlPlanProduct.del_flag == 0,
- )
- }
- for plan_product_id, qty, direction, price in iter_stop_archive_inventory_lines(
- db, tenant_no=tenant_no, plan_id=plan.id
- ):
- prod = product_map.get(plan_product_id)
- if not prod:
- continue
- delta = -qty if direction == "outbound" else qty
- apply_brand_stock_change(
- db,
- tenant_no=tenant_no,
- plan=plan,
- plan_product=prod,
- brand_id=EMPTY_BRAND_ID,
- brand_name=None,
- qty=qty,
- user_id=user_id,
- dept_id=dept_id,
- qty_delta_override=delta,
- unit_price_override=Decimal(str(price)) if price is not None else None,
- )
|