Files
uniprop-manual/prop-acc/scenarios/billing/void-paid-bill.md
2026-05-26 01:13:17 +08:00

8.9 KiB

title, aliases, tags, audience, status, sub_feature, last_review, code_version
title aliases tags audience status sub_feature last_review code_version
prop-acc · billing · 场景 - 作废已付账单(走作废 + 退款)
作废账单
VoidBillAction
void-paid-bill
作废加退款
场景-作废已付账单
场景
prop-acc
账单
作废
业务人员
财务
已发布 billing 2026-05-26 2026-05-22

场景:作废已付账单(走作废 + 退款)

业户已付的账单需要消除(误开账单业户付了 / 业户事后投诉成功 / 调解结果物业认错)。走作废 + 退款组合,留状态留审计 + 退还业户。

[!warning] 当前实施状态 VoidBillAction 本身只翻状态 + 留 meta + 写 activitylog(canBeVoided=true for 非 Paid)。

Paid 状态 canBeVoided=false,意味着 VoidBillAction 不直接处理 Paid 账单的作废。需要走类似 meter 的修正流程:

  • 当前手工 / tinker(运维操作)
  • 未来扩展 VoidPaidBillAction 自动化(包含退款 + 红字 CO 等)

详见 delete-vs-void-dual-track"canBeVoided 的微妙之处"段。

典型情境

情境 1:Partial 状态作废(系统支持)

[!example] 真实情境 陈先生 5 月物业费 ¥800,部分付了 ¥300(状态 Partial)。后续物业承认服务有问题,双方协议作废账单,退 ¥300 给业户。

业务流程:

  1. VoidBillAction(Partial → Void)
  2. 手工配套退款(给陈先生退 ¥300 现金 / 微信)
  3. (理想)系统自动建红字 CollectionOrder(待扩展)

情境 2:Paid 状态作废(当前需手工)

[!example] 真实情境 张阿姨 5 月物业费 ¥800 已付清(Paid)。物业发现金额算错了(应该 ¥600),多收 ¥200。要全额作废 + 退还 ¥800 + 重新建一张 ¥600 账单。

当前流程(因 canBeVoided=false for Paid,VoidBillAction 不允许):

  1. 运维 tinker 操作:把 Bill 状态强制改为 Void + 记 meta
  2. 手工建红字 CollectionOrder(¥-800)+ Receipt(红字)
  3. 物业线下退款 ¥800 给业户
  4. 手工建新 Bill ¥600
  5. 业户付 ¥600

整个流程复杂、易出错、无 UI。issue.md Q6 未明确实施时间,标"待业务方明确"。

业务人员视角(Partial 作废)

第 1 步:确认作废

  • 协议作废(业户与物业书面达成)
  • 调解 / 司法判决物业方有责
  • 业务上的特殊情况

第 2 步:打开账单

后台 → 账单 → 找到 Partial Bill → 进 ViewBill

第 3 步:点击 VoidBillAction(标签"作废")

[!warning] 按钮可见性 守护:canBeVoided() = 非 Paid 非 Void + ->authorize('void')(bill.void 权限)。

Modal 表单:

字段 填什么
作废原因(reason) 必填,如 "调解结果:物业服务质量有问题,账单作废 + 退还已付款"

第 4 步:提交

VoidBillAction 业务逻辑(详见 delete-vs-void-dual-track):

$bill->update([
    'status' => BillStatus::Void,
    'meta' => array_merge($bill->meta ?? [], [
        'voided_reason' => $reason,
        'voided_at' => now(),
        'voided_by' => $user->id,
    ]),
]);

activity()->performedOn($bill)->causedBy($user)
    ->withProperties([
        'reason' => $reason,
        'from_status' => $bill->getOriginal('status'),
        'to_status' => 'Void',
        'bill_no' => $bill->bill_no,
        'amount' => $bill->amount,
        'paid_amount' => $bill->paid_amount,  // 关键:有付款需要后续退
    ])
    ->event('voided')
    ->log('账单已作废');

第 5 步:手工退款(若 paid_amount > 0)

VoidBillAction 本身不退钱。业务人员后续:

  1. 看 activitylog 的 paid_amount 字段 = ¥300
  2. 联系业户确认退款方式(微信 / 现金 / 银行转账)
  3. 走线下退款(物业财务实际打钱)
  4. 理想:建红字 CollectionOrder(actual_amount=-300,type=Bill,关联到该 Bill)+ Receipt 红字(待扩展自动化)

[!info] 当前简化做法 Bill 作废后:

  • Bill 状态 = Void
  • CollectionOrderBill 关联不动(审计需要)
  • 业务人员线下手工退款给业户
  • 系统中没有红字 CO / 红字 Receipt(待 VoidPaidBillAction 自动化)

