fix(pl): 证书齐备待完善归档不再误出齐备按钮。

归档结转无用量行时保留待完善错误文案,前端按计划齐备状态只提醒完善证件。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-15 14:51:45 +08:00
1 parent 95ecb7236e
commit 02d576abfe
7 files changed
+146 -9

No files matched your search

+1 -1
View File
@@ -3807,7 +3807,7 @@ def archive_plan(
db.flush()
result = get_plan(id, db, user)
if not quota_settled:
result.quota_settlement_error = read_quota_settlement_error(db, tenant_no, plan.id)
result.quota_settlement_error = read_quota_settlement_error(db, tenant_no, plan.id, plan=plan)
return result
+16 -2
View File
@@ -171,26 +171,40 @@ def settle_plan_quota(db, user, plan, payload, *, adjustment=False):
except ArchiveQuotaBlocked as exc:
raise HTTPException(409, str(exc)) from exc
except (HTTPException, ValueError, AllocationIncomplete) as exc:
message = str(getattr(exc, 'detail', exc))
# 无用量行(如证书齐备仅放行安排)时仍保留原因,供同请求读回。
setattr(plan, '_quota_settlement_error', message)
rows = db.query(PlPlanCertUsage).filter_by(tenant_no=user.tenant_no, plan_id=plan.id, del_flag=0).all()
for row in rows:
if not adjustment:
row.usage_status = 'exception'
row.confirmation_snapshot = {**(row.confirmation_snapshot or {}), 'executed': True,
'settlementError': str(getattr(exc, 'detail', exc))}
'settlementError': message}
row.update_by = user.id
if not adjustment:
plan.archive_status = 'exception'
return False
def read_quota_settlement_error(db, tenant_no, plan_id):
def read_quota_settlement_error(db, tenant_no, plan_id, plan=None):
"""结转失败时返回已经写在用量快照上的原因。"""
from app.models.pl_models import PlPlan
from app.models.pl_quota_models import PlPlanCertUsage
if plan is not None:
cached = getattr(plan, '_quota_settlement_error', None)
if isinstance(cached, str) and cached.strip():
return cached.strip()
rows = db.query(PlPlanCertUsage).filter_by(tenant_no=tenant_no, plan_id=plan_id, del_flag=0).all()
for row in rows:
message = (row.confirmation_snapshot or {}).get('settlementError')
if isinstance(message, str) and message.strip():
return message.strip()
target = plan
if target is None:
target = db.query(PlPlan).filter_by(id=plan_id, tenant_no=tenant_no, del_flag=0).first()
if target is not None and int(getattr(target, 'quota_cert_pending_fulfill', 0) or 0) == 1:
return '该计划仍处于证书齐备待完善,请先在证书齐备中完善并关闭后再归档'
return '实际数量已保存,证件没有扣减。请把实际数量改小,或先补充购买证和运输证,再重新归档。'
@@ -61,6 +61,47 @@ def test_apply_plan_quota_new_rule_waive_skips_reserve_not_settle(monkeypatch):
assert '待完善' in str(exc.value.detail)
def test_settle_pending_fulfill_without_usage_rows_keeps_pending_message(monkeypatch):
"""证书齐备待完善且无用量行时,归档结转错误不能回落成通用扣减文案。"""
from contextlib import nullcontext
from app.services import pl_quota_business as biz
from fastapi import HTTPException
plan = SimpleNamespace(
id='p1',
quota_cert_waived=1,
quota_cert_pending_fulfill=1,
cert_quota_rule_version='customer_category',
cert_configuration={},
archive_status='none',
)
user = SimpleNamespace(id='u', tenant_no='t')
pending = '该计划仍处于证书齐备待完善,请先在证书齐备中完善并关闭后再归档'
class _Query:
def filter_by(self, **kwargs):
return self
def all(self):
return []
db = MagicMock()
db.query.return_value = _Query()
db.flush = MagicMock()
db.begin_nested = MagicMock(return_value=nullcontext())
def boom(*_a, **_k):
raise HTTPException(409, pending)
monkeypatch.setattr(biz, 'apply_plan_quota', boom)
assert biz.settle_plan_quota(db, user, plan, None) is False
message = biz.read_quota_settlement_error(db, 't', 'p1', plan=plan)
assert '待完善' in message
assert '证件没有扣减' not in message
def test_list_certs_default_excludes_closed_void(monkeypatch):
from app.services import pl_quota_cert_service as svc
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { resolveArchiveQuotaBlockMode } from '../src/utils/pl-archive-quota-block'
test('pending fulfill plan only alerts, no certificate-ready confirm', () => {
assert.equal(
resolveArchiveQuotaBlockMode(
'实际数量已保存,证件没有扣减。请把实际数量改小,或先补充购买证和运输证,再重新归档。',
'用证没有完成',
{ quotaCertPendingFulfill: 1, quotaCertWaived: 1 }
),
'alert-pending'
)
})
test('already waived plan does not offer archive-time certificate ready', () => {
assert.equal(
resolveArchiveQuotaBlockMode('余额不足', '用证没有完成', { quotaCertWaived: 1 }),
'alert-pending'
)
})
test('pending keywords still force alert mode without plan flags', () => {
assert.equal(
resolveArchiveQuotaBlockMode(
'该计划仍处于证书齐备待完善,请先在证书齐备中完善并关闭后再归档',
'用证没有完成',
{}
),
'alert-pending'
)
})
test('unmarked legacy quota block still offers certificate ready', () => {
assert.equal(
resolveArchiveQuotaBlockMode(
'实际数量已保存,证件没有扣减。请把实际数量改小,或先补充购买证和运输证,再重新归档。',
'用证没有完成',
{ quotaCertPendingFulfill: 0, quotaCertWaived: 0 }
),
'confirm-ready'
)
})
test('non quota-title blocks stay plain alert', () => {
assert.equal(resolveArchiveQuotaBlockMode('不能归档:缺证', '不能归档', {}), 'alert-plain')
})
+7
View File
@@ -624,6 +624,13 @@ export interface Plan {
hazardCertReady?: number
hazardCertReadyBy?: string
hazardCertReadyTime?: string
/** 证书齐备:新规则仅放行安排 */
quotaCertWaived?: number
quotaCertWaivedBy?: string
quotaCertWaivedByName?: string
quotaCertWaivedTime?: string
/** 新规则证书齐备待完善 */
quotaCertPendingFulfill?: number
erpEntered?: number
remark?: string
createBy?: string
@@ -299,6 +299,7 @@
extractArchiveLineOverageDetail,
showArchiveLineOverageConfirm
} from '@/utils/pl-contract-qty-reminder'
import { resolveArchiveQuotaBlockMode } from '@/utils/pl-archive-quota-block'
const props = defineProps<{
visible: boolean
@@ -1602,12 +1603,12 @@
async function showArchiveQuotaBlockDialog(message: string, title = '不能归档') {
const html = `<p class="pl-arrange-block-text">${escapeArchiveBlockText(message)}</p>`
const pendingFulfill =
message.includes('待完善') || message.includes('完善并关闭') || message.includes('证书齐备待完善')
if (title !== '用证没有完成' || pendingFulfill) {
const guide = pendingFulfill
? `${html}<p class="pl-arrange-block-text">新规则下证书齐备只放行安排,不能绕过归档挂证。请到「证书齐备 → 已标记待完善证书」上传证件并执行完善并关闭后再归档。</p>`
: html
const mode = resolveArchiveQuotaBlockMode(message, title, plan.value)
if (mode !== 'confirm-ready') {
const guide =
mode === 'alert-pending'
? `${html}<p class="pl-arrange-block-text">该运输计划已走证书齐备,归档前须完善证件。请到「证书齐备 → 已标记待完善证书」上传购买证/运输证并执行完善并关闭后再归档。</p>`
: html
await ElMessageBox.alert(guide, title, {
type: 'warning',
confirmButtonText: '知道了',
@@ -0,0 +1,27 @@
/** 归档用证拦截弹窗:已齐备/待完善只提醒,未齐备旧规则才可再点证书齐备。 */
export type ArchiveQuotaBlockMode = 'alert-pending' | 'alert-plain' | 'confirm-ready'
export type ArchiveQuotaBlockPlanFlags = {
quotaCertPendingFulfill?: number | boolean | null
quotaCertWaived?: number | boolean | null
}
function flagOn(value: number | boolean | null | undefined) {
return value === true || Number(value || 0) === 1
}
export function resolveArchiveQuotaBlockMode(
message: string,
title: string,
plan?: ArchiveQuotaBlockPlanFlags | null
): ArchiveQuotaBlockMode {
const pendingByMessage =
message.includes('待完善') ||
message.includes('完善并关闭') ||
message.includes('证书齐备待完善')
const pendingByPlan = flagOn(plan?.quotaCertPendingFulfill) || flagOn(plan?.quotaCertWaived)
if (title !== '用证没有完成') return 'alert-plain'
if (pendingByPlan || pendingByMessage) return 'alert-pending'
return 'confirm-ready'
}