fix(crm): 合同改号后同步运输计划合同号快照。
归档列表读 pl_plan_contract 快照,改 CRM 合同编号时同事务回写,避免列表与详情不一致。
This commit is contained in:
1 parent
d58f6ea5bd
commit
bfb611b2e1
4 files changed
+292
No files matched your search
@@ -15,6 +15,7 @@ from sqlalchemy.orm import Query, Session
|
||||
from app.core.delete_guard import assert_row_deletable_by_creator
|
||||
from app.core.utils import generate_uuid
|
||||
from app.services.wechat_approval_notices import sync_approval_notifications
|
||||
from app.services.pl_plan_contract_no_sync import sync_plan_contract_no_snapshots
|
||||
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope, ensure_crm_owner_row_in_scope, apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope
|
||||
from app.crud.crm.contact import get_contact
|
||||
from app.crud.crm.contract_approval import (
|
||||
@@ -1497,6 +1498,7 @@ def update_contract(
|
||||
if product_rows is not None:
|
||||
_resolve_contract_product_ids(db, tenant_no=tenant_no, products=product_rows, user_id=user_id)
|
||||
|
||||
prev_contract_no = (old.contract_no or "").strip()
|
||||
sign_date = old.sign_date or date.today()
|
||||
submitted_no = (_get_field(data, "contract_no", "contractNo") or "").strip() or (
|
||||
old.contract_no or ""
|
||||
@@ -1540,6 +1542,17 @@ def update_contract(
|
||||
contract_type=contract_type, user_id=user_id, dept_id=dept_id,
|
||||
prepared=prepared_products)
|
||||
|
||||
new_contract_no = (old.contract_no or "").strip()
|
||||
if new_contract_no != prev_contract_no:
|
||||
# 归档列表读计划侧 contract_no 快照;改号后必须同事务回写
|
||||
sync_plan_contract_no_snapshots(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
contract_id=contract_id,
|
||||
contract_no=new_contract_no,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
db.flush()
|
||||
sync_approval_notifications(
|
||||
db, "crm_contract", contract_id, tenant_no,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""CRM 合同编号变更时,回写运输计划侧合同号快照。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.pl_models import PlPlan, PlPlanContract
|
||||
|
||||
|
||||
def sync_plan_contract_no_snapshots(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
contract_id: str,
|
||||
contract_no: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> int:
|
||||
"""按 contract_id 刷新 pl_plan_contract / pl_plan 上的 contract_no 快照。
|
||||
|
||||
归档列表等读快照字段;仅改 CRM 主表会导致列表与详情不一致。
|
||||
返回实际改写的行数(关联表 + 主表遗留字段合计)。
|
||||
"""
|
||||
cid = (contract_id or "").strip()
|
||||
if not cid:
|
||||
return 0
|
||||
new_no = (contract_no or "").strip()
|
||||
now = datetime.now()
|
||||
changed = 0
|
||||
|
||||
link_rows = (
|
||||
db.query(PlPlanContract)
|
||||
.filter(
|
||||
PlPlanContract.tenant_no == tenant_no,
|
||||
PlPlanContract.contract_id == cid,
|
||||
PlPlanContract.del_flag == 0,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for row in link_rows:
|
||||
if (row.contract_no or "") == new_no:
|
||||
continue
|
||||
row.contract_no = new_no or None
|
||||
row.update_by = user_id
|
||||
row.update_time = now
|
||||
changed += 1
|
||||
|
||||
plan_rows = (
|
||||
db.query(PlPlan)
|
||||
.filter(
|
||||
PlPlan.tenant_no == tenant_no,
|
||||
PlPlan.contract_id == cid,
|
||||
PlPlan.del_flag == 0,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for row in plan_rows:
|
||||
if (row.contract_no or "") == new_no:
|
||||
continue
|
||||
row.contract_no = new_no or None
|
||||
row.update_by = user_id
|
||||
row.update_time = now
|
||||
changed += 1
|
||||
|
||||
return changed
|
||||
@@ -0,0 +1,174 @@
|
||||
"""CRM 合同改号后,运输计划合同号快照应同步。"""
|
||||
from datetime import date
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import pl_models as m
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db():
|
||||
engine = create_engine("sqlite://")
|
||||
m.PlPlan.__table__.create(engine, checkfirst=True)
|
||||
m.PlPlanContract.__table__.create(engine, checkfirst=True)
|
||||
with Session(engine, autoflush=False) as session:
|
||||
yield session
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _seed(db: Session):
|
||||
plan = m.PlPlan(
|
||||
id="plan-1",
|
||||
plan_no="PO20261012005",
|
||||
create_date=date(2026, 10, 12),
|
||||
belongs_company="shanxia",
|
||||
plan_type="direct",
|
||||
contract_id="contract-1",
|
||||
contract_no="SX/X-JT20261009002",
|
||||
tenant_no="t1",
|
||||
)
|
||||
other_plan = m.PlPlan(
|
||||
id="plan-2",
|
||||
plan_no="PO20261012099",
|
||||
create_date=date(2026, 10, 12),
|
||||
belongs_company="shanxia",
|
||||
plan_type="direct",
|
||||
contract_id="contract-other",
|
||||
contract_no="OTHER-001",
|
||||
tenant_no="t1",
|
||||
)
|
||||
link = m.PlPlanContract(
|
||||
id="link-1",
|
||||
plan_id="plan-1",
|
||||
side="delivery",
|
||||
stop_id="stop-1",
|
||||
contract_id="contract-1",
|
||||
contract_no="SX/X-JT20261009002",
|
||||
tenant_no="t1",
|
||||
)
|
||||
deleted = m.PlPlanContract(
|
||||
id="link-del",
|
||||
plan_id="plan-1",
|
||||
side="delivery",
|
||||
contract_id="contract-1",
|
||||
contract_no="SX/X-JT20261009002",
|
||||
tenant_no="t1",
|
||||
del_flag=1,
|
||||
)
|
||||
other_tenant = m.PlPlanContract(
|
||||
id="link-tenant",
|
||||
plan_id="plan-x",
|
||||
side="delivery",
|
||||
contract_id="contract-1",
|
||||
contract_no="SX/X-JT20261009002",
|
||||
tenant_no="other",
|
||||
)
|
||||
db.add_all([plan, other_plan, link, deleted, other_tenant])
|
||||
db.flush()
|
||||
return plan, link
|
||||
|
||||
|
||||
def test_sync_refreshes_plan_link_and_legacy_header(db):
|
||||
from app.services.pl_plan_contract_no_sync import sync_plan_contract_no_snapshots
|
||||
|
||||
plan, link = _seed(db)
|
||||
changed = sync_plan_contract_no_snapshots(
|
||||
db,
|
||||
tenant_no="t1",
|
||||
contract_id="contract-1",
|
||||
contract_no="P0-2026-10-0016",
|
||||
user_id="u1",
|
||||
)
|
||||
db.flush()
|
||||
db.expire_all()
|
||||
|
||||
assert changed >= 2
|
||||
assert db.get(m.PlPlanContract, "link-1").contract_no == "P0-2026-10-0016"
|
||||
assert db.get(m.PlPlan, "plan-1").contract_no == "P0-2026-10-0016"
|
||||
assert db.get(m.PlPlanContract, "link-1").update_by == "u1"
|
||||
assert db.get(m.PlPlan, "plan-2").contract_no == "OTHER-001"
|
||||
assert db.get(m.PlPlanContract, "link-del").contract_no == "SX/X-JT20261009002"
|
||||
assert db.get(m.PlPlanContract, "link-tenant").contract_no == "SX/X-JT20261009002"
|
||||
|
||||
|
||||
def test_sync_noop_when_already_current(db):
|
||||
from app.services.pl_plan_contract_no_sync import sync_plan_contract_no_snapshots
|
||||
|
||||
_seed(db)
|
||||
sync_plan_contract_no_snapshots(
|
||||
db,
|
||||
tenant_no="t1",
|
||||
contract_id="contract-1",
|
||||
contract_no="P0-2026-10-0016",
|
||||
user_id="u1",
|
||||
)
|
||||
db.flush()
|
||||
first_updated = db.get(m.PlPlanContract, "link-1").update_time
|
||||
changed = sync_plan_contract_no_snapshots(
|
||||
db,
|
||||
tenant_no="t1",
|
||||
contract_id="contract-1",
|
||||
contract_no="P0-2026-10-0016",
|
||||
user_id="u2",
|
||||
)
|
||||
db.flush()
|
||||
db.expire_all()
|
||||
assert changed == 0
|
||||
assert db.get(m.PlPlanContract, "link-1").update_by == "u1"
|
||||
assert db.get(m.PlPlanContract, "link-1").update_time == first_updated
|
||||
|
||||
|
||||
def test_update_contract_calls_sync_when_contract_no_changes():
|
||||
import app.crud.crm.contract as contract_crud
|
||||
|
||||
old = MagicMock()
|
||||
old.status = "executing"
|
||||
old.contract_type = "sale"
|
||||
old.contract_no = "SX/X-JT20261009002"
|
||||
old.contract_name = "临时"
|
||||
old.party_a = "A"
|
||||
old.party_b = "B"
|
||||
old.supplier_id = "s"
|
||||
old.customer_id = "c"
|
||||
old.customer_name = "客"
|
||||
old.sign_date = date(2026, 10, 9)
|
||||
old.id = "contract-1"
|
||||
|
||||
with patch.object(contract_crud, "_get_contract_row_in_scope", return_value=old), patch.object(
|
||||
contract_crud, "invalidate_all_approvals"
|
||||
), patch.object(contract_crud, "_validate_sale_purchase_contract_amount"), patch.object(
|
||||
contract_crud, "_apply_contract_entity_fields"
|
||||
), patch.object(contract_crud, "assert_products_allowed_for_supplier"), patch.object(
|
||||
contract_crud, "list_contract_products", return_value=[]
|
||||
), patch.object(
|
||||
contract_crud, "align_contract_no_to_sign_date", return_value="P0-2026-10-0016"
|
||||
), patch.object(
|
||||
contract_crud, "contract_no_exists", return_value=False
|
||||
), patch.object(
|
||||
contract_crud, "_build_contract_name_from_parties", return_value="正式"
|
||||
), patch.object(
|
||||
contract_crud, "get_contract_detail", return_value={"id": "contract-1"}
|
||||
), patch.object(contract_crud, "sync_contract_products"), patch(
|
||||
"app.crud.crm.contract.sync_plan_contract_no_snapshots"
|
||||
) as sync_snap, patch(
|
||||
"app.crud.crm.contract.sync_approval_notifications"
|
||||
):
|
||||
db = MagicMock()
|
||||
contract_crud.update_contract(
|
||||
db,
|
||||
"contract-1",
|
||||
tenant_no="t1",
|
||||
data={"contractNo": "P0-2026-10-0016"},
|
||||
user_id="u1",
|
||||
)
|
||||
sync_snap.assert_called_once_with(
|
||||
db,
|
||||
tenant_no="t1",
|
||||
contract_id="contract-1",
|
||||
contract_no="P0-2026-10-0016",
|
||||
user_id="u1",
|
||||
)
|
||||
assert old.contract_no == "P0-2026-10-0016"
|
||||
@@ -0,0 +1,39 @@
|
||||
-- 一次性数据修复:将 pl_plan_contract / pl_plan 合同号快照对齐到 CRM 现网编号。
|
||||
-- 背景:合同页直接改 contract_no 后,计划归档列表仍读快照,导致列表与详情不一致。
|
||||
-- 范围:全库 del_flag=0 且快照与 crm_contract.contract_no 不一致的关联/遗留主表行。
|
||||
-- 含客户反馈三单:PO20261008011 / PO20261009018 / PO20261012005(临时号 SX/X-JT20261009002 → P0-2026-10-0016)。
|
||||
-- MySQL 5.7。可重复执行(已对齐行不再更新)。
|
||||
-- !!! 生产执行须单独授权;本文件不随应用启动自动执行。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 预览(执行前建议先跑):
|
||||
-- SELECT p.plan_no, pc.id AS link_id, pc.side, pc.contract_id,
|
||||
-- pc.contract_no AS snapshot_no, c.contract_no AS live_no
|
||||
-- FROM pl_plan_contract pc
|
||||
-- JOIN pl_plan p ON p.id = pc.plan_id AND p.del_flag = 0
|
||||
-- JOIN crm_contract c ON c.id = pc.contract_id AND c.tenant_no = pc.tenant_no AND c.del_flag = 0
|
||||
-- WHERE pc.del_flag = 0
|
||||
-- AND IFNULL(pc.contract_no, '') <> IFNULL(c.contract_no, '');
|
||||
|
||||
UPDATE pl_plan_contract pc
|
||||
INNER JOIN crm_contract c
|
||||
ON c.id = pc.contract_id
|
||||
AND c.tenant_no = pc.tenant_no
|
||||
AND c.del_flag = 0
|
||||
SET pc.contract_no = c.contract_no,
|
||||
pc.update_time = NOW()
|
||||
WHERE pc.del_flag = 0
|
||||
AND IFNULL(pc.contract_no, '') <> IFNULL(c.contract_no, '');
|
||||
|
||||
UPDATE pl_plan p
|
||||
INNER JOIN crm_contract c
|
||||
ON c.id = p.contract_id
|
||||
AND c.tenant_no = p.tenant_no
|
||||
AND c.del_flag = 0
|
||||
SET p.contract_no = c.contract_no,
|
||||
p.update_time = NOW()
|
||||
WHERE p.del_flag = 0
|
||||
AND p.contract_id IS NOT NULL
|
||||
AND p.contract_id <> ''
|
||||
AND IFNULL(p.contract_no, '') <> IFNULL(c.contract_no, '');
|
||||
Reference in new issue
Block a user