财务账面临时不一致(Bill 是 Void 但 CO 仍是 Completed),需事后人工对账修复。

业务方提需求时,优先级会上来。

第 6 步:通知业户

陈先生您好,您的 5 月物业费账单已作废,已付的 ¥300 我们将退还您。请确认收款方式。

业务人员视角(Paid 作废,当前 tinker 流程)

详见上方"情境 2"。当前没自动化:

  1. 评估业务必要性(确认无误后操作)
  2. 联系运维 tinker:
    $bill->update([
      'status' => BillStatus::Void,
      'meta' => array_merge($bill->meta ?? [], [
        'voided_reason' => $reason,
        'voided_at' => now(),
        'voided_by' => $user->id,
        'manual_void' => true,  // 标记是 tinker 手工作废
      ]),
    ]);
    
  3. 走完整退款流程(线下退 + 系统记一笔红字 CO/Receipt 若可能)
  4. 重建账单(走 create-single-bill-manual)
  5. 业户付新账单

系统流程

sequenceDiagram
    participant 业务
    participant Filament
    participant Action[VoidBillAction]
    participant DB
    participant Activity

    业务->>Filament: ViewBill(Partial)→ VoidBillAction
    Filament->>Action: handle(bill, reason, user)
    Action->>Action: 校验 canBeVoided(非 Paid 非 Void)
    Action->>DB: 1. bill.status = Void + meta
    Action->>Activity: 2. log(event=voided, paid_amount=300)
    Filament-->>业务: 成功

    业务->>业务: 看 activitylog paid_amount=300
    业务->>业户: 线下退款 + 通知

    Note over Action: 注:不自动建红字 CO/Receipt
    Note over Action: 待 VoidPaidBillAction 实现

业户视角

您会感受到什么

  • 收到通知"您的账单 #XXX 已作废,理由 XXX"
  • 已付的钱未来几天收到退款
  • 收到收据备注(若系统支持)
  • 小程序账单状态:Partial → Void

您要做什么

  • 确认收款方式
  • 接收退款(银行 / 微信 / 现金)
  • 如有疑问联系物业

与 prop-acc 其他作废的对比

模块 作废 / void 机制
billing(本) VoidBillAction(非 Paid 非 Void)+ 手工退款配套(Paid 待扩展)
deposit 无单独 void;用 [[../deposit/force-close-refund
prepaid 无 void;走 [[../prepaid/refund-partial-after-consume
meter 无 void(reading 不可改,见 ../meter/exception-readings-locked-after-bill)
adhoc 走 [[../adhoc/cancel-amount-error-redo

bill 的 void 是最完整的"账单作废"设计,但 Paid 的作废仍待补(整个 prop-acc 通用问题:已收款的"反向"流程都不够成熟)。

已知限制(issue.md Q6 待补)

  • VoidBillAction 不处理 Paid 状态:canBeVoided=false,需走专门流程(未实现)
  • 作废后红字 CO + Receipt 自动生成:未实现,需手工
  • 退款金额自动算 / 自动建红字凭证:未实现
  • BulkRefundBillsAction(批量退款):未实现,issue.md 标优先级低

常见问题

[!question] 作废后业户已付款怎么记账? 当前(简版):

  • Bill 状态 = Void
  • CollectionOrderBill 关联保留(allocated_amount=300)
  • 物业账面"已收 ¥300"(从 CO 角度)= 实际"应退给业户"

财务月度对账时需人工识别这种情况(账上有钱但 Bill 已 void = 待退款)。

未来自动化后:作废 + 同时建红字 CO(¥-300)抵消原 CO,账面归零。

[!question] 作废能撤销吗? 不能(Void 是终态,详见 bill-six-state-machine)。如需"恢复":新建一张同信息 Bill。

[!question] 已付 + 已被预存款抵的 Bill 作废,怎么退到预存款? 自动化未实现。手工流程:

  1. tinker 作废 Bill
  2. 给业户预存款手工 deposit(走 ../prepaid/deposit-additional-topup 把 ¥800 充回预存款)
  3. 备注"账单作废退还"

系统层面:理想是自动反向(PrepaidAccount::reverseConsume 之类),未实现。

[!question] activitylog 怎么查作废历史?

SELECT * FROM activity_log
WHERE event = 'voided'
  AND properties->>'$.bill_no' = ?
ORDER BY created_at DESC;

[!question] 业户对作废结果不满意? 已作废不可逆。业户:

  • 走司法 / 仲裁
  • 业务方重新协商(可能再建新账单)

异常分支

  • Unpaid 无付款 → delete-bill-unpaid 更干净
  • 批量作废 → bulk-delete-batch-mistake 选 DeleteAndVoid 模式
  • 业户拒绝接受作废 → 协商 / 司法
  • Paid 作废自动化 → 待业务方明确 + 实施 VoidPaidBillAction

相关文档