feat(pl): Save certificate archives and contract bindings atomically after validation.

This commit is contained in:
wuzilong committed 2026-11-01 21:22:26 +08:00
1 parent 0e2f00f777
commit 71116ed69e
6 files changed
+403 -10

No files matched your search

+4 -3
View File
@@ -19,6 +19,7 @@ from app.schemas.pl.quota_scope import ContractCertBindingsBody
from app.schemas.pl.legal_use_document import LegalDocumentsBody, StopLegalDocumentsBody
from app.schemas.pl.quota_planning import PlanQuotaConfiguration, PlanQuotaPreview
from app.services import pl_quota_cert_service as certs
from app.services import pl_quota_cert_bindings as cert_bindings
from app.services import pl_quota_contract_service as scopes
from app.services import pl_quota_contract_query as contract_query
from app.services import pl_legal_use_document as documents
@@ -161,13 +162,13 @@ def recognize(cert_type: CertType, body: RecognizeBody,
@oper_log('证件档案', business_type=1)
def create_cert(cert_type: CertType, body: dict = Body(...),
db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)):
return _write(db, lambda: certs.save_cert(db, user, cert_type, body))
return _write(db, lambda: cert_bindings.save_cert(db, user, cert_type, body))
@router.get('/{cert_type}/{cert_id}')
def get_cert(cert_type: CertType, cert_id: str,
db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)):
return _wire(certs.get_cert(db, user, cert_type, cert_id))
return _wire(cert_bindings.get_cert(db, user, cert_type, cert_id))
@router.get('/{cert_type}/{cert_id}/contracts')
@@ -180,7 +181,7 @@ def certificate_contracts(cert_type: CertType, cert_id: str, db: Session = Depen
@oper_log('证件档案', business_type=2, path_param='cert_id')
def update_cert(cert_type: CertType, cert_id: str, body: dict = Body(...),
db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)):
return _write(db, lambda: certs.save_cert(db, user, cert_type, body, cert_id))
return _write(db, lambda: cert_bindings.save_cert(db, user, cert_type, body, cert_id))
@router.delete('/{cert_type}/{cert_id}', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))])
@@ -0,0 +1,145 @@
"""证面、购买证引用与多合同范围先整批校验,再在调用方事务内差量写入。"""
from typing import List, Literal, Optional
from uuid import uuid4
from fastapi import HTTPException
from pydantic import BaseModel, Field
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope
from app.models.crm_models import CrmContract
from app.models.pl_quota_models import PlContractCertLink, PlContractCertProduct, PlTransportCertPurchaseRef
from app.schemas.pl.quota_cert import InputBody
from app.schemas.pl.quota_scope import ScopeProductBody
from app.services import pl_quota_cert_service as certs
from app.services import pl_quota_contract_service as scopes
class CertificateContractBinding(InputBody):
id: Optional[str] = Field(None, max_length=64)
contract_id: str = Field(min_length=1, max_length=64)
link_status: Optional[Literal['active', 'inactive']] = None
products: List[ScopeProductBody]
remark: Optional[str] = Field(None, max_length=1000)
order_num: Optional[int] = None
class CertificateBindings(InputBody):
contract_bindings: Optional[List[CertificateContractBinding]] = None
def _target_links(db, user, cert_type, cert_id, current=False):
query = db.query(PlContractCertLink).filter_by(tenant_no=user.tenant_no,
cert_type=cert_type, cert_id=cert_id, del_flag=0)
if current:
query = query.with_for_update().populate_existing()
return query.all()
def _visible_contract_ids(db, user):
query = db.query(CrmContract.id).filter_by(tenant_no=user.tenant_no, del_flag=0)
return apply_crm_owner_scope(query, CrmContract, user.to_user_info(), db, user.tenant_no)
def get_cert(db, user, cert_type, cert_id):
result = certs.get_cert(db, user, cert_type, cert_id)
links = db.query(PlContractCertLink).filter(PlContractCertLink.tenant_no == user.tenant_no,
PlContractCertLink.cert_type == cert_type, PlContractCertLink.cert_id == cert_id,
PlContractCertLink.del_flag == 0,
PlContractCertLink.contract_id.in_(_visible_contract_ids(db, user))).order_by(
PlContractCertLink.create_time.desc(), PlContractCertLink.id).all()
products = db.query(PlContractCertProduct).filter(
PlContractCertProduct.tenant_no == user.tenant_no, PlContractCertProduct.del_flag == 0,
PlContractCertProduct.contract_cert_link_id.in_([r.id for r in links])).order_by(
PlContractCertProduct.create_time.desc(), PlContractCertProduct.id).all() if links else []
total_links = db.query(PlContractCertLink.id).filter_by(tenant_no=user.tenant_no,
cert_type=cert_type, cert_id=cert_id, del_flag=0).count()
result['bindingsEditable'] = total_links == len(links)
result['contractBindings'] = [{
'id': link.id, 'contractId': link.contract_id, 'linkStatus': link.link_status,
'remark': link.remark, 'orderNum': link.order_num,
'products': [{'id': p.id, 'contractProductId': p.contract_product_id,
'scopeStatus': p.scope_status, 'remark': p.remark, 'orderNum': p.order_num}
for p in products if p.contract_cert_link_id == link.id],
} for link in links]
return result
def _reference_ids(db, user, cert_id, submitted, current=False):
query = db.query(PlTransportCertPurchaseRef).filter_by(tenant_no=user.tenant_no,
transport_cert_id=cert_id)
if current:
query = query.with_for_update().populate_existing()
restored = {r['reference_no']: r for r in (submitted or [])}
ids = {r.purchase_cert_id for r in query.all() if r.purchase_cert_id and (
not r.del_flag or (r.reference_no in restored and 'purchase_cert_id' not in restored[r.reference_no]))}
ids.update(r['purchase_cert_id'] for r in (submitted or []) if r.get('purchase_cert_id'))
return ids
def save_cert(db, user, cert_type, data, cert_id=None):
payload = data.model_dump(exclude_unset=True) if isinstance(data, BaseModel) else dict(data)
present = 'contractBindings' in payload or 'contract_bindings' in payload
if not present:
result = certs.save_cert(db, user, cert_type, payload, cert_id)
return get_cert(db, user, cert_type, result['id'])
bindings_payload = {key: payload.pop(key) for key in ('contractBindings', 'contract_bindings') if key in payload}
body = CertificateBindings(**bindings_payload)
if body.contract_bindings is None:
raise HTTPException(400, '合同集合不可为null;清空请提交[]')
# 证面独立严格解析,未识别字段不会因拆分而被丢弃。
certs._model(cert_type)
face = certs.SCHEMAS[cert_type][1 if cert_id else 0](**payload)
values = body.model_dump(exclude_unset=True)['contract_bindings']
ids = [r['contract_id'] for r in values]
if len(ids) != len(set(ids)):
raise HTTPException(400, '合同重复提交')
effective_id = cert_id or str(uuid4())
target = (cert_type, effective_id)
initial = _target_links(db, user, *target)
contract_ids = set(ids) | {r.contract_id for r in initial}
# 无权合同即拒绝整个集合替换;省略集合仍允许修改证面。
for cid in sorted(contract_ids):
scopes._contract(db, user, cid)
keys = {target}
for cid in sorted(contract_ids):
keys.update((r.cert_type, r.cert_id) for r in scopes._links(db, user, cid) if r.del_flag == 0)
refs = face.model_dump(exclude_unset=True).get('purchase_refs')
if cert_type == 'transport':
keys.update(('purchase', pid) for pid in _reference_ids(db, user, effective_id, refs))
locked = {key: certs._get_row(db, user, *key, lock=True) for key in sorted(keys)
if cert_id or key != target}
# 新证仅构建临时对象,不 add/flush;全部准备成功后才能入会话。
if not cert_id:
locked[target] = certs.MODELS[cert_type](id=effective_id, status=face.status or 'draft')
contracts = {cid: scopes._contract(db, user, cid, lock=True) for cid in sorted(contract_ids)}
current = _target_links(db, user, *target, current=True)
if any(r.contract_id not in contract_ids for r in current):
raise HTTPException(409, '证件合同范围已变更,请重试')
if cert_type == 'transport' and any(('purchase', pid) not in keys for pid in
_reference_ids(db, user, effective_id, refs, current=True)):
raise HTTPException(409, '证面引用已变更,请重试')
prepared_face = certs.prepare_cert(db, user, cert_type, face, cert_id,
locked=locked, effective_id=effective_id)
# 用最终证面状态校验作废证不可扩大,不修改持久对象。
effective_certs = dict(locked)
effective_certs[target] = certs.MODELS[cert_type](id=effective_id,
status=prepared_face['values'].get('status', locked[target].status))
requested = {item['contract_id']: item for item in values}
prepared = []
for cid in sorted(contract_ids):
existing = scopes._links(db, user, cid, lock=True)
if any((r.cert_type, r.cert_id) not in keys for r in existing if r.del_flag == 0):
raise HTTPException(409, '合同用证范围已变更,请重试')
entries = [{'id': r.id, 'cert_type': r.cert_type, 'cert_id': r.cert_id}
for r in existing if r.del_flag == 0 and (r.cert_type, r.cert_id) != target]
if cid in requested:
item = {k: v for k, v in requested[cid].items() if k != 'contract_id'}
item.update(cert_type=cert_type, cert_id=effective_id)
entries.append(item)
normalized = scopes.ContractCertBindingsBody(bindings=entries).model_dump(exclude_unset=True)['bindings']
prepared.append(scopes.prepare_contract_bindings(db, user, contracts[cid], normalized,
effective_certs, keys))
row = certs.apply_cert(db, user, prepared_face)
for scope in prepared:
scopes.apply_contract_bindings(db, user, scope)
db.flush()
return get_cert(db, user, cert_type, row.id)
+22 -5
View File
@@ -141,7 +141,7 @@ def _validate_refs(db, user, cert_id, submitted, historical, purchases, current=
return pending, removed
def save_cert(db, user, cert_type, data, cert_id=None):
def prepare_cert(db, user, cert_type, data, cert_id=None, *, locked=None, effective_id=None):
model = _model(cert_type)
payload = data.model_dump(exclude_unset=True) if isinstance(data, BaseModel) else data
body = SCHEMAS[cert_type][1 if cert_id else 0](**payload)
@@ -158,8 +158,12 @@ def save_cert(db, user, cert_type, data, cert_id=None):
not r.del_flag or (r.reference_no in restored and 'purchase_cert_id' not in restored[r.reference_no]))}
purchase_ids.update(r['purchase_cert_id'] for r in (refs or []) if r.get('purchase_cert_id'))
# 与账本统一购买证→运输证顺序;当前引用若出现未锁证件,拒绝而不倒序补锁。
purchases = {pid: _get_row(db, user, 'purchase', pid, lock=True) for pid in sorted(purchase_ids)}
row = _get_row(db, user, cert_type, cert_id, lock=True) if cert_id else None
purchases = {pid: (locked.get(('purchase', pid)) if locked is not None else
_get_row(db, user, 'purchase', pid, lock=True)) for pid in sorted(purchase_ids)}
if any(row is None for row in purchases.values()):
raise HTTPException(409, '证面引用已变更,请重试')
row = (locked[(cert_type, cert_id)] if locked is not None else
_get_row(db, user, cert_type, cert_id, lock=True)) if cert_id else None
historical = bool(row and _has_history(db,user,cert_type,cert_id))
for key, value in values.items():
column = model.__table__.columns[key]
@@ -185,10 +189,18 @@ def save_cert(db, user, cert_type, data, cert_id=None):
duplicate = db.query(model).filter_by(tenant_no=user.tenant_no, cert_no=no).first()
if duplicate and duplicate.id != cert_id:
raise HTTPException(400, '证号已存在,删除后也不得重建额度池')
effective_id = cert_id or str(uuid4())
effective_id = cert_id or effective_id or str(uuid4())
pending, removed = _validate_refs(db,user,effective_id,refs,historical,purchases,current=bool(cert_id)) if refs_present else ([],[])
changed = {key for key, value in values.items() if row is None or getattr(row, key) != value}
refs_changed = bool(removed) or any(previous is None or previous.del_flag == 1 or any(getattr(previous, key) != value for key, value in item.items() if key != 'id') for previous, item in pending)
return dict(model=model, row=row, values=values, cert_id=cert_id, effective_id=effective_id,
pending=pending, removed=removed, changed=changed, refs_changed=refs_changed)
def apply_cert(db, user, prepared):
model, row, values, cert_id, effective_id, pending, removed, changed, refs_changed = (
prepared[key] for key in ('model', 'row', 'values', 'cert_id', 'effective_id',
'pending', 'removed', 'changed', 'refs_changed'))
# 整批校验通过后才开始修改。
if not row:
audit = _audit(user); audit['id'] = effective_id
@@ -215,7 +227,12 @@ def save_cert(db, user, cert_type, data, cert_id=None):
previous.del_flag = 1
previous.update_by = user.id
db.flush()
return get_cert(db,user,cert_type,row.id)
return row
def save_cert(db, user, cert_type, data, cert_id=None):
row = apply_cert(db, user, prepare_cert(db, user, cert_type, data, cert_id))
return get_cert(db, user, cert_type, row.id)
def delete_cert(db, user, cert_type, cert_id):
@@ -115,6 +115,14 @@ def save_contract_bindings(db, user, contract_id, data):
keys.update((item['cert_type'], item['cert_id']) for item in values)
certs = {key: _get_row(db, user, *key, lock=True) for key in sorted(keys)}
contract = _contract(db, user, contract_id, lock=True)
prepared = prepare_contract_bindings(db, user, contract, values, certs, keys)
apply_contract_bindings(db, user, prepared)
db.flush()
return get_contract_bindings(db, user, contract_id, current=True)
def prepare_contract_bindings(db, user, contract, values, certs, keys):
contract_id = contract.id
if contract.status not in ('approved', 'executing'):
raise HTTPException(400, '仅已审批或执行中的合同可配置用证范围')
if contract.contract_type not in ('sale', 'purchase', 'framework_sale', 'framework_purchase'):
@@ -156,6 +164,12 @@ def save_contract_bindings(db, user, contract_id, data):
removed = [r for r in existing if r.del_flag == 0 and (r.cert_type, r.cert_id) not in seen]
for row in removed:
_assert_unreferenced(db, user, link_id=row.id)
return contract, pending, removed
def apply_contract_bindings(db, user, prepared):
contract, pending, removed = prepared
contract_id = contract.id
# 所有主子身份与引用校验通过,才开始写入;关系以唯一业务组合差量复用。
for link, item, products in pending:
is_new = link is None
@@ -195,5 +209,3 @@ def save_contract_bindings(db, user, contract_id, data):
for product in _products(db, user, row.id):
if product.del_flag == 0:
product.del_flag = 1; product.update_by = user.id
db.flush()
return get_contract_bindings(db, user, contract_id, current=True)
+15
View File
@@ -133,3 +133,18 @@ def test_total_adjust_requires_provenance_and_replays_without_duplicate_log(api)
assert client.post(path, json={**body,'totalQty':'40'}).status_code == 409
assert client.post(path, json={**body,'operationId':'adjust-2','sourceNote':''}).status_code == 409
assert client.get(base + '/' + row['id']).json()['totalQty'] == '35.000000'
def test_cert_bindings_api_accepts_collection_and_roundtrips(api):
client, app, auth, permissions = api
app.dependency_overrides[auth] = lambda: {'role': 'super_admin', 'user_id': 'u', 'tenant_no': 't'}
path = '/api/transport/quota-certs/purchase'
saved = client.post(path, json={**payload(), 'contractBindings': []})
assert saved.status_code == 200
row = saved.json()
assert row['contractBindings'] == []
detail = path + '/' + row['id']
assert client.get(detail).json()['contractBindings'] == []
assert client.put(detail, json={'contractBindings': [], 'remark': 'edit'}).status_code == 200
assert client.put(detail, json={'contractBindings': [], 'unknownField': 'bad'}).status_code == 422
assert client.get(detail).json()['remark'] == 'edit'
@@ -0,0 +1,203 @@
"""证件建档与多合同范围原子保存,SQLite 验证。"""
import pytest
from fastapi import HTTPException
from test_pl_quota_contract_scope import case, binding
def save(c, data, cert_id='g1', cert_type='purchase'):
from app.services import pl_quota_cert_bindings as svc
return svc.save_cert(c.db, c.user, cert_type, data, cert_id)
def scopes():
return [{'contractId': 'c1', 'products': [{'contractProductId': 'p1'}]},
{'contractId': 'c2', 'products': [{'contractProductId': 'p3'}]}]
def test_roundtrip_omission_empty_and_other_cert_preserved(case):
c = case
c.svc.save_contract_bindings(c.db, c.user, 'c1', {'bindings': [binding('g2')]})
result = save(c, {'contractBindings': scopes()})
original = result['contractBindings']
assert len(original) == 2
payload = [{k: v for k, v in x.items() if k in ('id', 'contractId', 'linkStatus', 'products', 'remark', 'orderNum')} for x in original]
for item in payload:
item['products'] = [{k: v for k, v in p.items() if k in ('id', 'contractProductId', 'scopeStatus')} for p in item['products']]
assert save(c, {'contractBindings': payload})['contractBindings'] == original
assert save(c, {'remark': 'only face'})['contractBindings'] == original
assert save(c, {'contractBindings': []})['contractBindings'] == []
assert c.svc.get_contract_bindings(c.db, c.user, 'c1')[0]['certId'] == 'g2'
def test_second_invalid_contract_does_not_mutate_face_or_first(case):
c = case
data = scopes()
data[1]['products'][0]['contractProductId'] = 'p1'
with pytest.raises(HTTPException):
save(c, {'remark': 'must not write', 'contractBindings': data})
assert not c.db.new and not c.db.dirty
assert c.db.query(c.m.PlContractCertLink).count() == 0
assert c.db.query(c.m.PlPurchaseCert).filter_by(id='g1').one().remark is None
@pytest.mark.parametrize('change', ['duplicate', 'foreign_id', 'foreign_product', 'null'])
def test_bad_identity_rejected_before_writes(case, change):
data = scopes()
if change == 'duplicate': data.append(data[0])
if change == 'foreign_id': data[0]['id'] = 'foreign'
if change == 'foreign_product': data[0]['products'][0]['id'] = 'foreign'
if change == 'null': data = None
with pytest.raises(HTTPException):
save(case, {'contractBindings': data})
assert case.db.query(case.m.PlContractCertLink).count() == 0
def test_create_with_bindings_and_bad_create_has_no_pending_row(case):
c = case
body = dict(certNo='new', productNameOnCert='硫酸', totalQty='300', quantityUnit='吨',
validUseText='多次有效', buyerName='买方', sellerName='卖方', contractBindings=scopes())
broken = {**body, 'contractBindings': [{'contractId': 'missing', 'products': []}]}
with pytest.raises(HTTPException): save(c, broken, None)
assert not c.db.new and not c.db.dirty
result = save(c, body, None)
assert len(result['contractBindings']) == 2
assert result['id'] not in ('g1', 'g2')
def test_hidden_contract_cannot_be_cleared_but_face_can_change(case, monkeypatch):
from app.services import pl_quota_cert_bindings as svc
from app.models.crm_models import CrmContract
c = case
save(c, {'contractBindings': scopes()})
original = c.svc.ensure_crm_owner_row_in_scope
def deny_second(db, row, *args):
if row.id == 'c2': raise ValueError('hidden')
return original(db, row, *args)
monkeypatch.setattr(c.svc, 'ensure_crm_owner_row_in_scope', deny_second)
monkeypatch.setattr(svc, '_visible_contract_ids', lambda db, user: db.query(CrmContract.id).filter(CrmContract.id == 'c1'))
with pytest.raises(HTTPException) as error:
save(c, {'contractBindings': []})
assert error.value.status_code == 403
result = save(c, {'remark': 'allowed'})
assert result['remark'] == 'allowed'
assert [x['contractId'] for x in result['contractBindings']] == ['c1']
assert c.db.query(c.m.PlContractCertLink).filter_by(del_flag=0).count() == 2
def test_transport_locks_all_certificates_before_contracts_and_current_reads(case, monkeypatch):
from app.services import pl_quota_cert_service as certs
from sqlalchemy import event
from app.models.crm_models import CrmContract, CrmContractProduct
c = case
c.svc.save_contract_bindings(c.db, c.user, 'c1', {'bindings': [binding('g2')]})
certs.save_cert(c.db, c.user, 'transport', dict(certNo='T1', productNameOnCert='硫酸',
totalQty='300', quantityUnit='吨', validUseText='多次有效', shipperName='发货人'), None)
target = c.db.query(c.m.PlTransportCert).one().id
events = []
original = certs._get_row
contract = c.svc._contract
def cert_lock(db, user, kind, rid, lock=False):
if lock: events.append(('cert', kind, rid))
return original(db, user, kind, rid, lock)
def contract_lock(db, user, cid, lock=False):
if lock: events.append(('contract', cid))
return contract(db, user, cid, lock)
monkeypatch.setattr(certs, '_get_row', cert_lock)
monkeypatch.setattr(c.svc, '_contract', contract_lock)
reads = []
def capture(state):
if state.is_select and state.statement._for_update_arg is not None:
reads.append((state.bind_mapper.class_, state.load_options._populate_existing))
event.listen(c.db, 'do_orm_execute', capture)
try:
result = save(c, {'contractBindings': scopes(), 'purchaseRefs': [
{'referenceType': 'purchase_permit', 'referenceNo': 'g1', 'purchaseCertId': 'g1'}]}, target, 'transport')
finally:
event.remove(c.db, 'do_orm_execute', capture)
assert events == [('cert', 'purchase', 'g1'), ('cert', 'purchase', 'g2'),
('cert', 'transport', target), ('contract', 'c1'), ('contract', 'c2')]
for model in (CrmContract, CrmContractProduct, c.m.PlContractCertLink,
c.m.PlTransportCertPurchaseRef):
assert any(m == model and refresh for m, refresh in reads)
assert len(result['contractBindings']) == 2 and len(result['purchaseRefs']) == 1
def test_new_key_after_lock_retries_without_face_mutation(case, monkeypatch):
c = case
original = c.svc._contract
def changed(db, user, cid, lock=False):
row = original(db, user, cid, lock)
if lock and cid == 'c1':
db.add(c.m.PlContractCertLink(id='concurrent', tenant_no='t', contract_id='c1',
cert_type='purchase', cert_id='g2', contract_no_snapshot='c1', scope_revision=1, link_status='active'))
db.flush()
return row
monkeypatch.setattr(c.svc, '_contract', changed)
with pytest.raises(HTTPException) as error:
save(c, {'remark': 'must not write', 'contractBindings': scopes()})
assert error.value.status_code == 409
assert c.db.query(c.m.PlPurchaseCert).filter_by(id='g1').one().remark is None
def test_referenced_scope_cannot_be_removed_or_disabled(case):
c = case
result = save(c, {'contractBindings': scopes()})
link = next(x for x in result['contractBindings'] if x['contractId'] == 'c1')
c.db.add(c.m.PlPlanCertUsageItem(id='item', usage_id='usage', tenant_no='t',
contract_cert_link_id=link['id'], contract_cert_product_id=link['products'][0]['id'],
contract_scope_revision=1, contract_id='c1', contract_product_id='p1', plan_product_id='pp',
source_stop_type='delivery', source_stop_id='stop', source_stop_product_id='sp',
allocation_seq=1, source_unit='吨', planned_source_qty=10))
c.db.flush()
for bindings in ([], [{**scopes()[0], 'linkStatus': 'inactive'}, scopes()[1]]):
with pytest.raises(HTTPException) as error:
save(c, {'remark': 'do not write', 'contractBindings': bindings})
assert error.value.status_code == 409
assert c.db.query(c.m.PlPurchaseCert).filter_by(id='g1').one().remark is None
def test_void_new_or_existing_certificate_cannot_expand(case):
c = case
with pytest.raises(HTTPException):
save(c, {'status': 'void', 'contractBindings': scopes()})
assert c.db.query(c.m.PlPurchaseCert).filter_by(id='g1').one().status == 'draft'
def test_get_reports_partial_binding_scope_for_editor(case, monkeypatch):
from app.services import pl_quota_cert_bindings as svc
from app.models.crm_models import CrmContract
c = case
result = save(c, {'contractBindings': scopes()})
assert result['bindingsEditable'] is True
monkeypatch.setattr(svc, '_visible_contract_ids', lambda db, user: db.query(CrmContract.id).filter(CrmContract.id == 'c1'))
result = svc.get_cert(c.db, c.user, 'purchase', 'g1')
assert result['bindingsEditable'] is False
assert len(result['contractBindings']) == 1
def test_foreign_tenant_contract_rejected_and_no_changes(case):
from app.models.crm_models import CrmContract
c = case
c.db.query(CrmContract).filter_by(id='c2').one().tenant_no = 'other'
c.db.commit()
with pytest.raises(HTTPException) as error:
save(c, {'remark': 'must not write', 'contractBindings': scopes()})
assert error.value.status_code == 404
assert c.db.query(c.m.PlContractCertLink).count() == 0
assert not c.db.dirty and not c.db.new
def test_transport_refs_and_face_not_written_when_second_contract_invalid(case):
from app.services import pl_quota_cert_service as certs
c = case
result = certs.save_cert(c.db, c.user, 'transport', dict(certNo='T1', productNameOnCert='硫酸',
totalQty='300', quantityUnit='吨', validUseText='多次有效', shipperName='发货人'))
data = scopes()
data[1]['products'] = [{'contractProductId': 'p1'}]
with pytest.raises(HTTPException):
save(c, {'remark': 'must not write', 'contractBindings': data, 'purchaseRefs': [
{'referenceType': 'purchase_permit', 'referenceNo': 'g1', 'purchaseCertId': 'g1'}]}, result['id'], 'transport')
assert c.db.query(c.m.PlTransportCert).one().remark is None
assert c.db.query(c.m.PlTransportCertPurchaseRef).count() == 0
assert c.db.query(c.m.PlContractCertLink).count() == 0
assert not c.db.dirty and not c.db.new