Files
shanxia2.0/backend/tests/test_crm_product_metadata_diff.py
T
wuzilongandCursor bde19c0644 feat: 锁定执行中计划并按装卸明细差量归档库存,补齐派车日历数量与酸碱类型。
执行中的运输计划不再随意改单;库存流水记到装卸产品明细;手工派车日历区分酸碱并记录数量。同时收紧合同附件与子表的差量保存,并清理已废弃的 bak 备份目录。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 07:59:53 +08:00

155 lines
5.9 KiB
Python

"""Product metadata saves use stable identities; SQLite only."""
from datetime import datetime
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from app.crud.crm import product as crud
from app.models.crm_models import CrmProduct, CrmProductAlias, CrmProductBrand
from app.models.pl_models import PlInventory, PlPlan, PlPlanProduct
@pytest.fixture
def db():
engine = create_engine('sqlite://')
for model in (CrmProduct, CrmProductAlias, CrmProductBrand, PlInventory, PlPlan, PlPlanProduct):
model.__table__.create(engine)
with Session(engine) as session:
yield session
def seed(db, kind, identity='old', name='Original', **kwargs):
model = CrmProductBrand if kind == 'brands' else CrmProductAlias
attr = 'brand_name' if kind == 'brands' else 'alias_name'
values = dict(id=identity, tenant_no='t', product_id='p', del_flag=0,
create_by='creator', create_time=datetime(2020, 1, 1), **{attr: name})
values.update(kwargs)
row = model(**values)
db.add(row)
db.flush()
return row
def save(db, kind, values):
getattr(crud, '_replace_product_' + kind)(db, product_id='p', tenant_no='t',
user_id='editor', **{kind: values})
db.flush()
def item(kind, name='Original', **kwargs):
return {'brandName' if kind == 'brands' else 'aliasName': name, **kwargs}
@pytest.mark.parametrize('kind', ['brands', 'aliases'])
def test_legacy_unique_name_reuses_identity(db, kind):
old = seed(db, kind)
save(db, kind, [item(kind)])
assert old.del_flag == 0
assert db.query(type(old)).count() == 1
assert old.create_by == 'creator' and old.create_time == datetime(2020, 1, 1)
@pytest.mark.parametrize('kind', ['brands', 'aliases'])
@pytest.mark.parametrize('bad', ['missing', 'foreign_tenant', 'foreign_product', 'deleted', 'duplicate'])
def test_bad_ids_rejected_before_mutation(db, kind, bad):
old = seed(db, kind)
foreign = {'foreign_tenant': {'tenant_no': 'other'}, 'foreign_product': {'product_id': 'other'},
'deleted': {'del_flag': 1}}
if bad in foreign:
seed(db, kind, 'bad', **foreign[bad])
values = [item(kind, 'Changed', id='old'), item(kind, 'Other', id='old' if bad == 'duplicate' else 'bad')]
with pytest.raises(ValueError):
save(db, kind, values)
assert old.del_flag == 0 and not db.dirty and not db.new
@pytest.mark.parametrize('kind', ['brands', 'aliases'])
def test_ambiguous_legacy_name_is_rejected(db, kind):
old = seed(db, kind)
seed(db, kind, 'second')
with pytest.raises(ValueError):
save(db, kind, [item(kind)])
assert old.del_flag == 0 and not db.dirty
@pytest.mark.parametrize('kind', ['brands', 'aliases'])
def test_edit_add_remove_and_omission(db, kind):
old = seed(db, kind)
removed = seed(db, kind, 'removed', name='Remove')
save(db, kind, None)
assert old.del_flag == removed.del_flag == 0
save(db, kind, [item(kind, 'Changed', id='old'), item(kind, 'New')])
assert old.del_flag == 0 and removed.del_flag == 1
rows = db.query(type(old)).filter_by(del_flag=0).all()
assert len(rows) == 2 and len({row.id for row in rows}) == 2
save(db, kind, [])
assert db.query(type(old)).filter_by(del_flag=0).count() == 0
def test_validate_both_collections_before_header_or_other_collection_mutation(db):
parent = CrmProduct(id='p', tenant_no='t', product_no='P', product_name='Original', del_flag=0)
db.add(parent)
brand = seed(db, 'brands')
seed(db, 'aliases', 'alias')
with pytest.raises(ValueError):
crud.update_product(db, 'p', product_name='Changed', brands=[],
aliases=[item('aliases', id='foreign')])
assert parent.product_name == 'Original' and brand.del_flag == 0
assert not db.dirty and not db.new
def test_existing_brand_omitted_status_preserves_it(db):
old = seed(db, 'brands', status='off')
save(db, 'brands', [item('brands', 'Renamed', id='old')])
assert old.status == 'off'
def test_brand_removal_guard_runs_before_any_mutation(db):
from fastapi import HTTPException
old = seed(db, 'brands')
guarded = seed(db, 'brands', 'guarded', name='Guarded')
db.add(PlInventory(id='inventory', tenant_no='t', product_id='p', brand_id='guarded',
product_no='P', product_name='Product', stock_qty=1, del_flag=0))
db.flush()
with pytest.raises(HTTPException):
save(db, 'brands', [])
assert old.del_flag == guarded.del_flag == 0 and not db.dirty
@pytest.mark.parametrize('kind', ['brands', 'aliases'])
def test_create_rejects_source_ids_before_adding_parent(db, kind):
with pytest.raises(ValueError):
crud.create_product(db, tenant_no='t', product_no='P', product_name='New', barcode='123',
**{kind: [item(kind, id='source')]})
assert db.query(CrmProduct).count() == 0 and not db.new
@pytest.mark.parametrize('kind', ['brands', 'aliases'])
def test_explicit_same_name_and_new_row_have_distinct_identity(db, kind):
old = seed(db, kind)
save(db, kind, [item(kind, id='old'), item(kind)])
assert old.del_flag == 0
assert db.query(type(old)).filter_by(del_flag=0).count() == 2
@pytest.mark.parametrize('kind', ['brands', 'aliases'])
def test_repeated_anonymous_names_rejected_before_mutation(db, kind):
old = seed(db, kind)
with pytest.raises(ValueError):
save(db, kind, [item(kind), item(kind)])
assert old.del_flag == 0 and not db.dirty
def test_omitted_product_images_stay_and_empty_string_clears(db):
parent = CrmProduct(
id='p', tenant_no='t', product_no='P', product_name='甲醇',
images='https://a/1.jpg,https://a/2.jpg', del_flag=0,
)
db.add(parent)
db.commit()
crud.update_product(db, 'p', remark='只改备注')
assert parent.images == 'https://a/1.jpg,https://a/2.jpg'
crud.update_product(db, 'p', images='')
assert parent.images == ''