ERP销售单已经作废,删除商品时仍提示"该资料已被单据引用,不能删除",可以继续检查引用查询是否包含已作废单据。当前项目在删除基础资料时会扫描仍保留的业务记录。
本文使用自有进销存工作台实际Store.prepare与execute,在独立内存SQLite中创建演示客户、商品和销售单,再做过账、作废及删除尝试。所有写入只发生在构造内存库,真实客户库与外部ERP没有参与。
1. 删除入口检查哪些引用
execute处理客户、供应商和商品的delete操作时,读取当前租户的销售、采购和收付款记录,再查找相关ID。
javascript
const refs = ['sales', 'purchases', 'payments']
.flatMap(e => this.list(s.tenant, e));
if (refs.some(r =>
[r.customerId, r.supplierId, r.partyId].includes(c.id) ||
r.items?.some(i => i.productId === c.id)
)) {
fail('该资料已被单据引用,不能删除');
}
list限定租户、业务类型和deleted为0。上面的引用条件没有继续按已过账或有效状态筛选,所以仍保留的草稿、已作废单据,也可能进入引用判断。
实施排查应读取实际关联ID及记录状态。商品名称相同、库存已经恢复或单据页面显示作废,都不能代替这段引用查询的结果。
2. 用三份独立内存样本对照
每份样本先创建库存5的构造商品,再按不同条件尝试删除商品。第三份样本先创建销售单、过账扣减1,再作废恢复库存。
| 构造条件 | 销售单状态 | 删除结果 | 商品deleted | 商品列表可见数 |
|---|---|---|---|---|
| 商品未被单据引用 | 无销售单 | 执行完成 | 1 | 0 |
| 商品被销售草稿引用 | 草稿 | 提示已被引用 | 0 | 1 |
| 引用销售单已过账再作废 | 已作废 | 仍提示已被引用 | 0 | 1 |
后两份样本的商品库存均为5。作废样本恢复库存后,订单明细中的productId仍保留,所以引用检查继续命中。
本次实际执行了内存数据库写入与查询,表里的删除标记和列表结果分别回读。这个结果只适用于当前自有实现,其他ERP的资料停用、删除和历史单据规则需要单独核对。

图1依据真实Store与内存SQLite构造结果绘制,商品、库存和销售单均为演示数据。
3. 作废路径保留了哪些内容
当前void对订单进行反向库存变动,然后保存已作废状态。它保留订单主体和明细数组,商品关联仍可供查询。
因此,库存恢复与资料能否删除由不同条件决定。排查可以先读订单状态及明细,再沿productId找到商品,最后确认删除操作在哪一步报错。
本文只验证销售单引用商品的样本。客户、供应商和收付款引用条件来自源码阅读,本次没有分别构造这些组合。
4. 准备删除成功后,最终确认仍可能失败
样本调用prepare创建删除待确认操作时能够返回pending,真正的引用检查位于execute。商品被引用的两个样本最终确认失败,待确认操作继续保持pending。
所以核验时应同时保存准备结果与执行结果。若界面只展示"待确认已生成",还要继续读取最终操作结果及商品记录。
执行期间处于事务内,引用检查抛出错误后回滚。本文没有模拟准备与确认之间新增引用的并发情况,该场景仍需要额外验收。
5. 未引用资料的删除标记怎样核对
无引用样本删除完成后,records表中的商品行仍存在,deleted变为1,常规商品列表返回0条。当前实现采用软删除,列表读取会过滤这个标记。
这解释了"列表里找不到,底层表里还有行"的情况。排查时保持同一租户、业务类型及记录ID,分别看常规列表和删除标记。
建议把资料停用、历史引用保留和可删除条件写成清晰业务规则,再按规则设计入口。本文未新增停用功能,也未修改引用保护逻辑。
本次确认了无引用商品软删除、草稿引用阻断、销售单作废后仍保留引用三个边界。历史数据清理、跨模块全量引用检查和生产环境删除操作均属未验证范围。