执行中的运输计划不再随意改单;库存流水记到装卸产品明细;手工派车日历区分酸碱并记录数量。同时收紧合同附件与子表的差量保存,并清理已废弃的 bak 备份目录。 Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
5.9 KiB
Python
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 == ''
|