diff --git a/backend/app/services/pl_dibang_bridge_service.py b/backend/app/services/pl_dibang_bridge_service.py index 235073d3b..e256fe2bb 100644 --- a/backend/app/services/pl_dibang_bridge_service.py +++ b/backend/app/services/pl_dibang_bridge_service.py @@ -698,21 +698,29 @@ def _update_existing_stop_inventory( """就地强制同步出入库正文(含已过磅),并回写关联过磅单业务字段。""" label = inventory.batch_no or inventory.id - # —— 强制更新 partner / vehicle / 产品快照 / quantity / date(跨月改挂)—— - if partner and (inventory.partner_company or "").strip() != partner: + old_partner = (inventory.partner_company or "").strip() + old_product_id = (inventory.product_id or "").strip() + old_spec = (inventory.specification or "").strip() + old_plate = (inventory.vehicle_no or "").strip() + old_date = _normalize_to_date(inventory.date) + + # —— 强制更新 partner / vehicle / 产品快照 / quantity —— + if partner and old_partner != partner: inventory.partner_company = partner plate = (license_plate or "").strip() - if plate and (inventory.vehicle_no or "").strip() != plate: + plate_changed = bool(plate and old_plate != plate) + if plate_changed: inventory.vehicle_no = plate - new_product_id = (product.product_id or "").strip() - if new_product_id: + incoming_product_id = (product.product_id or "").strip() + if incoming_product_id: snapshot = resolve_crm_product( - db, tenant_no, new_product_id, require_on_shelf=False + db, tenant_no, incoming_product_id, require_on_shelf=False ) apply_product_snapshot_to_inventory(inventory, snapshot) + # 数量差量先作用于当前挂靠月计划(改挂前);改挂时会整额扣回旧计划 new_qty = float(product.quantity) old_qty = _resolve_link_contributed_qty(link, inventory) if abs(new_qty - old_qty) > 1e-9: @@ -732,7 +740,57 @@ def _update_existing_stop_inventory( inventory.quantity = Decimal(str(new_qty)) link.contributed_qty = Decimal(str(new_qty)) - old_date = _normalize_to_date(inventory.date) + new_partner = (inventory.partner_company or "").strip() + new_product_id = (inventory.product_id or "").strip() + new_spec = (inventory.specification or "").strip() + need_remount = ( + (partner and old_partner != new_partner) + or (old_product_id and new_product_id and old_product_id != new_product_id) + or (old_spec != new_spec and bool(new_product_id or old_product_id)) + ) + + remounted = False + if need_remount: + 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 + remounted = True + except ValueError as exc: + warnings.append(f"出入库单 {label} 往来/产品改挂失败:{exc}") + elif plate_changed and 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: + db_plan.vehicle_no = plate + db_plan.update_by = user_id + + # —— date:同月仅改日期;跨月改挂(本轮已因往来/产品改挂则不再二次扣量)—— if old_date != dibang_date: same_month = ( old_date is not None @@ -740,7 +798,7 @@ def _update_existing_stop_inventory( and old_date.month == dibang_date.month ) inventory.date = dibang_date - if not same_month: + if not same_month and not remounted: qty = _resolve_link_contributed_qty(link, inventory) _subtract_plan_contribution( db, diff --git a/backend/tests/test_pl_dibang_upsert_on_arrange.py b/backend/tests/test_pl_dibang_upsert_on_arrange.py index 7bbc1d4dd..1cb10db9b 100644 --- a/backend/tests/test_pl_dibang_upsert_on_arrange.py +++ b/backend/tests/test_pl_dibang_upsert_on_arrange.py @@ -458,3 +458,184 @@ def test_upsert_updates_inventory_and_weighing_plate_when_already_weighed( assert slip.tare_weight == 18730.0 assert slip.net_weight == -18730.0 assert not any("未同步" in w for w in warnings) + + +@patch("app.services.pl_dibang_bridge_service._inventory_has_weighing_slip", return_value=False) +@patch("app.services.pl_dibang_bridge_service._create_db_plan") +@patch("app.services.pl_dibang_bridge_service.find_monthly_db_plan") +@patch("app.services.pl_dibang_bridge_service.resolve_crm_product") +def test_upsert_remounts_monthly_plan_when_partner_changes( + mock_crm, + mock_find_plan, + mock_create_plan, + _mock_weighing, +): + """往来单位变更:同月利旧月计划并改挂,出入库 id 不变。""" + mock_crm.return_value = MagicMock( + product_id="prod-1", + product_name="硫酸", + specification="98%", + ) + old_partner = "客户旧" + new_partner = "客户新" + old_plan = DbPurchasePlan( + id="dbp-old", + tenant_no="t1", + plan_no="SX-OLD", + customer_name=old_partner, + planned_quantity=30.0, + del_flag=0, + ) + new_plan = DbPurchasePlan( + id="dbp-new", + tenant_no="t1", + plan_no="SX-NEW", + customer_name=new_partner, + planned_quantity=0.0, + del_flag=0, + ) + mock_find_plan.return_value = new_plan + + 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="山峡", + 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=new_partner, + 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, + ) + inventory = DbInboundOutbound( + id="inv-1", + tenant_no="t1", + date=date(2026, 9, 1), + partner_company=old_partner, + business_type="出库", + product_id="prod-1", + product="硫酸", + specification="98%", + quantity=30.0, + batch_no="CRKDH0000001", + vehicle_no="皖A12345", + status="待出库", + purchase_plan_id="dbp-old", + plan_no="SX-OLD", + 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-old", + 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 = [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): + # _subtract_plan_contribution 查旧计划;后续可能再查 + m.filter.return_value.first.return_value = old_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 inventory.id == "inv-1" + assert inventory.partner_company == new_partner + assert inventory.purchase_plan_id == new_plan.id + assert link.db_purchase_plan_id == new_plan.id + assert inventory.plan_no == "SX-NEW" + mock_find_plan.assert_called() + mock_create_plan.assert_not_called()