协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
209 lines
6.6 KiB
Python
209 lines
6.6 KiB
Python
"""子表按行同步:更新原行、新增缺行、只软删去掉的行。"""
|
|
|
|
import unittest
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Column, DateTime, Integer, SmallInteger, String, create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from app.core.database import Base
|
|
from app.crud.child_row_sync import sync_child_rows
|
|
|
|
|
|
class DemoChild(Base):
|
|
__tablename__ = "demo_child_row"
|
|
|
|
id = Column(String(64), primary_key=True)
|
|
parent_id = Column(String(64), nullable=False)
|
|
tenant_no = Column(String(64), nullable=False)
|
|
item_code = Column(String(64))
|
|
item_name = Column(String(128))
|
|
order_num = Column(Integer)
|
|
create_time = Column(DateTime)
|
|
update_by = Column(String(64))
|
|
update_time = Column(DateTime)
|
|
del_flag = Column(SmallInteger, nullable=False, default=0)
|
|
|
|
|
|
def _session():
|
|
engine = create_engine("sqlite://")
|
|
DemoChild.__table__.create(bind=engine)
|
|
return sessionmaker(bind=engine)()
|
|
|
|
|
|
def _row(**kwargs):
|
|
now = datetime(2026, 10, 27, 9, 0, 0)
|
|
data = dict(
|
|
parent_id="parent-1",
|
|
tenant_no="tenant-a",
|
|
item_code="R-1",
|
|
item_name="试剂",
|
|
order_num=0,
|
|
create_time=now,
|
|
update_time=now,
|
|
del_flag=0,
|
|
)
|
|
data.update(kwargs)
|
|
return DemoChild(**data)
|
|
|
|
|
|
def _sync(db, items):
|
|
now = datetime(2026, 10, 27, 10, 0, 0)
|
|
|
|
def build_row(row_id):
|
|
return DemoChild(
|
|
id=row_id,
|
|
parent_id="parent-1",
|
|
tenant_no="tenant-a",
|
|
create_time=now,
|
|
)
|
|
|
|
def apply_row(row, item, idx, _now):
|
|
row.item_code = item.get("item_code")
|
|
row.item_name = item.get("item_name")
|
|
row.order_num = idx
|
|
|
|
return sync_child_rows(
|
|
db,
|
|
DemoChild,
|
|
parent_attr="parent_id",
|
|
parent_id="parent-1",
|
|
tenant_no="tenant-a",
|
|
items=items,
|
|
user_id="user-1",
|
|
now=now,
|
|
build_row=build_row,
|
|
apply_fields=apply_row,
|
|
natural_key=lambda item: (item.get("item_code") or "").strip(),
|
|
row_natural_key=lambda row: (row.item_code or "").strip(),
|
|
)
|
|
|
|
|
|
class TestChildRowSync(unittest.TestCase):
|
|
def test_id_keeps_row_when_fields_change(self):
|
|
db = _session()
|
|
db.add(_row(id="row-keep", item_name="旧名称"))
|
|
db.commit()
|
|
|
|
saved = _sync(db, [{"id": "row-keep", "item_code": "R-2", "item_name": "新名称"}])
|
|
|
|
self.assertEqual(saved[0].id, "row-keep")
|
|
self.assertEqual(saved[0].item_name, "新名称")
|
|
self.assertEqual(db.query(DemoChild).filter_by(del_flag=0).count(), 1)
|
|
|
|
def test_natural_key_keeps_id_when_payload_omits_id(self):
|
|
db = _session()
|
|
db.add(_row(id="row-keep", item_code="R-1"))
|
|
db.commit()
|
|
|
|
saved = _sync(db, [{"item_code": "R-1", "item_name": "改名"}])
|
|
|
|
self.assertEqual([row.id for row in saved], ["row-keep"])
|
|
self.assertEqual(saved[0].item_name, "改名")
|
|
|
|
def test_insert_update_and_soft_delete_removed(self):
|
|
db = _session()
|
|
db.add(_row(id="row-keep", item_code="R-1"))
|
|
db.add(_row(id="row-drop", item_code="R-9", item_name="去掉"))
|
|
db.commit()
|
|
|
|
saved = _sync(
|
|
db,
|
|
[
|
|
{"id": "row-keep", "item_code": "R-1", "item_name": "留下"},
|
|
{"item_code": "R-NEW", "item_name": "新增"},
|
|
],
|
|
)
|
|
|
|
self.assertEqual(saved[0].id, "row-keep")
|
|
self.assertNotEqual(saved[1].id, "row-drop")
|
|
rows = {row.id: row for row in db.query(DemoChild).all()}
|
|
self.assertEqual(rows["row-keep"].del_flag, 0)
|
|
self.assertEqual(rows["row-drop"].del_flag, 1)
|
|
self.assertEqual(rows[saved[1].id].del_flag, 0)
|
|
|
|
def test_id_from_another_parent_is_rejected(self):
|
|
db = _session()
|
|
db.add(_row(id="row-other", parent_id="parent-2"))
|
|
db.commit()
|
|
|
|
with self.assertRaises(ValueError):
|
|
_sync(db, [{"id": "row-other", "item_code": "R-1", "item_name": "占用"}])
|
|
other = db.query(DemoChild).filter_by(id="row-other").one()
|
|
self.assertEqual(other.parent_id, "parent-2")
|
|
self.assertEqual(other.del_flag, 0)
|
|
|
|
def test_readded_business_key_restores_deleted_row(self):
|
|
db = _session()
|
|
db.add(_row(id="row-old", item_code="R-1", del_flag=1))
|
|
db.commit()
|
|
now = datetime(2026, 10, 27, 10, 0, 0)
|
|
|
|
def build_row(row_id):
|
|
return DemoChild(
|
|
id=row_id,
|
|
parent_id="parent-1",
|
|
tenant_no="tenant-a",
|
|
create_time=now,
|
|
)
|
|
|
|
def apply_row(row, item, idx, _now):
|
|
row.item_code = item.get("item_code")
|
|
row.item_name = item.get("item_name")
|
|
row.order_num = idx
|
|
|
|
saved = sync_child_rows(
|
|
db,
|
|
DemoChild,
|
|
parent_attr="parent_id",
|
|
parent_id="parent-1",
|
|
tenant_no="tenant-a",
|
|
items=[{"item_code": "R-1", "item_name": "恢复"}],
|
|
user_id="user-1",
|
|
now=now,
|
|
build_row=build_row,
|
|
apply_fields=apply_row,
|
|
natural_key=lambda item: (item.get("item_code") or "").strip(),
|
|
row_natural_key=lambda row: (row.item_code or "").strip(),
|
|
reuse_deleted=True,
|
|
)
|
|
db.commit()
|
|
|
|
self.assertEqual(saved[0].id, "row-old")
|
|
self.assertEqual(saved[0].del_flag, 0)
|
|
self.assertEqual(db.query(DemoChild).count(), 1)
|
|
|
|
def test_removing_live_row_clears_older_tombstone(self):
|
|
db = _session()
|
|
db.add(_row(id="row-live", item_code="R-1"))
|
|
db.add(_row(id="row-tomb", item_code="R-1", del_flag=1))
|
|
db.commit()
|
|
now = datetime(2026, 10, 27, 10, 0, 0)
|
|
|
|
sync_child_rows(
|
|
db,
|
|
DemoChild,
|
|
parent_attr="parent_id",
|
|
parent_id="parent-1",
|
|
tenant_no="tenant-a",
|
|
items=[],
|
|
user_id="user-1",
|
|
now=now,
|
|
build_row=lambda row_id: DemoChild(
|
|
id=row_id, parent_id="parent-1", tenant_no="tenant-a", create_time=now
|
|
),
|
|
apply_fields=lambda row, item, idx, _now: None,
|
|
natural_key=lambda item: (item.get("item_code") or "").strip(),
|
|
row_natural_key=lambda row: (row.item_code or "").strip(),
|
|
reuse_deleted=True,
|
|
)
|
|
db.commit()
|
|
|
|
self.assertIsNone(db.query(DemoChild).filter_by(id="row-tomb").first())
|
|
live = db.query(DemoChild).filter_by(id="row-live").one()
|
|
self.assertEqual(live.del_flag, 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|