From 697075462a9a71224388ce4da22d394e4cea0b18 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Sun, 15 Nov 2026 10:04:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(pl):=20=E8=AF=81=E4=BB=B6=E5=BB=BA=E6=A1=A3?= =?UTF-8?q?=E5=90=8E=E8=87=AA=E5=8A=A8=E5=90=AF=E7=94=A8=EF=BC=8C=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E6=9C=9F=E5=88=9D=E7=A1=AE=E8=AE=A4=E9=97=A8=E6=A7=9B?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保存即写入期初已用0并置为active;Web/H5仅保留复核入口,存量未确认证在读取或选证时自动补期初。 Co-authored-by: Cursor --- backend/app/services/pl_quota_cert_service.py | 144 +++++++++++++++++- .../app/services/pl_quota_customer_select.py | 12 ++ .../app/services/pl_quota_usage_service.py | 4 + backend/tests/test_pl_quota_api.py | 5 +- backend/tests/test_pl_quota_cert_archive.py | 56 ++++--- .../src/views/m/pl/plan-cert/ArchiveTab.vue | 4 +- .../views/m/pl/plan-cert/CertConfirmSheet.vue | 38 ++--- web-admin/src/views/pl/quota-cert/index.vue | 42 +---- 8 files changed, 216 insertions(+), 89 deletions(-) diff --git a/backend/app/services/pl_quota_cert_service.py b/backend/app/services/pl_quota_cert_service.py index 705c6dfd6..a248e8298 100644 --- a/backend/app/services/pl_quota_cert_service.py +++ b/backend/app/services/pl_quota_cert_service.py @@ -87,6 +87,9 @@ def _transport_derived_categories(db, user, transport_cert_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( @@ -98,7 +101,9 @@ def get_cert(db, user, cert_type, cert_id, current=False): result['availableQty'] = row.total_qty - row.used_qty - row.reserved_qty 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 @@ -169,8 +174,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: @@ -178,6 +186,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;清空请提交[]') @@ -282,7 +319,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, @@ -329,19 +367,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': @@ -388,7 +505,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, '期初已用量不能超过核定量') diff --git a/backend/app/services/pl_quota_customer_select.py b/backend/app/services/pl_quota_customer_select.py index 2813138e5..3802af3f2 100644 --- a/backend/app/services/pl_quota_customer_select.py +++ b/backend/app/services/pl_quota_customer_select.py @@ -42,6 +42,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): @@ -116,6 +126,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): @@ -165,6 +176,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): diff --git a/backend/app/services/pl_quota_usage_service.py b/backend/app/services/pl_quota_usage_service.py index d80836ec7..0ffdb19df 100644 --- a/backend/app/services/pl_quota_usage_service.py +++ b/backend/app/services/pl_quota_usage_service.py @@ -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('证件尚未启用复核') @@ -370,6 +372,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: diff --git a/backend/tests/test_pl_quota_api.py b/backend/tests/test_pl_quota_api.py index 06180aee0..1988f27e5 100644 --- a/backend/tests/test_pl_quota_api.py +++ b/backend/tests/test_pl_quota_api.py @@ -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' diff --git a/backend/tests/test_pl_quota_cert_archive.py b/backend/tests/test_pl_quota_cert_archive.py index e871e3825..cc1f0a422 100644 --- a/backend/tests/test_pl_quota_cert_archive.py +++ b/backend/tests/test_pl_quota_cert_archive.py @@ -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': '重新启用'}), diff --git a/web-admin/src/views/m/pl/plan-cert/ArchiveTab.vue b/web-admin/src/views/m/pl/plan-cert/ArchiveTab.vue index ffad13519..e05032341 100644 --- a/web-admin/src/views/m/pl/plan-cert/ArchiveTab.vue +++ b/web-admin/src/views/m/pl/plan-cert/ArchiveTab.vue @@ -60,12 +60,12 @@ >编辑 diff --git a/web-admin/src/views/m/pl/plan-cert/CertConfirmSheet.vue b/web-admin/src/views/m/pl/plan-cert/CertConfirmSheet.vue index 12f220b17..984c87683 100644 --- a/web-admin/src/views/m/pl/plan-cert/CertConfirmSheet.vue +++ b/web-admin/src/views/m/pl/plan-cert/CertConfirmSheet.vue @@ -8,37 +8,21 @@ :close-on-click-overlay="!saving" @update:show="onShow" > - +
-

{{ row.certNo }} · {{ row.openingConfirmed ? '复核已启用的证件' : '核对历史用量后启用' }}

- +

{{ row.certNo }} · 复核已启用的证件