Files
shanxia2.0/backend/tests/test_pl_quota_cert_archive.py
T
wuzilongandCursor 8e9536332a feat: 保存运输证件、报销付款和开发流程的当前实现。
补齐证书齐备、证件折算、移动用证和合同绑定,并带上报销批量发放、付款备注以及个人开发流程升级,便于继续讨论已安排计划的调整规则。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 21:42:33 +08:00

478 lines
23 KiB
Python

"""隔离 SQLite 档案回归,不连接配置数据库。"""
import importlib.util
from decimal import Decimal
from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from pydantic import ValidationError
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
def test_archive_module_exists():
assert importlib.util.find_spec('app.services.pl_quota_cert_service') is not None
@pytest.fixture
def ctx():
from app.models import pl_quota_models as m
from app.services import pl_quota_cert_service as svc
engine = create_engine('sqlite://')
for table in m.Base.metadata.sorted_tables:
if table.name.startswith(('pl_purchase_cert', 'pl_transport_cert', 'pl_contract_cert', 'pl_plan_cert', 'pl_cert_')):
table.create(engine)
db = Session(engine)
user = SimpleNamespace(id='u', dept_id='d', tenant_no='t', to_user_info=lambda: {'role': 'super_admin', 'user_id': 'u', 'tenant_no': 't'})
yield db, user, svc, m
db.close()
engine.dispose()
def purchase(**kw):
return dict(certNo='G1', productNameOnCert='硫酸', totalQty='100.123456', quantityUnit='吨', validUseText='多次有效', buyerName='购方', sellerName='销方', **kw)
def transport(**kw):
data = purchase()
data.pop('buyerName'); data.pop('sellerName')
data.update(certNo='Y1', shipperName='货主', vehicleNumbersJson=['皖A1', '皖A2'])
data.update(kw)
return data
def test_manual_optional_image_complete_fields(ctx):
db,u,s,m = ctx
data = purchase(buyerAddress='地址', buyerPhone='电话', buyerLegalRepresentative='甲', sellerAddress='乙址', sellerPhone='乙电话', sellerLegalRepresentative='乙', purchasePurpose='用途', issueDate='2026-01-01', validFrom='2026-01-01', validTo='2027-01-01')
row = s.save_cert(db,u,'purchase',data)
assert row['buyerAddress'] == '地址' and row['attachmentUrls'] is None
assert row['totalQty'] == Decimal('100.123456')
assert row['status'] == 'draft'
assert db.query(m.PlCertQtyLog).count() == 0
db.rollback()
assert db.query(m.PlPurchaseCert).count() == 0
def test_opening_confirmation_remains_visible_after_reverification_required(ctx):
db, user, svc, models = ctx
row = svc.save_cert(db, user, 'purchase', purchase())
assert row['openingConfirmed'] is False
row = svc.confirm_cert(db, user, 'purchase', row['id'], {
'operationId': 'op-ui', 'usedQty': '0', 'singleUseConsumed': False, 'reason': '全新证'})
changed = svc.save_cert(db, user, 'purchase', {'buyerAddress': '更正地址'}, row['id'])
assert changed['verifiedTime'] is None
assert changed['openingConfirmed'] is True
@pytest.mark.parametrize('qty', ['-1', 'NaN', 'Infinity', '1.0000001', '1000000000000'])
def test_decimal_validation(qty):
from app.schemas.pl.quota_cert import PurchaseCertCreate
data=purchase(); data['totalQty']=qty
with pytest.raises(ValidationError): PurchaseCertCreate(**data)
def test_reference_delta_omitted_empty_and_invalid_prewrite(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'外部1'}]))
ref=row['purchaseRefs'][0]
row=s.save_cert(db,u,'transport',{'remark':'补图'},row['id'])
assert row['purchaseRefs'][0]['id']==ref['id']
row=s.save_cert(db,u,'transport',{'purchaseRefs':[{'id':ref['id'],'referenceType':'unknown','referenceNo':'外部1','remark':'留痕'}]},row['id'])
assert row['purchaseRefs'][0]['id']==ref['id']
with pytest.raises(HTTPException): s.save_cert(db,u,'transport',{'remark':'不可写','purchaseRefs':[{'id':'foreign','referenceType':'unknown','referenceNo':'X'}]},row['id'])
assert s.get_cert(db,u,'transport',row['id'])['remark']=='补图'
row=s.save_cert(db,u,'transport',{'purchaseRefs':[]},row['id'])
assert row['purchaseRefs']==[]
assert db.query(m.PlTransportCertPurchaseRef).one().del_flag==1
def test_confirmation_and_history_protection(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
with pytest.raises((HTTPException,ValidationError)): s.confirm_cert(db,u,'purchase',row['id'],{'usedQty':'0','singleUseConsumed':True,'reason':'核对','operationId':'op'})
row=s.confirm_cert(db,u,'purchase',row['id'],{'usedQty':'0','singleUseConsumed':True,'reason':'核对','evidenceUrls':'https://minio/evidence','operationId':'op'})
assert row['openingSingleUseConsumed']==1 and row['status']=='active'
assert db.query(m.PlCertQtyLog).one().event_type=='opening'
for data in ({'quantityUnit':'千克'},{'certNo':'new'},{'totalQty':'90'}):
with pytest.raises(HTTPException): s.save_cert(db,u,'purchase',data,row['id'])
with pytest.raises(HTTPException): s.delete_cert(db,u,'purchase',row['id'])
def test_tenant_and_scope(ctx,monkeypatch):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
other=SimpleNamespace(**{**u.__dict__,'tenant_no':'other'})
with pytest.raises(HTTPException): s.get_cert(db,other,'purchase',row['id'])
def deny(*args): raise HTTPException(403,'数据范围')
monkeypatch.setattr(s,'assert_pl_row_in_scope',deny)
with pytest.raises(HTTPException): s.save_cert(db,u,'purchase',{'remark':'bad'},row['id'])
def opening(**kw):
data=dict(usedQty='0',singleUseConsumed=False,reason='期初核对',evidenceUrls='https://minio/evidence',operationId='opening')
data.update(kw)
return data
def test_used_transport_vehicle_identity_cannot_change(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'transport',transport())
s.confirm_cert(db,u,'transport',row['id'],opening())
with pytest.raises(HTTPException, match='只能追加新车牌'):
s.save_cert(db,u,'transport',{'vehicleNumbersJson':['皖B1']},row['id'])
kept=s.get_cert(db,u,'transport',row['id'])
assert kept['vehicleNumbersJson']==['皖A1','皖A2']
def test_ton_label_can_be_stored_as_unit_code_without_losing_verification(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
confirmed=s.confirm_cert(db,u,'purchase',row['id'],opening())
saved=s.save_cert(db,u,'purchase',{'quantityUnit':'吨'},row['id'])
assert saved['quantityUnit']=='dun'
assert saved['status']=='active'
assert saved['verifiedTime']==confirmed['verifiedTime']
def test_used_transport_can_append_vehicle_without_losing_verification(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'transport',transport())
confirmed=s.confirm_cert(db,u,'transport',row['id'],opening())
saved=s.save_cert(db,u,'transport',{'vehicleNumbersJson':['皖A2','皖A1','皖B1']},row['id'])
assert saved['vehicleNumbersJson']==['皖A2','皖A1','皖B1']
assert saved['status']=='active'
assert saved['verifiedTime']==confirmed['verifiedTime']
def test_opening_retry_preserves_balance_and_operation_identity(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
s.confirm_cert(db,u,'purchase',row['id'],opening())
db.query(m.PlPurchaseCert).one().used_qty=Decimal('5')
retry=s.confirm_cert(db,u,'purchase',row['id'],opening())
assert retry['usedQty']==Decimal('5')
assert db.query(m.PlCertQtyLog).count()==1
assert db.query(m.PlCertOperation).count()==1
with pytest.raises(HTTPException): s.confirm_cert(db,u,'purchase',row['id'],opening(usedQty='7'))
with pytest.raises(HTTPException): s.confirm_cert(db,u,'purchase',row['id'],opening(operationId='second'))
def test_refs_duplicate_cross_owner_and_used_removal(ctx):
db,u,s,m=ctx
first=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'A'}]))
data=transport(); data['certNo']='Y2'
second=s.save_cert(db,u,'transport',data)
assert first['referencesEditable'] is True
ref=first['purchaseRefs'][0]
child={k:ref[k] for k in ('id','referenceType','referenceNo')}
for cert_id,refs in ((first['id'],[child,child]),(second['id'],[child])):
with pytest.raises(HTTPException): s.save_cert(db,u,'transport',{'purchaseRefs':refs},cert_id)
s.confirm_cert(db,u,'transport',first['id'],opening())
assert s.get_cert(db,u,'transport',first['id'])['referencesEditable'] is False
with pytest.raises(HTTPException, match='联系管理员'): s.save_cert(db,u,'transport',{'purchaseRefs':[]},first['id'])
assert len(s.get_cert(db,u,'transport',first['id'])['purchaseRefs'])==1
def test_readonly_cache_and_tenant_cannot_be_written(ctx):
db,u,s,m=ctx
for field,value in [('tenantNo','other'),('usedQty','1'),('reservedQty','1'),('openingSingleUseConsumed',True)]:
with pytest.raises(ValidationError): s.save_cert(db,u,'purchase',{**purchase(),field:value})
def test_transport_all_fields_and_scope_filtered_list(ctx):
db,u,s,m=ctx
data=transport(shipperAddress='地址',shipperPhone='电话',shipperLegalRepresentative='甲',packagingText='槽罐',carrierName='运方',carrierAddressPhone='住址/电话',transportMode='汽车',routeText='路线',consigneeName='收方',applicantName='申请方')
row=s.save_cert(db,u,'transport',data)
for key,value in data.items():
expected = 'dun' if key=='quantityUnit' and value=='吨' else value
assert row[key] == (Decimal(expected) if key=='totalQty' else expected)
assert s.list_certs(db,u,'transport')[0]['id']==row['id']
def test_reverify_after_correction_keeps_opening_and_balances(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
s.confirm_cert(db,u,'purchase',row['id'],opening(usedQty='5'))
corrected=s.save_cert(db,u,'purchase',{'buyerAddress':'更正地址'},row['id'])
assert corrected['status']=='suspended' and corrected['verifiedBy'] is None
result=s.verify_cert(db,u,'purchase',row['id'],{'operationId':'verify-1','reason':'地址更正核对','evidenceUrls':'https://minio/evidence'})
assert result['status']=='active' and result['usedQty']==Decimal('5')
assert db.query(m.PlCertQtyLog).count()==1
assert db.query(m.PlCertOperation).count()==2
result=s.verify_cert(db,u,'purchase',row['id'],{'operationId':'verify-1','reason':'地址更正核对','evidenceUrls':'https://minio/evidence'})
assert db.query(m.PlCertOperation).count()==2
def test_actual_scope_denies_user_without_roles(ctx):
from app.models.models import SysRole, SysUserRole
db,u,s,m=ctx
for model in (SysRole, SysUserRole): model.__table__.create(db.get_bind())
row=s.save_cert(db,u,'purchase',purchase())
restricted=SimpleNamespace(id='restricted',dept_id='d',tenant_no='t',to_user_info=lambda:{'role':'admin','user_id':'restricted','tenant_no':'t'})
assert s.list_certs(db,restricted,'purchase') == []
with pytest.raises(HTTPException) as error: s.get_cert(db,restricted,'purchase',row['id'])
assert error.value.status_code==403
def test_sql_and_models_have_same_columns(ctx):
import re
from pathlib import Path
db,u,s,m=ctx
sql=(Path(__file__).resolve().parents[2]/'docs/sql/2026-11-01-pl-contract-cert-quota.sql').read_text()
tables=re.findall(r'CREATE TABLE `(\w+)` \((.*?)\) ENGINE=',sql,re.S)
assert len(tables)==9
for table,body in tables:
names=re.findall(r'^ `(\w+)` ',body,re.M)
assert list(m.Base.metadata.tables[table].columns.keys())==names
def test_no_change_save_preserves_verification(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
row=s.confirm_cert(db,u,'purchase',row['id'],opening())
saved=s.save_cert(db,u,'purchase',purchase(),row['id'])
assert saved['status']=='active'
assert saved['revision']==row['revision']
assert saved['verifiedTime']==row['verifiedTime']
def test_zero_opening_can_activate_without_image(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
result=s.confirm_cert(db,u,'purchase',row['id'],{'operationId':'no-image','usedQty':'0','singleUseConsumed':False,'reason':'核实全新证,尚未使用'})
assert result['status']=='active' and result['attachmentUrls'] is None
assert result['verifiedBy']=='u' and result['verifiedTime'] is not None
def test_historical_opening_accepts_written_source_without_image(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
result=s.confirm_cert(db,u,'purchase',row['id'],{'operationId':'written-source','usedQty':'5','singleUseConsumed':True,'reason':'历史核对','sourceNote':'2026年10月纸质台账第3页,经张三核实'})
assert result['usedQty']==Decimal('5') and result['status']=='active'
assert db.query(m.PlCertQtyLog).one().event_snapshot['sourceNote'].startswith('2026')
def test_unknown_use_text_is_preserved_and_requires_confirmation(ctx):
db,u,s,m=ctx
data=purchase(); data['validUseText']=' 按核发机关规定使用 '
row=s.save_cert(db,u,'purchase',data)
assert row['validUseText']==data['validUseText']
assert row['usagePolicy']=='needs_confirmation'
with pytest.raises(HTTPException) as error:
s.confirm_cert(db,u,'purchase',row['id'],opening())
assert '次数' in error.value.detail
assert s.get_cert(db,u,'purchase',row['id'])['status']=='draft'
assert db.query(m.PlCertQtyLog).count()==0
def test_known_use_policy_is_derived_and_readonly(ctx):
db,u,s,m=ctx
for number,text,policy in [('G1','一次有效','once'),('G2','多次有效','multiple')]:
data=purchase(); data.update(certNo=number,validUseText=text)
assert s.save_cert(db,u,'purchase',data)['usagePolicy']==policy
with pytest.raises(ValidationError):
s.save_cert(db,u,'purchase',{**purchase(),'usagePolicy':'once'})
def test_verify_refuses_unknown_use_policy(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
s.confirm_cert(db,u,'purchase',row['id'],opening())
# 模拟迁移导入的原文,不能被复核入口猜测放行。
stored=db.query(m.PlPurchaseCert).one()
stored.valid_use_text='另有规定'
stored.status='suspended'
with pytest.raises(HTTPException) as error:
s.verify_cert(db,u,'purchase',row['id'],{'operationId':'verify-unknown','reason':'复核'})
assert '次数' in error.value.detail
assert stored.status=='suspended'
def test_unlinked_reference_can_bind_purchase_archive_after_opening(ctx):
db,u,s,m=ctx
purchase_row=s.save_cert(db,u,'purchase',purchase())
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'G35260264950502'}]))
confirmed=s.confirm_cert(db,u,'transport',row['id'],opening())
ref=row['purchaseRefs'][0]
saved=s.save_cert(db,u,'transport',{'purchaseRefs':[{
'id':ref['id'],'referenceType':'purchase_permit','referenceNo':'G35260264950502','purchaseCertId':purchase_row['id'],
}]},row['id'])
assert saved['status']=='active'
assert saved['verifiedTime']==confirmed['verifiedTime']
assert saved['purchaseRefs'][0]['purchaseCertId']==purchase_row['id']
assert saved['purchaseRefs'][0]['referenceNo']=='G35260264950502'
linked=saved['purchaseRefs'][0]
with pytest.raises(HTTPException, match='不能改变证面引用身份'):
s.save_cert(db,u,'transport',{'purchaseRefs':[{'id':linked['id'],'referenceType':'unknown','referenceNo':purchase_row['certNo'],'purchaseCertId':purchase_row['id']}]},row['id'])
with pytest.raises(HTTPException, match='不能移除'):
s.save_cert(db,u,'transport',{'purchaseRefs':[]},row['id'])
with pytest.raises(HTTPException, match='不能改变证面引用身份'):
s.save_cert(db,u,'transport',{'purchaseRefs':[
{k:linked[k] for k in ('id','referenceType','referenceNo','purchaseCertId')},
{'referenceType':'unknown','referenceNo':'NEW'},
]},row['id'])
def test_reference_number_stays_independent_of_purchase_archive(ctx):
db,u,s,m=ctx
purchase_row=s.save_cert(db,u,'purchase',purchase())
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G35260264950502','purchaseCertId':purchase_row['id']}]))
reference=row['purchaseRefs'][0]
assert reference['referenceNo']=='G35260264950502'
assert reference['purchaseCertId']==purchase_row['id']
saved=s.save_cert(db,u,'transport',{'purchaseRefs':[{'id':reference['id'],'referenceType':'purchase_permit','referenceNo':'G34260362240160'}]},row['id'])
assert saved['purchaseRefs'][0]['referenceNo']=='G34260362240160'
assert saved['purchaseRefs'][0]['purchaseCertId']==purchase_row['id']
def test_purchase_number_change_cannot_break_active_transport_reference(ctx):
db,u,s,m=ctx
purchase_row=s.save_cert(db,u,'purchase',purchase())
transport_row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G1','purchaseCertId':purchase_row['id']}]))
with pytest.raises(HTTPException):
s.save_cert(db,u,'purchase',{'certNo':'G3','remark':'不应写入'},purchase_row['id'])
unchanged=s.get_cert(db,u,'purchase',purchase_row['id'])
assert unchanged['certNo']=='G1' and unchanged['remark'] is None
assert s.get_cert(db,u,'transport',transport_row['id'])['purchaseRefs'][0]['referenceNo']=='G1'
def test_draft_reference_can_restore_same_business_key_without_deleted_id(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'A','remark':'原备注'}]))
ref=db.query(m.PlTransportCertPurchaseRef).one()
original=(ref.id,ref.create_by,ref.create_time)
s.save_cert(db,u,'transport',{'purchaseRefs':[]},row['id'])
assert ref.del_flag==1
with pytest.raises(HTTPException):
s.save_cert(db,u,'transport',{'purchaseRefs':[{'id':ref.id,'referenceType':'unknown','referenceNo':'A'}]},row['id'])
restored=s.save_cert(db,u,'transport',{'purchaseRefs':[{'referenceType':'unknown','referenceNo':'A'}]},row['id'])
assert restored['purchaseRefs'][0]['id']==original[0]
assert (ref.id,ref.create_by,ref.create_time)==original
assert ref.remark=='原备注' and ref.del_flag==0
assert db.query(m.PlTransportCertPurchaseRef).count()==1
def test_historical_transport_cannot_restore_deleted_reference(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'A'}]))
s.save_cert(db,u,'transport',{'purchaseRefs':[]},row['id'])
s.confirm_cert(db,u,'transport',row['id'],opening())
with pytest.raises(HTTPException):
s.save_cert(db,u,'transport',{'purchaseRefs':[{'referenceType':'unknown','referenceNo':'A'}]},row['id'])
assert db.query(m.PlTransportCertPurchaseRef).one().del_flag==1
def test_transport_edit_locks_referenced_purchases_before_transport(ctx, monkeypatch):
db,u,s,m=ctx
p=s.save_cert(db,u,'purchase',purchase())
t=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G1','purchaseCertId':p['id']}]))
acquired=[]
original=s._get_row
def traced(db,user,kind,cert_id,lock=False):
if lock: acquired.append((kind,cert_id))
return original(db,user,kind,cert_id,lock)
monkeypatch.setattr(s,'_get_row',traced)
s.save_cert(db,u,'transport',{'purchaseRefs':[{k:v for k,v in t['purchaseRefs'][0].items() if k in ('id','referenceType','referenceNo','purchaseCertId')}]},t['id'])
assert acquired[:2]==[('purchase',p['id']),('transport',t['id'])]
def test_deleted_old_purchase_does_not_block_unrelated_reference_save(ctx):
db,u,s,m=ctx
p=s.save_cert(db,u,'purchase',purchase())
t=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G1','purchaseCertId':p['id']}]))
s.save_cert(db,u,'transport',{'purchaseRefs':[]},t['id'])
s.delete_cert(db,u,'purchase',p['id'])
result=s.save_cert(db,u,'transport',{'purchaseRefs':[]},t['id'])
assert result['purchaseRefs']==[]
@pytest.mark.parametrize('action',['opening','verify'])
def test_opening_verify_stale_operation_snapshot_uses_unique_insert_arbitration(ctx,monkeypatch,action):
from sqlalchemy.orm import Query
db,u,s,m=ctx
p=s.save_cert(db,u,'purchase',purchase())
initial=opening()
s.confirm_cert(db,u,'purchase',p['id'],initial)
if action=='verify':
body={'operationId':'verify-retry','reason':'重新核对'}
fn=s.verify_cert
fn(db,u,'purchase',p['id'],body)
else:
body=initial;fn=s.confirm_cert
count=db.query(m.PlCertOperation).count()
original=Query.first
missed=[]
def stale_once(query):
if query.column_descriptions[0].get('entity') is m.PlCertOperation and not missed:
missed.append(True)
return None
return original(query)
monkeypatch.setattr(Query,'first',stale_once)
result=fn(db,u,'purchase',p['id'],body)
assert missed and result['id']==p['id']
assert db.query(m.PlCertOperation).count()==count
assert db.query(m.PlCertQtyLog).count()==1
def test_opening_and_verify_allow_blank_reason(ctx):
db,u,s,m=ctx
p=s.save_cert(db,u,'purchase',purchase())
row=s.confirm_cert(db,u,'purchase',p['id'],{**opening(),'reason':' '})
assert row['status']=='active'
assert db.query(m.PlCertQtyLog).one().reason==''
s.save_cert(db,u,'purchase',{'buyerAddress':'更正地址'},p['id'])
verified=s.verify_cert(db,u,'purchase',p['id'],{'operationId':'verify-blank','reason':''})
assert verified['status']=='active' and verified['verifiedTime']
def test_rejected_opening_rolls_back_claimed_operation(ctx):
db,u,s,m=ctx
p=s.save_cert(db,u,'purchase',purchase())
with pytest.raises(HTTPException):
s.confirm_cert(db,u,'purchase',p['id'],{**opening(),'usedQty':'999'})
assert db.query(m.PlCertOperation).count()==0
assert db.query(m.PlCertQtyLog).count()==0
def test_replay_reads_current_certificate_and_opening_not_old_snapshot(ctx,monkeypatch):
from sqlalchemy.orm import Query
db,u,s,m=ctx
p=s.save_cert(db,u,'purchase',purchase())
row=db.get(m.PlPurchaseCert,p['id'])
stale=m.PlPurchaseCert(**{c.name:getattr(row,c.name) for c in row.__table__.columns})
s.confirm_cert(db,u,'purchase',p['id'],opening())
original=Query.first
def stale_reads(query):
entity=query.column_descriptions[0].get('entity')
if query._for_update_arg is None:
if entity is m.PlPurchaseCert: return stale
if entity is m.PlCertQtyLog: return None
return original(query)
monkeypatch.setattr(Query,'first',stale_reads)
replay=s.confirm_cert(db,u,'purchase',p['id'],opening())
assert replay['status']=='active' and replay['openingConfirmed'] is True
def test_closed_certificate_keeps_history_and_cannot_be_reopened(ctx):
db, u, s, m = ctx
row = s.save_cert(db, u, 'purchase', purchase())
confirmed = s.confirm_cert(db, u, 'purchase', row['id'], opening())
used = confirmed['usedQty']
closed = s.save_cert(db, u, 'purchase', {'status': 'closed'}, row['id'])
assert closed['status'] == 'closed' and closed['usedQty'] == used
assert db.query(m.PlCertQtyLog).count() == 1
for action, payload in (
('confirm', {**opening(), 'operationId': 'reopen-opening'}),
('verify', {'operationId': 'reopen-verify', 'reason': '重新启用'}),
):
with pytest.raises(HTTPException) as error:
getattr(s, f'{action}_cert')(db, u, 'purchase', row['id'], payload)
assert '已核销' in error.value.detail
with pytest.raises(HTTPException) as restored:
s.save_cert(db, u, 'purchase', {'status': 'suspended'}, row['id'])
assert restored.value.detail == '已核销证件不可恢复'
noted = s.save_cert(db, u, 'purchase', {'remark': '办结说明'}, row['id'])
assert noted['status'] == 'closed' and noted['remark'] == '办结说明'
stored = db.get(m.PlPurchaseCert, row['id'])
assert stored.status == 'closed' and stored.used_qty == used