feat(T+): 完成数据贯通与自动接入闭环

打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。
新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。
补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
This commit is contained in:
liuwenxin committed 2026-11-05 18:47:13 +08:00
1 parent 3efe72c344
commit 1e53792ad6
151 files changed
+8842 -401

No files matched your search

+45 -4
View File
@@ -24,7 +24,7 @@
import logging
from dataclasses import dataclass
from datetime import date
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy.exc import IntegrityError
@@ -38,7 +38,7 @@ from app.core.constants import (
NO_PREFIX_RECEIPT,
)
from app.core.db_errors import is_duplicate_entry
from app.core.exceptions import BadRequestError, NotFoundError
from app.core.exceptions import BadRequestError, ConflictError, NotFoundError
from app.core.response import PageResult
from app.models import Business, Receipt
from app.models.business import BUSINESS_STATUS_ACTIVE
@@ -50,7 +50,7 @@ from app.schemas.receipt import (
ReceiptQuery,
ReceiptUpdateIn,
)
from app.services import audit_service
from app.services import audit_service, receipt_confirmation
from app.services.audit_service import AuditRecord, RequestMeta
from app.services.business_service import _recalc_totals
from app.services.formula import payment_days
@@ -81,6 +81,10 @@ def _to_out(row: tuple) -> ReceiptOut:
amount=str(receipt.amount),
is_barter=receipt.is_barter,
settlement_status=receipt.settlement_status,
confirmation_status=1 if receipt_confirmation.is_confirmed(receipt) else 0,
confirmed_by=receipt.confirmed_by,
confirmed_at=receipt.confirmed_at,
confirmation_revision=receipt_confirmation.revision(receipt),
payment_days=(
payment_days(ship_date, receipt.receipt_date) if ship_date is not None else None
),
@@ -151,6 +155,33 @@ def get_detail(session: Session, receipt_id: int) -> ReceiptOut:
return _to_out(row)
def confirm_receipt(session, receipt_id, payload, *, operator, meta=None) -> ReceiptOut:
"""独立确认已核对内容;锁内复核指纹,重复确认不改首次确认人/时间。"""
from app.services.authorization_service import require_permission
require_permission(session, operator, "business:receipts:confirm")
with audit_service.audit(
session, operator=operator,
record=AuditRecord(module=MODULE_RECEIPT, action="人工确认", summary=f"人工确认回款 #{receipt_id}"),
meta=meta,
):
receipt = receipt_repository.get_by_id(session, receipt_id)
if receipt is None:
raise NotFoundError("回款不存在")
_ensure_business_writable(session, receipt.business_id, "确认")
session.refresh(receipt, with_for_update=True)
if payload.expected_revision != receipt_confirmation.revision(receipt):
raise ConflictError("回款内容已变化,请重新查看并核对后确认")
if not receipt_confirmation.is_confirmed(receipt):
receipt.confirmation_status = 1
receipt.confirmed_by = operator.username
receipt.confirmed_at = datetime.now()
receipt.confirmed_revision = payload.expected_revision
out = get_detail(session, receipt_id)
session.commit()
return out
@dataclass(frozen=True, slots=True)
class _CreatedReceipt:
"""建行结果(出参 + 固化的 ID/批次号,commit 后不再触碰 ORM 属性)。"""
@@ -296,9 +327,13 @@ def update_receipt(
):
if receipt is None:
raise NotFoundError("回款不存在")
from app.services.business_source import ensure_source_editable
ensure_source_editable(session, "receipts", receipt_id)
business = _ensure_business_writable(session, receipt.business_id, "修改")
session.refresh(receipt, with_for_update=True)
if receipt.settlement_status == SETTLEMENT_DONE:
raise BadRequestError("已核算回款禁止修改")
business = _ensure_business_writable(session, receipt.business_id, "修改")
# 月结锁定拦截:原月与新月都不得落入已锁定月份(改到账日=跨月搬数,
# 只查新月会让「从锁定月改到未锁定月」绕过拦截)
from app.services import settlement_service
@@ -311,11 +346,14 @@ def update_receipt(
session, payload.shipment_id, receipt.business_id
)
old_revision = receipt_confirmation.revision(receipt)
receipt.receipt_date = payload.receipt_date
receipt.shipment_id = payload.shipment_id
receipt.amount = Decimal(payload.amount)
receipt.is_barter = payload.is_barter
receipt.remark = payload.remark
if receipt_confirmation.revision(receipt) != old_revision:
receipt_confirmation.invalidate(receipt)
_recalc_totals(session, business)
out = _to_out((receipt, business.business_no, shipment_no, ship_date))
batch_no = receipt.receipt_batch_no
@@ -353,6 +391,9 @@ def delete_receipt(
):
if receipt is None:
raise NotFoundError("回款不存在")
from app.services.business_source import ensure_source_editable
ensure_source_editable(session, "receipts", receipt_id)
if receipt.settlement_status == SETTLEMENT_DONE:
raise BadRequestError("已核算回款禁止删除")
business = _ensure_business_writable(session, receipt.business_id, "删除")