feat(pl): upsert dibang links by stop instead of revoke-and-rebuild.
Keep inventory ids across rearrange/plan edits; block cancel when weighing exists. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
c90ff7c419
commit
68564c61f0
4 files changed
+739
-71
No files matched your search
@@ -30,6 +30,8 @@ from app.models.pl_models import (
|
||||
PlCarrier,
|
||||
PlDriver,
|
||||
PlPlan,
|
||||
PlPlanDelivery,
|
||||
PlPlanLoad,
|
||||
PlPlanProduct,
|
||||
PlPlanTransport,
|
||||
PlTransport,
|
||||
@@ -84,8 +86,10 @@ from app.schemas.pl.transport import (
|
||||
TransportSiteLinkRead,
|
||||
)
|
||||
from app.services.pl_dibang_bridge_service import (
|
||||
StopDibangOption,
|
||||
revoke_regeneratable_dibang_for_transport,
|
||||
sync_linked_dibang_dates_for_plan,
|
||||
upsert_stop_dibang_links,
|
||||
)
|
||||
from app.services.pl_inventory_link_read import build_transport_inventory_link_read
|
||||
from app.services.pl_io_order_service import get_active_io_order_for_plan, void_io_orders_for_plan
|
||||
@@ -1792,13 +1796,80 @@ def update_plan(
|
||||
user_id=user.id,
|
||||
dept_id=user.dept_id or "",
|
||||
)
|
||||
dibang_upsert_warnings: list[str] = []
|
||||
if (
|
||||
plan.transport_status == "arranged"
|
||||
and plan.transport_id
|
||||
and multi_stop
|
||||
and load_body is not None
|
||||
):
|
||||
transport_row = (
|
||||
db.query(PlTransport)
|
||||
.filter(
|
||||
PlTransport.id == plan.transport_id,
|
||||
PlTransport.tenant_no == tenant_no,
|
||||
PlTransport.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if transport_row:
|
||||
stop_options: list[StopDibangOption] = []
|
||||
load_row = (
|
||||
db.query(PlPlanLoad)
|
||||
.filter(
|
||||
PlPlanLoad.plan_id == id,
|
||||
PlPlanLoad.tenant_no == tenant_no,
|
||||
PlPlanLoad.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if load_row:
|
||||
stop_options.append(
|
||||
StopDibangOption(
|
||||
stop_type="load",
|
||||
stop_id=load_row.id,
|
||||
generate_dibang=bool(load_row.generate_dibang),
|
||||
)
|
||||
)
|
||||
delivery_rows = (
|
||||
db.query(PlPlanDelivery)
|
||||
.filter(
|
||||
PlPlanDelivery.plan_id == id,
|
||||
PlPlanDelivery.tenant_no == tenant_no,
|
||||
PlPlanDelivery.del_flag == 0,
|
||||
)
|
||||
.order_by(PlPlanDelivery.seq.asc(), PlPlanDelivery.create_time.asc())
|
||||
.all()
|
||||
)
|
||||
for delivery_row in delivery_rows:
|
||||
stop_options.append(
|
||||
StopDibangOption(
|
||||
stop_type="delivery",
|
||||
stop_id=delivery_row.id,
|
||||
generate_dibang=bool(delivery_row.generate_dibang),
|
||||
)
|
||||
)
|
||||
if stop_options:
|
||||
try:
|
||||
_link_results, dibang_upsert_warnings = upsert_stop_dibang_links(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
plan=plan,
|
||||
transport=transport_row,
|
||||
stop_options=stop_options,
|
||||
user_id=user.id,
|
||||
dept_id=user.dept_id or "",
|
||||
)
|
||||
except ValueError as exc:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
db.commit()
|
||||
db.refresh(plan)
|
||||
result = PlanWithProducts.model_validate(plan)
|
||||
result.products = plan_products
|
||||
result.supplier_contracts = supplier_contracts
|
||||
result.demander_contracts = demander_contracts
|
||||
result.warnings = [*qty_warnings, *dibang_date_warnings]
|
||||
result.warnings = [*qty_warnings, *dibang_date_warnings, *dibang_upsert_warnings]
|
||||
stops = load_plan_stops_grouped(db, tenant_no, [id]).get(id, (None, []))
|
||||
attach_plan_stops_to_item(result, plan, stops)
|
||||
return result
|
||||
@@ -1844,6 +1915,8 @@ def patch_plan_hazard_cert_ready(
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> PlanWithProducts:
|
||||
"""标记或取消危化品证书齐备(人工确认)"""
|
||||
if not user_can_manage_plan_cert_ready(db, user):
|
||||
raise HTTPException(status_code=403, detail="无证书齐备操作权限")
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
_get_plan(db, tenant_no, id, user)
|
||||
set_hazard_cert_ready(
|
||||
|
||||
@@ -56,7 +56,6 @@ from app.schemas.pl.transport import (
|
||||
from app.services.pl_dibang_bridge_service import (
|
||||
StopDibangOption,
|
||||
plan_has_load_stop,
|
||||
revoke_regeneratable_dibang_for_transport,
|
||||
sync_dibang_for_plan_stops,
|
||||
sync_dibang_for_transport,
|
||||
)
|
||||
@@ -391,19 +390,6 @@ def _is_rearrange(plan: PlPlan, existing_transport: Optional[PlTransport]) -> bo
|
||||
return plan.transport_status == "arranged"
|
||||
|
||||
|
||||
def _transport_has_db_links(db: Session, tenant_no: str, transport_id: str) -> bool:
|
||||
return (
|
||||
db.query(PlTransportDbLink.id)
|
||||
.filter(
|
||||
PlTransportDbLink.tenant_no == tenant_no,
|
||||
PlTransportDbLink.transport_id == transport_id,
|
||||
PlTransportDbLink.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def _apply_plan_logistics_from_payload(plan: PlPlan, payload: TransportArrange) -> None:
|
||||
if payload.pickup_address is not None:
|
||||
plan.pickup_address = payload.pickup_address
|
||||
@@ -817,20 +803,7 @@ def arrange_transport(
|
||||
_apply_plan_logistics_from_payload(plan, payload)
|
||||
start_point, end_point = _resolve_transport_endpoints(plan, payload)
|
||||
|
||||
if (
|
||||
is_rearrange
|
||||
and existing_transport
|
||||
and _transport_has_db_links(db, tenant_no, existing_transport.id)
|
||||
):
|
||||
try:
|
||||
revoke_regeneratable_dibang_for_transport(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
transport_id=existing_transport.id,
|
||||
user_id=user.id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
# 地磅关联改为按段 upsert,不再在调整前整批 revoke 再建
|
||||
|
||||
if existing_transport and existing_transport.transport_status != "cancelled":
|
||||
transport = existing_transport
|
||||
|
||||
@@ -515,6 +515,260 @@ def _inventory_date_editable(db: Session, inventory: DbInboundOutbound) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _inventory_mutable_editable(db: Session, inventory: DbInboundOutbound) -> bool:
|
||||
"""未过磅且状态可作废时,可就地更新日期/往来/车牌/数量。"""
|
||||
return _inventory_date_editable(db, inventory)
|
||||
|
||||
|
||||
def _find_active_stop_link(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
transport_id: str,
|
||||
plan_id: str,
|
||||
stop_type: str,
|
||||
stop_id: str,
|
||||
plan_product_id: str,
|
||||
) -> Optional[PlTransportDbLink]:
|
||||
return (
|
||||
db.query(PlTransportDbLink)
|
||||
.filter(
|
||||
PlTransportDbLink.tenant_no == tenant_no,
|
||||
PlTransportDbLink.transport_id == transport_id,
|
||||
PlTransportDbLink.plan_id == plan_id,
|
||||
PlTransportDbLink.stop_type == stop_type,
|
||||
PlTransportDbLink.stop_id == stop_id,
|
||||
PlTransportDbLink.plan_product_id == plan_product_id,
|
||||
PlTransportDbLink.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def _soft_delete_link_and_inventory(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
link: PlTransportDbLink,
|
||||
user_id: Optional[str],
|
||||
) -> None:
|
||||
inventory = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.id == link.db_inventory_id,
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if inventory:
|
||||
_subtract_plan_contribution(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
link=link,
|
||||
inventory=inventory,
|
||||
user_id=user_id,
|
||||
)
|
||||
inventory.del_flag = 1
|
||||
inventory.update_by = user_id
|
||||
link.del_flag = 1
|
||||
link.update_by = user_id
|
||||
|
||||
|
||||
def _cancel_stop_dibang_links(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
transport_id: str,
|
||||
plan_id: str,
|
||||
stop_type: str,
|
||||
stop_id: str,
|
||||
user_id: Optional[str],
|
||||
) -> None:
|
||||
"""取消段 generate_dibang:已过磅阻断;否则软删 link+出入库并回减月计划。"""
|
||||
links = (
|
||||
db.query(PlTransportDbLink)
|
||||
.filter(
|
||||
PlTransportDbLink.tenant_no == tenant_no,
|
||||
PlTransportDbLink.transport_id == transport_id,
|
||||
PlTransportDbLink.plan_id == plan_id,
|
||||
PlTransportDbLink.stop_type == stop_type,
|
||||
PlTransportDbLink.stop_id == stop_id,
|
||||
PlTransportDbLink.del_flag == 0,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if not links:
|
||||
return
|
||||
|
||||
blocking: list[str] = []
|
||||
for link in links:
|
||||
inventory = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.id == link.db_inventory_id,
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not inventory:
|
||||
continue
|
||||
label = inventory.batch_no or inventory.id
|
||||
status = (inventory.status or "").strip()
|
||||
if status and status not in _REVOKABLE_INVENTORY_STATUSES:
|
||||
blocking.append(f"{label}(状态:{status})")
|
||||
continue
|
||||
if _inventory_has_weighing_slip(db, inventory.id):
|
||||
blocking.append(label)
|
||||
|
||||
if blocking:
|
||||
raise ValueError(
|
||||
"以下出入库单已执行或已关联地磅,无法取消生成地磅:"
|
||||
+ ";".join(blocking)
|
||||
+ "。请在地磅管理中处理后再操作。"
|
||||
)
|
||||
|
||||
for link in links:
|
||||
_soft_delete_link_and_inventory(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
link=link,
|
||||
user_id=user_id,
|
||||
)
|
||||
db.flush()
|
||||
|
||||
|
||||
def _update_existing_stop_inventory(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
plan: PlPlan,
|
||||
link: PlTransportDbLink,
|
||||
inventory: DbInboundOutbound,
|
||||
product: _StopDibangProduct,
|
||||
partner: str,
|
||||
license_plate: str,
|
||||
dibang_date: date,
|
||||
user_id: Optional[str],
|
||||
dept_id: str,
|
||||
warnings: list[str],
|
||||
) -> DibangInventoryLinkResult:
|
||||
"""就地更新已有出入库可变字段;不可编辑时写 warnings,不抛错。"""
|
||||
inv_product_id = (inventory.product_id or "").strip()
|
||||
new_product_id = (product.product_id or "").strip()
|
||||
if inv_product_id and new_product_id and inv_product_id != new_product_id:
|
||||
label = inventory.batch_no or inventory.id
|
||||
raise ValueError(
|
||||
f"出入库单 {label} 已关联产品,禁止更换为其他产品。请取消地磅后重新勾选,或保留原产品。"
|
||||
)
|
||||
|
||||
label = inventory.batch_no or inventory.id
|
||||
editable = _inventory_mutable_editable(db, inventory)
|
||||
|
||||
if not editable:
|
||||
status = (inventory.status or "").strip() or "未知状态"
|
||||
if _inventory_has_weighing_slip(db, inventory.id):
|
||||
reason = "已关联地磅单"
|
||||
else:
|
||||
reason = f"状态:{status}"
|
||||
warnings.append(
|
||||
f"出入库单 {label}({reason})未同步往来单位/车牌/数量等可变字段,请在地磅管理中手工处理"
|
||||
)
|
||||
# 备注中计划编号文案仍可校正
|
||||
desired_remark = f"运输计划 {plan.plan_no}"
|
||||
if (inventory.remark or "").strip() != desired_remark:
|
||||
inventory.remark = desired_remark
|
||||
inventory.update_by = user_id
|
||||
if (link.remark or "").strip() != desired_remark:
|
||||
link.remark = desired_remark
|
||||
link.update_by = user_id
|
||||
return DibangInventoryLinkResult(
|
||||
plan_product_id=product.plan_product_id,
|
||||
product_name=product.product_name or "",
|
||||
db_purchase_plan_id=link.db_purchase_plan_id or "",
|
||||
db_inventory_id=inventory.id,
|
||||
batch_no=inventory.batch_no or "",
|
||||
plan_no=(inventory.plan_no or plan.plan_no) or "",
|
||||
)
|
||||
|
||||
# —— 可编辑:更新 partner / vehicle / quantity / date(跨月改挂)——
|
||||
if partner and (inventory.partner_company or "").strip() != partner:
|
||||
inventory.partner_company = partner
|
||||
|
||||
plate = (license_plate or "").strip()
|
||||
if plate and (inventory.vehicle_no or "").strip() != plate:
|
||||
inventory.vehicle_no = plate
|
||||
|
||||
new_qty = float(product.quantity)
|
||||
old_qty = _resolve_link_contributed_qty(link, inventory)
|
||||
if abs(new_qty - old_qty) > 1e-9:
|
||||
delta = new_qty - old_qty
|
||||
if link.db_purchase_plan_id:
|
||||
db_plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(
|
||||
DbPurchasePlan.id == link.db_purchase_plan_id,
|
||||
DbPurchasePlan.tenant_no == tenant_no,
|
||||
DbPurchasePlan.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if db_plan:
|
||||
_apply_plan_quantity_delta(db_plan, delta, user_id=user_id)
|
||||
inventory.quantity = Decimal(str(new_qty))
|
||||
link.contributed_qty = Decimal(str(new_qty))
|
||||
|
||||
old_date = _normalize_to_date(inventory.date)
|
||||
if old_date != dibang_date:
|
||||
same_month = (
|
||||
old_date is not None
|
||||
and old_date.year == dibang_date.year
|
||||
and old_date.month == dibang_date.month
|
||||
)
|
||||
inventory.date = dibang_date
|
||||
if not same_month:
|
||||
qty = _resolve_link_contributed_qty(link, inventory)
|
||||
_subtract_plan_contribution(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
link=link,
|
||||
inventory=inventory,
|
||||
user_id=user_id,
|
||||
)
|
||||
try:
|
||||
new_db_plan = _ensure_monthly_db_plan_for_inventory(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
plan=plan,
|
||||
inventory=inventory,
|
||||
dibang_date=dibang_date,
|
||||
contributed_qty=qty,
|
||||
user_id=user_id,
|
||||
dept_id=dept_id or "",
|
||||
)
|
||||
link.db_purchase_plan_id = new_db_plan.id
|
||||
inventory.purchase_plan_id = new_db_plan.id
|
||||
inventory.plan_no = (new_db_plan.plan_no or "").strip() or inventory.plan_no
|
||||
except ValueError as exc:
|
||||
warnings.append(f"出入库单 {label} 跨月改挂失败:{exc}")
|
||||
|
||||
desired_remark = f"运输计划 {plan.plan_no}"
|
||||
inventory.remark = desired_remark
|
||||
link.remark = desired_remark
|
||||
inventory.update_by = user_id
|
||||
link.update_by = user_id
|
||||
|
||||
return DibangInventoryLinkResult(
|
||||
plan_product_id=product.plan_product_id,
|
||||
product_name=product.product_name or "",
|
||||
db_purchase_plan_id=link.db_purchase_plan_id or "",
|
||||
db_inventory_id=inventory.id,
|
||||
batch_no=inventory.batch_no or "",
|
||||
plan_no=(inventory.plan_no or plan.plan_no) or "",
|
||||
)
|
||||
|
||||
|
||||
def _resolve_link_dibang_date(
|
||||
plan: PlPlan,
|
||||
link: PlTransportDbLink,
|
||||
@@ -728,13 +982,16 @@ def _sync_stop_inventory_links(
|
||||
stop_products: list[_StopDibangProduct],
|
||||
user_id: Optional[str],
|
||||
dept_id: str,
|
||||
warnings: Optional[list[str]] = None,
|
||||
) -> list[DibangInventoryLinkResult]:
|
||||
"""按段产品 upsert 出入库+link:已有则就地更新,无则新建。"""
|
||||
if not partner:
|
||||
raise ValueError("无法解析往来单位,请检查段客户信息")
|
||||
|
||||
month_start, month_end = _month_range(dibang_date)
|
||||
license_plate = (transport.license_plate or plan.license_plate or "").strip()
|
||||
results: list[DibangInventoryLinkResult] = []
|
||||
warn_buf = warnings if warnings is not None else []
|
||||
|
||||
for product in stop_products:
|
||||
if not product.product_id:
|
||||
@@ -744,6 +1001,47 @@ def _sync_stop_inventory_links(
|
||||
if contributed_qty <= 0:
|
||||
continue
|
||||
|
||||
existing_link = _find_active_stop_link(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
transport_id=transport.id,
|
||||
plan_id=plan.id,
|
||||
stop_type=stop_type,
|
||||
stop_id=stop_id,
|
||||
plan_product_id=product.plan_product_id,
|
||||
)
|
||||
if existing_link:
|
||||
inventory = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.id == existing_link.db_inventory_id,
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if inventory:
|
||||
results.append(
|
||||
_update_existing_stop_inventory(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
plan=plan,
|
||||
link=existing_link,
|
||||
inventory=inventory,
|
||||
product=product,
|
||||
partner=partner,
|
||||
license_plate=license_plate,
|
||||
dibang_date=dibang_date,
|
||||
user_id=user_id,
|
||||
dept_id=dept_id,
|
||||
warnings=warn_buf,
|
||||
)
|
||||
)
|
||||
continue
|
||||
# link 悬空:软删后走新建
|
||||
existing_link.del_flag = 1
|
||||
existing_link.update_by = user_id
|
||||
|
||||
plan_product = PlPlanProduct(
|
||||
id=product.plan_product_id,
|
||||
product_id=product.product_id,
|
||||
@@ -899,7 +1197,7 @@ def _delivery_stop_products(
|
||||
]
|
||||
|
||||
|
||||
def sync_dibang_for_plan_stops(
|
||||
def upsert_stop_dibang_links(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
@@ -908,8 +1206,11 @@ def sync_dibang_for_plan_stops(
|
||||
stop_options: list[StopDibangOption],
|
||||
user_id: Optional[str],
|
||||
dept_id: str,
|
||||
) -> list[DibangInventoryLinkResult]:
|
||||
"""按装货/送货段勾选生成地磅计划与出入库单。"""
|
||||
) -> tuple[list[DibangInventoryLinkResult], list[str]]:
|
||||
"""按段 upsert 地磅出入库与 link:新建 / 就地更新 / 取消勾选软删。
|
||||
|
||||
返回 (link_results, warnings)。已过磅取消勾选或产品变更时抛 ValueError。
|
||||
"""
|
||||
apply_stop_dibang_options(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
@@ -920,6 +1221,7 @@ def sync_dibang_for_plan_stops(
|
||||
|
||||
option_map = {(item.stop_type, item.stop_id): item for item in stop_options}
|
||||
results: list[DibangInventoryLinkResult] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
deliveries = (
|
||||
db.query(PlPlanDelivery)
|
||||
@@ -941,73 +1243,113 @@ def sync_dibang_for_plan_stops(
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if load:
|
||||
option = option_map.get(("load", load.id))
|
||||
if option and option.generate_dibang:
|
||||
business_type = _resolve_stop_business_type(load, "load")
|
||||
|
||||
def _handle_stop(
|
||||
*,
|
||||
stop_type: Literal["load", "delivery"],
|
||||
stop: PlPlanLoad | PlPlanDelivery,
|
||||
partner: str,
|
||||
dibang_date: date,
|
||||
) -> None:
|
||||
option = option_map.get((stop_type, stop.id))
|
||||
if option is None:
|
||||
return
|
||||
if not option.generate_dibang:
|
||||
_cancel_stop_dibang_links(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
transport_id=transport.id,
|
||||
plan_id=plan.id,
|
||||
stop_type=stop_type,
|
||||
stop_id=stop.id,
|
||||
user_id=user_id,
|
||||
)
|
||||
return
|
||||
|
||||
if stop_type == "load":
|
||||
stop_products = _load_stop_products(db, tenant_no, stop.id)
|
||||
else:
|
||||
stop_products = _delivery_stop_products(db, tenant_no, stop.id)
|
||||
|
||||
business_type = _resolve_stop_business_type(stop, stop_type)
|
||||
if not business_type:
|
||||
raise ValueError("装货段未配置地磅方向,无法生成地磅单")
|
||||
stop_products = _load_stop_products(db, tenant_no, load.id)
|
||||
stop_label = getattr(stop, "customer_name", None) or stop.id
|
||||
raise ValueError(f"{'装货' if stop_type == 'load' else '送货'}段【{stop_label}】未配置地磅方向,无法生成地磅单")
|
||||
if not stop_products:
|
||||
raise ValueError("装货段无产品明细,无法生成地磅单")
|
||||
partner = resolve_stop_dibang_partner("load", load, deliveries)
|
||||
stop_label = getattr(stop, "customer_name", None) or stop.id
|
||||
raise ValueError(f"{'装货' if stop_type == 'load' else '送货'}段【{stop_label}】无产品明细,无法生成地磅单")
|
||||
if not partner:
|
||||
raise ValueError("无法解析装货段地磅往来单位,请先配置送货客户")
|
||||
raise ValueError(
|
||||
"无法解析装货段地磅往来单位,请先配置送货客户"
|
||||
if stop_type == "load"
|
||||
else "无法解析送货段地磅往来单位,请先配置装货客户"
|
||||
)
|
||||
results.extend(
|
||||
_sync_stop_inventory_links(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
plan=plan,
|
||||
transport=transport,
|
||||
stop_type="load",
|
||||
stop_id=load.id,
|
||||
stop_type=stop_type,
|
||||
stop_id=stop.id,
|
||||
partner=partner,
|
||||
business_type=business_type,
|
||||
dibang_date=dibang_date,
|
||||
stop_products=stop_products,
|
||||
user_id=user_id,
|
||||
dept_id=dept_id,
|
||||
warnings=warnings,
|
||||
)
|
||||
)
|
||||
|
||||
if load:
|
||||
_handle_stop(
|
||||
stop_type="load",
|
||||
stop=load,
|
||||
partner=resolve_stop_dibang_partner("load", load, deliveries),
|
||||
dibang_date=_resolve_stop_dibang_date(
|
||||
plan,
|
||||
stop_type="load",
|
||||
load_time=load.load_time,
|
||||
),
|
||||
stop_products=stop_products,
|
||||
user_id=user_id,
|
||||
dept_id=dept_id,
|
||||
)
|
||||
)
|
||||
|
||||
for delivery in deliveries:
|
||||
option = option_map.get(("delivery", delivery.id))
|
||||
if not option or not option.generate_dibang:
|
||||
continue
|
||||
business_type = _resolve_stop_business_type(delivery, "delivery")
|
||||
if not business_type:
|
||||
raise ValueError(f"送货段【{delivery.customer_name or delivery.seq}】未配置地磅方向,无法生成地磅单")
|
||||
stop_products = _delivery_stop_products(db, tenant_no, delivery.id)
|
||||
if not stop_products:
|
||||
raise ValueError(f"送货段【{delivery.customer_name or delivery.seq}】无产品明细,无法生成地磅单")
|
||||
partner = resolve_stop_dibang_partner("delivery", load, deliveries)
|
||||
if not partner:
|
||||
raise ValueError("无法解析送货段地磅往来单位,请先配置装货客户")
|
||||
results.extend(
|
||||
_sync_stop_inventory_links(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
plan=plan,
|
||||
transport=transport,
|
||||
_handle_stop(
|
||||
stop_type="delivery",
|
||||
stop_id=delivery.id,
|
||||
partner=partner,
|
||||
business_type=business_type,
|
||||
stop=delivery,
|
||||
partner=resolve_stop_dibang_partner("delivery", load, deliveries),
|
||||
dibang_date=_resolve_stop_dibang_date(
|
||||
plan,
|
||||
stop_type="delivery",
|
||||
delivery_time=delivery.delivery_time,
|
||||
),
|
||||
stop_products=stop_products,
|
||||
)
|
||||
|
||||
db.flush()
|
||||
return results, warnings
|
||||
|
||||
|
||||
def sync_dibang_for_plan_stops(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
plan: PlPlan,
|
||||
transport: PlTransport,
|
||||
stop_options: list[StopDibangOption],
|
||||
user_id: Optional[str],
|
||||
dept_id: str,
|
||||
) -> list[DibangInventoryLinkResult]:
|
||||
"""按装货/送货段勾选 upsert 地磅计划与出入库单(兼容旧调用方,忽略 warnings)。"""
|
||||
results, _warnings = upsert_stop_dibang_links(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
plan=plan,
|
||||
transport=transport,
|
||||
stop_options=stop_options,
|
||||
user_id=user_id,
|
||||
dept_id=dept_id,
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
"""地磅按段 upsert — 取消勾选阻断 / 复用出入库 id"""
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models.db_models import DbInboundOutbound, DbPurchasePlan
|
||||
from app.models.pl_models import (
|
||||
PlPlan,
|
||||
PlPlanDelivery,
|
||||
PlPlanDeliveryProduct,
|
||||
PlPlanLoad,
|
||||
PlPlanLoadProduct,
|
||||
PlTransport,
|
||||
PlTransportDbLink,
|
||||
)
|
||||
from app.services.pl_dibang_bridge_service import (
|
||||
StopDibangOption,
|
||||
upsert_stop_dibang_links,
|
||||
)
|
||||
|
||||
|
||||
def _make_query_router(mapping):
|
||||
def query_router(model):
|
||||
mock = MagicMock()
|
||||
handler = mapping.get(model)
|
||||
if handler is None:
|
||||
return MagicMock()
|
||||
return handler(mock)
|
||||
|
||||
return query_router
|
||||
|
||||
|
||||
@patch("app.services.pl_dibang_bridge_service._inventory_has_weighing_slip", return_value=True)
|
||||
def test_cancel_generate_dibang_blocked_when_weighing_exists(_mock_weighing):
|
||||
plan = PlPlan(
|
||||
id="plan-1",
|
||||
plan_no="PO1",
|
||||
plan_type="outbound",
|
||||
plan_date=date(2026, 9, 1),
|
||||
tenant_no="t1",
|
||||
)
|
||||
transport = PlTransport(id="tr-1", tenant_no="t1", license_plate="皖A12345", del_flag=0)
|
||||
load = PlPlanLoad(
|
||||
id="load-1",
|
||||
plan_id="plan-1",
|
||||
customer_name="客户A",
|
||||
generate_dibang=1,
|
||||
dibang_direction="outbound",
|
||||
load_time=datetime(2026, 9, 1, 0, 0),
|
||||
tenant_no="t1",
|
||||
del_flag=0,
|
||||
)
|
||||
inventory = DbInboundOutbound(
|
||||
id="inv-1",
|
||||
tenant_no="t1",
|
||||
date=date(2026, 9, 1),
|
||||
partner_company="客户A",
|
||||
business_type="出库",
|
||||
product_id="prod-1",
|
||||
product="硫酸",
|
||||
quantity=30.0,
|
||||
batch_no="CRKDH0002400",
|
||||
status="待出库",
|
||||
del_flag=0,
|
||||
)
|
||||
link = PlTransportDbLink(
|
||||
id="link-1",
|
||||
tenant_no="t1",
|
||||
plan_id="plan-1",
|
||||
transport_id="tr-1",
|
||||
plan_product_id="pp-1",
|
||||
stop_type="load",
|
||||
stop_id="load-1",
|
||||
db_purchase_plan_id="dbp-1",
|
||||
db_inventory_id="inv-1",
|
||||
contributed_qty=Decimal("30"),
|
||||
del_flag=0,
|
||||
)
|
||||
|
||||
db = MagicMock()
|
||||
|
||||
def link_q(m):
|
||||
filtered = MagicMock()
|
||||
filtered.all.return_value = [link]
|
||||
filtered.first.return_value = link
|
||||
m.filter.return_value = filtered
|
||||
return m
|
||||
|
||||
def load_q(m):
|
||||
m.filter.return_value.first.return_value = load
|
||||
return m
|
||||
|
||||
def delivery_q(m):
|
||||
m.filter.return_value.order_by.return_value.all.return_value = []
|
||||
return m
|
||||
|
||||
def inv_q(m):
|
||||
m.filter.return_value.first.return_value = inventory
|
||||
return m
|
||||
|
||||
db.query.side_effect = _make_query_router(
|
||||
{
|
||||
PlTransportDbLink: link_q,
|
||||
PlPlanLoad: load_q,
|
||||
PlPlanDelivery: delivery_q,
|
||||
DbInboundOutbound: inv_q,
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="CRKDH0002400"):
|
||||
upsert_stop_dibang_links(
|
||||
db,
|
||||
tenant_no="t1",
|
||||
plan=plan,
|
||||
transport=transport,
|
||||
stop_options=[
|
||||
StopDibangOption(stop_type="load", stop_id="load-1", generate_dibang=False),
|
||||
],
|
||||
user_id="u1",
|
||||
dept_id="d1",
|
||||
)
|
||||
|
||||
|
||||
@patch("app.services.pl_dibang_bridge_service._inventory_has_weighing_slip", return_value=False)
|
||||
@patch("app.services.pl_dibang_bridge_service.find_monthly_db_plan")
|
||||
@patch("app.services.pl_dibang_bridge_service._create_db_inventory")
|
||||
@patch("app.services.pl_dibang_bridge_service.resolve_crm_product")
|
||||
def test_upsert_reuses_existing_inventory_id(
|
||||
_mock_crm,
|
||||
mock_create_inv,
|
||||
mock_find_plan,
|
||||
_mock_weighing,
|
||||
):
|
||||
plan = PlPlan(
|
||||
id="plan-1",
|
||||
plan_no="PO1",
|
||||
plan_type="outbound",
|
||||
plan_date=date(2026, 9, 1),
|
||||
pickup_time=datetime(2026, 9, 1, 0, 0),
|
||||
license_plate="皖A12345",
|
||||
tenant_no="t1",
|
||||
)
|
||||
transport = PlTransport(id="tr-1", tenant_no="t1", license_plate="皖A12345", del_flag=0)
|
||||
load = PlPlanLoad(
|
||||
id="load-1",
|
||||
plan_id="plan-1",
|
||||
customer_name="客户A",
|
||||
generate_dibang=1,
|
||||
dibang_direction="outbound",
|
||||
load_time=datetime(2026, 9, 1, 0, 0),
|
||||
tenant_no="t1",
|
||||
del_flag=0,
|
||||
)
|
||||
delivery = PlPlanDelivery(
|
||||
id="del-1",
|
||||
plan_id="plan-1",
|
||||
customer_name="客户B",
|
||||
seq=1,
|
||||
generate_dibang=0,
|
||||
tenant_no="t1",
|
||||
del_flag=0,
|
||||
)
|
||||
stop_product = PlPlanLoadProduct(
|
||||
id="lp-1",
|
||||
load_id="load-1",
|
||||
plan_product_id="pp-1",
|
||||
product_id="prod-1",
|
||||
product_name="硫酸",
|
||||
model="98%",
|
||||
unit="吨",
|
||||
plan_qty=Decimal("30"),
|
||||
tenant_no="t1",
|
||||
del_flag=0,
|
||||
order_num=1,
|
||||
)
|
||||
db_plan = DbPurchasePlan(
|
||||
id="dbp-1",
|
||||
tenant_no="t1",
|
||||
plan_no="SX-1",
|
||||
planned_quantity=30.0,
|
||||
del_flag=0,
|
||||
)
|
||||
inventory = DbInboundOutbound(
|
||||
id="inv-1",
|
||||
tenant_no="t1",
|
||||
date=date(2026, 9, 1),
|
||||
partner_company="客户B",
|
||||
business_type="出库",
|
||||
product_id="prod-1",
|
||||
product="硫酸",
|
||||
specification="98%",
|
||||
quantity=30.0,
|
||||
batch_no="CRKDH0000001",
|
||||
vehicle_no="皖A12345",
|
||||
status="待出库",
|
||||
purchase_plan_id="dbp-1",
|
||||
plan_no="SX-1",
|
||||
del_flag=0,
|
||||
)
|
||||
link = PlTransportDbLink(
|
||||
id="link-1",
|
||||
tenant_no="t1",
|
||||
plan_id="plan-1",
|
||||
transport_id="tr-1",
|
||||
plan_product_id="pp-1",
|
||||
stop_type="load",
|
||||
stop_id="load-1",
|
||||
db_purchase_plan_id="dbp-1",
|
||||
db_inventory_id="inv-1",
|
||||
contributed_qty=Decimal("30"),
|
||||
del_flag=0,
|
||||
)
|
||||
|
||||
mock_create_inv.side_effect = AssertionError("must reuse existing inventory")
|
||||
mock_find_plan.return_value = db_plan
|
||||
|
||||
db = MagicMock()
|
||||
|
||||
def link_q(m):
|
||||
filtered = MagicMock()
|
||||
filtered.all.return_value = [link]
|
||||
filtered.first.return_value = link
|
||||
m.filter.return_value = filtered
|
||||
return m
|
||||
|
||||
def load_q(m):
|
||||
m.filter.return_value.first.return_value = load
|
||||
return m
|
||||
|
||||
def delivery_q(m):
|
||||
m.filter.return_value.order_by.return_value.all.return_value = [delivery]
|
||||
return m
|
||||
|
||||
def load_prod_q(m):
|
||||
m.filter.return_value.order_by.return_value.all.return_value = [stop_product]
|
||||
return m
|
||||
|
||||
def delivery_prod_q(m):
|
||||
m.filter.return_value.order_by.return_value.all.return_value = []
|
||||
return m
|
||||
|
||||
def inv_q(m):
|
||||
m.filter.return_value.first.return_value = inventory
|
||||
return m
|
||||
|
||||
def plan_q(m):
|
||||
m.filter.return_value.first.return_value = db_plan
|
||||
return m
|
||||
|
||||
db.query.side_effect = _make_query_router(
|
||||
{
|
||||
PlTransportDbLink: link_q,
|
||||
PlPlanLoad: load_q,
|
||||
PlPlanDelivery: delivery_q,
|
||||
PlPlanLoadProduct: load_prod_q,
|
||||
PlPlanDeliveryProduct: delivery_prod_q,
|
||||
DbInboundOutbound: inv_q,
|
||||
DbPurchasePlan: plan_q,
|
||||
}
|
||||
)
|
||||
|
||||
results, warnings = upsert_stop_dibang_links(
|
||||
db,
|
||||
tenant_no="t1",
|
||||
plan=plan,
|
||||
transport=transport,
|
||||
stop_options=[
|
||||
StopDibangOption(stop_type="load", stop_id="load-1", generate_dibang=True),
|
||||
],
|
||||
user_id="u1",
|
||||
dept_id="d1",
|
||||
)
|
||||
|
||||
assert warnings == []
|
||||
assert len(results) == 1
|
||||
assert results[0].db_inventory_id == "inv-1"
|
||||
assert results[0].batch_no == "CRKDH0000001"
|
||||
mock_create_inv.assert_not_called()
|
||||
Reference in new issue
Block a user