Compare commits

...
Author SHA1 Message Date
wuzilongandCursor 937a857347 fix(pl): 证书齐备待完善列表将多选列置于首列。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-15 15:13:14 +08:00
wuzilongandCursor e0598ed07b fix(pl): 证件建档后自动启用,取消期初确认门槛。
保存即写入期初已用0并置为active;Web/H5仅保留复核入口,存量未确认证在读取或选证时自动补期初。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-15 14:53:43 +08:00
10 changed files with 238 additions and 95 deletions

No files matched your search

+139 -5
View File
@@ -138,6 +138,9 @@ def _customer_display_name(db, user, customer_id):
def get_cert(db, user, cert_type, cert_id, current=False):
row = _get_row(db, user, cert_type, cert_id, lock=current)
# 存量未期初确认的证件在读取时自动补期初0并启用(void/closed 除外)。
if not current:
row = ensure_zero_opening(db, user, cert_type, cert_id, row=row)
result = _row_to_dict(row)
result['usagePolicy'] = usage_policy(row.valid_use_text)
opening_query = db.query(PlCertQtyLog.id).filter_by(
@@ -151,7 +154,9 @@ def get_cert(db, user, cert_type, cert_id, current=False):
result['customerName'] = _customer_display_name(db, user, getattr(row, 'customer_id', None))
if cert_type == 'transport':
result['purchaseRefs'] = [_row_to_dict(r) for r in _refs(db, user, cert_id, lock=current)]
result['referencesEditable'] = not _has_history(db, user, cert_type, cert_id, lock=current)
result['referencesEditable'] = not _has_usage_beyond_zero_opening(
db, user, cert_type, cert_id, lock=current
)
result['derivedCategoryIds'] = _transport_derived_categories(db, user, cert_id)
return result
@@ -224,8 +229,11 @@ def _refs_keep_verified(pending, removed):
def _has_history(db, user, cert_type, cert_id, lock=True):
"""任意额度流水或计划占用均视为已有历史(含自动期初),用于锁证号/核定量等身份。"""
for model in (PlCertQtyLog, PlPlanCertUsage):
query = db.query(model.id).filter_by(tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id)
query = db.query(model.id).filter_by(
tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id, del_flag=0,
)
if lock:
query = query.with_for_update()
if query.first() is not None:
@@ -233,6 +241,35 @@ def _has_history(db, user, cert_type, cert_id, lock=True):
return False
def _has_usage_beyond_zero_opening(db, user, cert_type, cert_id, lock=True):
"""真实用量/占用才锁运输证引用;期初已用=0 的 opening 仍可补全关联购买证。"""
usage_q = db.query(PlPlanCertUsage.id).filter_by(
tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id, del_flag=0,
)
if lock:
usage_q = usage_q.with_for_update()
if usage_q.first() is not None:
return True
log_q = db.query(PlCertQtyLog).filter_by(
tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id, del_flag=0,
)
if lock:
log_q = log_q.with_for_update()
for log in log_q.all():
snap = log.event_snapshot or {}
# 仅建档自动期初(已用0)不锁引用;手工期初或其它流水仍锁。
if (
log.event_type == 'opening'
and snap.get('auto')
and (log.used_delta or 0) == 0
and (log.reserved_delta or 0) == 0
and (log.total_delta or 0) == 0
):
continue
return True
return False
def _validate_refs(db, user, cert_id, submitted, historical, purchases, current=False):
if submitted is None:
raise HTTPException(400, '引用集合不可为null;清空请提交[]')
@@ -337,7 +374,8 @@ def prepare_cert(db, user, cert_type, data, cert_id=None, *, locked=None, effect
if duplicate and duplicate.id != cert_id:
raise HTTPException(400, '证号已存在,删除后也不得重建额度池')
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 ([],[])
refs_locked = bool(row and _has_usage_beyond_zero_opening(db, user, cert_type, cert_id))
pending, removed = _validate_refs(db,user,effective_id,refs,refs_locked,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,
@@ -384,19 +422,98 @@ def apply_cert(db, user, prepared):
return row
def ensure_zero_opening(db, user, cert_type, cert_id, *, row=None):
"""建档后无需手工期初确认:缺 opening 流水时写入期初已用=0;draft 直接启用。"""
if row is None:
row = _get_row(db, user, cert_type, cert_id)
if row.status in ('void', 'closed'):
return row
existing = db.query(PlCertQtyLog.id).filter_by(
tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id, event_type='opening', del_flag=0,
).first()
if existing:
return row
row = _get_row(db, user, cert_type, cert_id, lock=True)
if row.status in ('void', 'closed'):
return row
if db.query(PlCertQtyLog.id).filter_by(
tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id, event_type='opening', del_flag=0,
).with_for_update().first():
return row
# 有效次数原文未识别时保留 draft,不自动启用;明确一次/多次后保存或读取再启用。
if usage_policy(row.valid_use_text) == 'needs_confirmation':
return row
used = Decimal(row.used_qty or 0)
op_id = f'auto-opening:{cert_type}:{cert_id}'
digest = hashlib.sha256(op_id.encode()).hexdigest()
operation, replay = _claim_operation(db, user, 'opening', op_id, digest)
if replay:
return row
db.add(PlCertQtyLog(
**_audit(user),
operation_id=op_id,
request_hash=digest,
cert_type=cert_type,
cert_id=cert_id,
event_type='opening',
quantity_unit=row.quantity_unit,
total_delta=Decimal(0),
reserved_delta=Decimal(0),
used_delta=used,
before_total_qty=row.total_qty,
after_total_qty=row.total_qty,
before_reserved_qty=row.reserved_qty,
after_reserved_qty=row.reserved_qty,
before_used_qty=used,
after_used_qty=used,
reason='建档自动启用',
evidence_urls=None,
event_snapshot={'singleUseConsumed': False, 'sourceNote': 'auto', 'auto': True},
))
if row.status == 'draft':
row.status = 'active'
row.verified_by = user.id
row.verified_time = datetime.now()
row.update_by = user.id
operation.result_json = {'certType': cert_type, 'certId': cert_id, 'auto': True}
db.flush()
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))
ensure_zero_opening(db, user, cert_type, row.id, row=row)
return get_cert(db, user, cert_type, row.id)
def delete_cert(db, user, cert_type, cert_id):
row = _get_row(db,user,cert_type,cert_id,lock=True)
if _has_history(db,user,cert_type,cert_id) or row.reserved_qty or row.used_qty:
if row.reserved_qty or row.used_qty:
raise HTTPException(400, '已有用量或流水的证件只能停用、核销或作废')
if db.query(PlPlanCertUsage.id).filter_by(
tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id, del_flag=0,
).with_for_update().first():
raise HTTPException(400, '已有用量或流水的证件只能停用、核销或作废')
logs = (
db.query(PlCertQtyLog)
.filter_by(tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id, del_flag=0)
.with_for_update()
.all()
)
# 仅建档自动期初(或无流水)仍允许删除;手工期初或其它额度流水则禁止。
def _auto_opening(log):
snap = log.event_snapshot or {}
return log.event_type == 'opening' and bool(snap.get('auto')) and (log.used_delta or 0) == 0
if any(not _auto_opening(log) for log in logs):
raise HTTPException(400, '已有用量或流水的证件只能停用、核销或作废')
if db.query(PlContractCertLink.id).filter_by(tenant_no=user.tenant_no,cert_type=cert_type,cert_id=cert_id,del_flag=0).with_for_update().first():
raise HTTPException(400, '证件已绑定合同')
if cert_type == 'purchase' and db.query(PlTransportCertPurchaseRef.id).filter_by(tenant_no=user.tenant_no,purchase_cert_id=cert_id,del_flag=0).with_for_update().first():
raise HTTPException(400, '证件被运输证引用')
for log in logs:
log.del_flag = 1
log.update_by = user.id
row.del_flag = 1
row.update_by = user.id
if cert_type == 'transport':
@@ -443,7 +560,24 @@ def _confirm_cert(db, user, cert_type, cert_id, data):
_assert_known_usage_policy(row)
if row.status == 'void': raise HTTPException(400, '作废证件不能启用')
if row.status == 'closed': raise HTTPException(400, '已核销证件不能启用')
if row.verified_time or _has_history(db,user,cert_type,cert_id):
# 建档自动期初(used=0)可被手工/接口期初覆盖;已有真实用量或非自动期初则拒绝。
openings = (
db.query(PlCertQtyLog)
.filter_by(
tenant_no=user.tenant_no, cert_type=cert_type, cert_id=cert_id,
event_type='opening', del_flag=0,
)
.with_for_update()
.all()
)
for log in openings:
snap = log.event_snapshot or {}
if snap.get('auto') and (log.used_delta or 0) == 0:
log.del_flag = 1
log.update_by = user.id
else:
raise HTTPException(400, '期初已确认,不能重复初始化额度')
if _has_history(db, user, cert_type, cert_id):
raise HTTPException(400, '期初已确认,不能重复初始化额度')
if body.used_qty > row.total_qty:
raise HTTPException(400, '期初已用量不能超过核定量')
@@ -43,6 +43,16 @@ def _has_opening(db, user, cert_type, cert_id) -> bool:
)
def _ensure_opening_quiet(db, user, cert_type, cert_id, row):
"""选证前尽量自动补期初;单元测试 mock 库失败时静默跳过。"""
try:
from app.services.pl_quota_cert_service import ensure_zero_opening
ensure_zero_opening(db, user, cert_type, cert_id, row=row)
except Exception:
return
def _vehicle_ok(cert, vehicle_no: str) -> bool:
plates = cert.vehicle_numbers_json or []
if not isinstance(plates, list):
@@ -117,6 +127,7 @@ def select_whole_cover_certs(db, user, sources, *, vehicle_no: str, on_date: dat
for purchase in candidates:
if not purchase.verified_by or not purchase.verified_time:
continue
_ensure_opening_quiet(db, user, 'purchase', purchase.id, purchase)
if not _has_opening(db, user, 'purchase', purchase.id):
continue
if not _in_force(purchase, on_date):
@@ -166,6 +177,7 @@ def select_whole_cover_certs(db, user, sources, *, vehicle_no: str, on_date: dat
for transport in transports:
if not transport.verified_by or not transport.verified_time:
continue
_ensure_opening_quiet(db, user, 'transport', transport.id, transport)
if not _has_opening(db, user, 'transport', transport.id):
continue
if not _in_force(transport, on_date):
@@ -200,8 +200,10 @@ def _candidate_context(db,user,r,plan,certs,usages,items,selected,preview=False,
skipped_for_vehicle=False
from app.services.pl_cert_quota_rule_config_service import plan_uses_customer_category
customer_rule = plan_uses_customer_category(plan)
from app.services.pl_quota_cert_service import ensure_zero_opening
for k in sorted(selected):
kind,pid=k; c=certs[k]
c = certs[k] = ensure_zero_opening(db, user, kind, pid, row=c)
if not query(m.PlCertQtyLog,cert_type=kind,cert_id=pid,event_type='opening',del_flag=0).first(): raise ValueError('证件尚未确认期初')
if c.status=='closed': raise ValueError('已核销证件不能再安排用证')
if c.status!='active' or not c.verified_time or not c.verified_by: raise ValueError('证件尚未启用复核')
@@ -368,6 +370,8 @@ ARCHIVE_QUOTA_BLOCK = '合同绑定的购买证和运输证剩余额度不够覆
def _settlement_cover_cert(db, user, r, plan, kind, cert):
"""合同上另有证件时,不合格的证跳过,不因此挡住其余还能扣的证。"""
from app.services.pl_quota_cert_service import ensure_zero_opening
cert = ensure_zero_opening(db, user, kind, cert.id, row=cert)
if not _query(db, user, m.PlCertQtyLog, cert_type=kind, cert_id=cert.id, event_type='opening', del_flag=0).first():
return False
if cert.status != 'active' or not cert.verified_time or not cert.verified_by:
+3 -2
View File
@@ -57,7 +57,7 @@ def test_permission_guards_validation_and_atomic_creation(api):
saved = client.post(path, json=payload())
assert saved.status_code == 200
row = saved.json()
assert row['tenantNo'] == 't' and row['status'] == 'draft' and row['openingConfirmed'] is False
assert row['tenantNo'] == 't' and row['status'] == 'active' and row['openingConfirmed'] is True
assert client.put(path + '/' + row['id'], json={'totalQty': '-1'}).status_code == 422
assert client.get(path + '/' + row['id']).json()['totalQty'] == '30.000000'
app.dependency_overrides[auth] = lambda: {'role': 'admin', 'user_id': 'u', 'tenant_no': 'other'}
@@ -130,7 +130,8 @@ def test_total_adjust_requires_provenance_and_replays_without_duplicate_log(api)
assert response.status_code == 200
assert client.post(path, json=body).status_code == 200
logs = client.get(base + '/' + row['id'] + '/logs').json()
assert len(logs) == 1 and logs[0]['totalDelta'] == '5.000000'
adjust_logs = [item for item in logs if item.get('eventType') == 'total_adjust']
assert len(adjust_logs) == 1 and adjust_logs[0]['totalDelta'] == '5.000000'
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'
+39 -17
View File
@@ -41,14 +41,31 @@ def transport(**kw):
return data
def test_save_auto_enables_with_zero_opening(ctx):
"""建档即启用:期初已用默认 0,无需手工期初确认。"""
db, u, s, m = ctx
row = s.save_cert(db, u, 'purchase', purchase())
assert row['status'] == 'active'
assert row['openingConfirmed'] is True
assert row['usedQty'] == Decimal('0')
assert row['verifiedBy'] == 'u'
assert row['verifiedTime'] is not None
log = db.query(m.PlCertQtyLog).filter_by(del_flag=0, event_type='opening').one()
assert (log.event_snapshot or {}).get('auto') is True
transport_row = s.save_cert(db, u, 'transport', transport())
assert transport_row['status'] == 'active' and transport_row['openingConfirmed'] is True
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
assert row['status'] == 'active'
assert row['openingConfirmed'] is True
assert row['usedQty'] == Decimal('0')
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0, event_type='opening').count() == 1
db.rollback()
assert db.query(m.PlPurchaseCert).count() == 0
@@ -56,9 +73,8 @@ def test_manual_optional_image_complete_fields(ctx):
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': '全新证'})
assert row['openingConfirmed'] is True
assert row['status'] == 'active'
changed = svc.save_cert(db, user, 'purchase', {'buyerAddress': '更正地址'}, row['id'])
assert changed['verifiedTime'] is None
assert changed['openingConfirmed'] is True
@@ -89,10 +105,12 @@ def test_reference_delta_omitted_empty_and_invalid_prewrite(ctx):
def test_confirmation_and_history_protection(ctx):
db,u,s,m=ctx
row=s.save_cert(db,u,'purchase',purchase())
assert row['status']=='active' and row['openingConfirmed'] is True
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'
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).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'])
@@ -151,8 +169,9 @@ def test_opening_retry_preserves_balance_and_operation_identity(ctx):
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
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).count()==1
# 自动期初操作号 + 手工期初操作号;重放不新增手工操作
assert db.query(m.PlCertOperation).filter_by(operation_id='opening').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'))
@@ -209,10 +228,11 @@ def test_reverify_after_correction_keeps_opening_and_balances(ctx):
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
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).count()==1
# 自动期初 + 手工期初 + 复核
assert db.query(m.PlCertOperation).count()==3
result=s.verify_cert(db,u,'purchase',row['id'],{'operationId':'verify-1','reason':'地址更正核对','evidenceUrls':'https://minio/evidence'})
assert db.query(m.PlCertOperation).count()==2
assert db.query(m.PlCertOperation).count()==3
def test_actual_scope_denies_user_without_roles(ctx):
@@ -261,7 +281,7 @@ def test_historical_opening_accepts_written_source_without_image(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')
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).one().event_snapshot['sourceNote'].startswith('2026')
def test_unknown_use_text_is_preserved_and_requires_confirmation(ctx):
@@ -423,7 +443,7 @@ def test_opening_verify_stale_operation_snapshot_uses_unique_insert_arbitration(
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
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).count()==1
def test_opening_and_verify_allow_blank_reason(ctx):
@@ -431,7 +451,7 @@ def test_opening_and_verify_allow_blank_reason(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==''
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).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']
@@ -440,10 +460,12 @@ def test_opening_and_verify_allow_blank_reason(ctx):
def test_rejected_opening_rolls_back_claimed_operation(ctx):
db,u,s,m=ctx
p=s.save_cert(db,u,'purchase',purchase())
auto_ops = db.query(m.PlCertOperation).count()
auto_logs = db.query(m.PlCertQtyLog).filter_by(del_flag=0).count()
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
assert db.query(m.PlCertOperation).count()==auto_ops
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).count()==auto_logs
def test_replay_reads_current_certificate_and_opening_not_old_snapshot(ctx,monkeypatch):
@@ -472,7 +494,7 @@ def test_closed_certificate_keeps_history_and_cannot_be_reopened(ctx):
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
assert db.query(m.PlCertQtyLog).filter_by(del_flag=0).count() == 1
for action, payload in (
('confirm', {**opening(), 'operationId': 'reopen-opening'}),
('verify', {'operationId': 'reopen-verify', 'reason': '重新启用'}),
@@ -25,6 +25,20 @@ test('incomplete certificate filter is sent as quotaCertWaivedIn=0', () => {
assert.deepEqual(params, { quotaCertWaivedIn: '0' })
})
test('fulfill tab selection column is first among table columns', () => {
const web = readFileSync(new URL('../src/views/pl/cert-ready/index.vue', import.meta.url), 'utf8')
const selectionIdx = web.indexOf('type="selection"')
const columnsIdx = web.indexOf('v-for="column in columns"')
const actionsIdx = web.indexOf('label="操作"')
assert.ok(selectionIdx > 0, 'selection column missing')
assert.ok(columnsIdx > selectionIdx, 'selection must precede business columns')
assert.ok(actionsIdx > columnsIdx, 'actions must follow business columns')
assert.match(
web,
/v-if="listMode === 'fulfill'"[\s\S]*?type="selection"[\s\S]*?v-for="column in columns"/
)
})
test('web funnel and miniprogram status dropdown default to incomplete', () => {
const web = readFileSync(new URL('../src/views/pl/cert-ready/index.vue', import.meta.url), 'utf8')
const mobile = readFileSync(
@@ -60,12 +60,12 @@
>编辑</button
>
<button
v-if="canEdit && row.status !== 'void' && row.status !== 'closed'"
v-if="canEdit && row.openingConfirmed && row.status !== 'void' && row.status !== 'closed'"
class="mp-wv-btn"
type="button"
@click="confirmRow = row"
>
{{ row.openingConfirmed ? '复核' : '期初确认' }}
复核
</button>
</div>
</article>
@@ -8,37 +8,21 @@
:close-on-click-overlay="!saving"
@update:show="onShow"
>
<van-nav-bar
:title="row?.openingConfirmed ? '复核证件' : '期初核对'"
left-text="关闭"
@click-left="!saving && emit('close')"
/>
<van-nav-bar title="复核证件" left-text="关闭" @click-left="!saving && emit('close')" />
<div v-if="row" class="mp-cert-confirm-sheet__body mp-cert-form">
<p class="plan-empty-hint">{{ row.certNo }} · {{ row.openingConfirmed ? '复核已启用的证件' : '核对历史用量后启用' }}</p>
<template v-if="!row.openingConfirmed">
<van-field v-model="opening.usedQty" label="历史已用量" label-align="top" type="number" :disabled="saving" />
<van-cell title="一次有效证已有使用记录" center>
<template #right-icon>
<van-switch v-model="opening.singleUseConsumed" :disabled="saving" size="20px" />
</template>
</van-cell>
<van-field v-model="opening.sourceNote" label="历史来源说明" label-align="top" type="textarea" rows="2" :disabled="saving" />
<p class="plan-empty-hint">核对凭据</p>
<AttachmentUploadField v-model="opening.evidenceUrls" folder="pl/quota-cert" :disabled="saving" hint="" />
</template>
<p class="plan-empty-hint">{{ row.certNo }} · 复核已启用的证件</p>
<van-field v-model="opening.reason" label="核对说明" label-align="top" type="textarea" rows="2" :disabled="saving" />
</div>
<footer>
<button class="mp-wv-btn" type="button" :disabled="saving" @click="emit('close')">取消</button>
<button class="mp-wv-btn mp-wv-btn--primary" type="button" :disabled="saving" @click="submit">
{{ saving ? '正在提交…' : '确认启用' }}
{{ saving ? '正在提交…' : '确认复核' }}
</button>
</footer>
</van-popup>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { quotaCertApi, type CertType, type QuotaCert } from '@/api/pl/quota-cert'
import { randomOperationId } from '@/utils/random-id'
import './cert-mobile.scss'
@@ -76,19 +60,15 @@
opening.reason = opening.reason.trim()
saving.value = true
try {
if (props.row.openingConfirmed) {
await quotaCertApi.verify(kind, props.row.id, {
operationId: opening.operationId,
reason: opening.reason
})
} else {
await quotaCertApi.confirm(kind, props.row.id, { ...opening })
}
ElMessage.success(props.row.openingConfirmed ? '证件已复核' : '期初已确认')
await quotaCertApi.verify(kind, props.row.id, {
operationId: opening.operationId,
reason: opening.reason
})
ElMessage.success('证件已复核')
emit('saved')
emit('close')
} catch {
ElMessage.error('核对没有提交,请重试')
ElMessage.error('复核没有提交,请重试')
} finally {
saving.value = false
}
+8 -6
View File
@@ -50,6 +50,14 @@
class="list-table"
@selection-change="onSelectionChange"
>
<el-table-column
v-if="listMode === 'fulfill'"
type="selection"
width="48"
align="center"
header-align="center"
:reserve-selection="false"
/>
<el-table-column
v-for="column in columns"
:key="column.prop"
@@ -93,12 +101,6 @@
<span v-else>{{ row[column.prop] || '—' }}</span>
</template>
</el-table-column>
<el-table-column
v-if="listMode === 'fulfill'"
type="selection"
width="48"
:reserve-selection="false"
/>
<el-table-column label="操作" min-width="260" fixed="right">
<template #default="{ row }">
<template v-if="listMode === 'fulfill'">
+8 -34
View File
@@ -93,11 +93,10 @@
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button
v-if="canEdit && row.status !== 'void' && row.status !== 'closed'"
v-if="canEdit && row.openingConfirmed && row.status !== 'void' && row.status !== 'closed'"
size="small"
@click="openConfirm(row)"
><el-icon><Check /></el-icon
>{{ row.openingConfirmed ? '复核' : '期初确认' }}</el-button
><el-icon><Check /></el-icon>复核</el-button
>
<el-button size="small" @click="view(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -591,35 +590,12 @@
</SystemViewDialog>
<el-dialog
v-model="confirmVisible"
:title="confirmRow?.openingConfirmed ? '复核证件' : '期初核对'"
title="复核证件"
width="640px"
align-center
class="dialog-form-layout quota-cert-confirm-dialog"
>
<el-form label-position="top">
<template v-if="!confirmRow?.openingConfirmed">
<div class="form-grid-2">
<el-form-item>
<template #label>
历史已用量(证面单位)<span v-if="confirmUnit" class="form-label-tip">{{
confirmUnit
}}</span>
</template>
<el-input v-model="opening.usedQty">
<template v-if="confirmUnit" #append>{{ confirmUnit }}</template>
</el-input>
</el-form-item>
<el-form-item label="一次有效证是否已有使用记录">
<el-switch v-model="opening.singleUseConsumed" />
</el-form-item>
</div>
<el-form-item label="历史来源说明">
<el-input v-model="opening.sourceNote" type="textarea" />
</el-form-item>
<el-form-item label="核对凭据">
<AttachmentUploadField v-model="opening.evidenceUrls" folder="pl/quota-cert" />
</el-form-item>
</template>
<el-form-item label="核对说明"
><el-input v-model="opening.reason" type="textarea"
/></el-form-item>
@@ -628,7 +604,7 @@
><el-button @click="confirmVisible = false"
><el-icon><Close /></el-icon>取消</el-button
><el-button type="primary" :loading="saving" @click="confirm"
><el-icon><Check /></el-icon>确认启用</el-button
><el-icon><Check /></el-icon>确认复核</el-button
></template
>
</el-dialog>
@@ -1818,12 +1794,10 @@
saving.value = true
try {
const targetKind = rowCertType(confirmRow.value)
if (confirmRow.value.openingConfirmed)
await quotaCertApi.verify(targetKind, confirmRow.value.id, {
operationId: opening.operationId,
reason: opening.reason
})
else await quotaCertApi.confirm(targetKind, confirmRow.value.id, opening)
await quotaCertApi.verify(targetKind, confirmRow.value.id, {
operationId: opening.operationId,
reason: opening.reason
})
confirmVisible.value = false
await load()
} finally {