# 发货模块改造:部分发货 + 批量发货 + 解除完工限制 ## 背景 发货模块本次改造三项能力: 1. **部分发货**:系统不再强制要求订单 100% 完工才可发货,发货弹窗可输入"本次发货数量",审批通过后仅按该数量扣减库存。 2. **解除发货按钮锁定**:不再因产品无库存记录 / 产量不足(未完工)锁定"发货"按钮,只要存在剩余可发数量即可发起发货。 3. **批量发货**:支持同一客户多条订单勾选后批量发货,无需逐条操作。 ## 涉及页面 - 销售台账列表页(`salesLedger/index.vue`)— 发货弹窗、批量发货入口 - 发货信息管理页(`deliveryLedger/index.vue`)— 发货数量展示 ## API | 方法 | 路径 | 说明 | |------|------|------| | POST | /shippingInfo/add | 创建发货单并发起发货审批(新增本次发货数量参数) | | POST | /shippingInfo/batchAdd | 批量发货:同一客户多条订单,逐条创建发货单并发起审批 | | GET | /sales/product/list | 产品明细列表(响应新增已发数量、剩余可发数量) | ### 1. POST /shippingInfo/add(请求参数变更) **请求参数:** 在原有字段基础上新增 `quantity`(本次发货数量)。 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | salesLedgerProductId | Long | 是 | 销售台账产品明细ID | | quantity | BigDecimal | 否 | 本次发货数量,不传时默认取产品明细全量数量 | | type | String | 否 | 审批备注前缀(如 部分发货/批量发货) | | approveUserIds | Array | 否 | 审批人ID列表 | | tempFileIds | Array | 否 | 临时附件ID列表 | | expressNumber | String | 否 | 快递单号 | | expressCompany | String | 否 | 快递公司 | | shippingCarNumber | String | 否 | 发货车牌号 | | shippingDate | String | 否 | 发货日期 | **校验规则:** `quantity` 必须大于 0 且不超过"剩余可发数量",否则接口返回错误。 **响应:** `{ "code": 200, "msg": "操作成功" }` ### 2. POST /shippingInfo/batchAdd(新增接口) **请求体:** 数组,元素为发货单数据(字段同 `/shippingInfo/add`)。 ```json [ { "salesLedgerId": 1, "salesLedgerProductId": 100, "quantity": 10, "type": "批量发货" }, { "salesLedgerId": 2, "salesLedgerProductId": 200, "quantity": 5, "type": "批量发货" } ] ``` **校验规则:** 所有产品明细所属销售台账必须为**同一客户**,否则返回错误"批量发货仅支持同一客户的多条订单"。每条数据独立生成发货编号并独立发起审批。 **响应:** `{ "code": 200, "msg": "操作成功" }` ### 3. GET /sales/product/list(响应新增字段) 响应中每条产品明细新增两个字段: | 字段 | 类型 | 说明 | |------|------|------| | alreadyShippedQuantity | BigDecimal | 已发数量(状态为"审核通过 + 已发货"的发货记录数量之和) | | unshippedQuantity | BigDecimal | 剩余可发数量 = 产品明细数量 − 已发数量 | ## 前端修改点 ### 1. 解除发货按钮锁定(canShip) `canShip` 原逻辑要求 `hasStockInventory`(产品必须有库存记录)且发货状态为"待发货/审核拒绝",导致未完工/产量不足时发货按钮被锁定。改造后**移除 `!row.hasStockInventory` 限制**,改为判断"剩余可发数量 > 0"。 ```js // 原逻辑 canShip(row) { return row.hasStockInventory && (row.shippingStatus === '待发货' || row.shippingStatus === '审核拒绝'); } // 新逻辑:解除完工/库存锁定,仅需存在剩余可发数量 canShip(row) { return row.unshippedQuantity > 0 && (row.shippingStatus === '待发货' || row.shippingStatus === '审核拒绝'); } ``` ### 2. 发货弹窗新增"本次发货数量" 发货弹窗(`deliveryForm`)新增"本次发货数量"输入框,默认值为 `unshippedQuantity`(剩余可发数量),并做上限校验。 ```html 剩余可发:{{ deliveryForm.unshippedQuantity }} ``` ```js // data 中初始化 deliveryForm: { salesLedgerProductId: null, salesLedgerId: null, quantity: null, // 本次发货数量 unshippedQuantity: 0, // 剩余可发数量(打开弹窗时赋值) type: '部分发货', approveUserIds: [], tempFileIds: [], }, // 打开发货弹窗 openDelivery(row) { this.deliveryForm = { salesLedgerProductId: row.id, salesLedgerId: row.salesLedgerId, quantity: row.unshippedQuantity, // 默认取剩余可发数量 unshippedQuantity: row.unshippedQuantity, type: '部分发货', approveUserIds: [], tempFileIds: [], }; this.deliveryOpen = true; }, // 提交发货 submitDelivery() { const qty = Number(this.deliveryForm.quantity); if (!qty || qty <= 0) { this.$modal.msgError('本次发货数量必须大于 0'); return; } if (qty > Number(this.deliveryForm.unshippedQuantity)) { this.$modal.msgError('本次发货数量不能超过剩余可发数量'); return; } addShipping(this.deliveryForm).then(res => { if (res.code === 200) { this.$modal.msgSuccess('发货成功,已发起审批'); this.deliveryOpen = false; this.getList(); // 刷新 } }); } ``` ### 3. 批量发货按钮 + 弹窗 主表已有 `type="selection"` 复选框。新增"批量发货"按钮,勾选**同一客户**的多条订单后点击,弹出批量发货弹窗。 ```html 批量发货 ``` ```js data() { return { selectedLedgers: [], // 勾选的销售台账(订单) batchShippingOpen: false, // 批量发货弹窗开关 batchShippingRows: [], // 可发货产品行列表 }; }, // 勾选变化 handleSelectionChange(selection) { this.selectedLedgers = selection; }, // 批量发货:校验同一客户后拉取可发产品行 async handleBatchShipping() { if (!this.selectedLedgers.length) { this.$modal.msgWarning('请先勾选要发货的订单'); return; } // 同一客户校验(后端也会校验) const customerNames = [...new Set(this.selectedLedgers.map(l => l.customerName))]; if (customerNames.length > 1) { this.$modal.msgError('批量发货仅支持同一客户的多条订单'); return; } // 拉取各订单的产品明细,收集剩余可发数量 > 0 的行 this.batchShippingRows = []; for (const ledger of this.selectedLedgers) { const res = await productList({ salesLedgerId: ledger.id, type: 1 }); const rows = (res.data || []).filter(r => Number(r.unshippedQuantity) > 0); rows.forEach(r => { r.salesLedgerId = ledger.id; r.shipQuantity = r.unshippedQuantity; // 默认本次发货数量 = 剩余可发 }); this.batchShippingRows = this.batchShippingRows.concat(rows); } if (!this.batchShippingRows.length) { this.$modal.msgWarning('所选订单无剩余可发货产品'); return; } this.batchShippingOpen = true; }, // 批量提交 submitBatchShipping() { // 逐行校验数量 for (const row of this.batchShippingRows) { const qty = Number(row.shipQuantity); if (!qty || qty <= 0) { this.$modal.msgError('存在本次发货数量为空或为 0 的行'); return; } if (qty > Number(row.unshippedQuantity)) { this.$modal.msgError('存在超过剩余可发数量的行'); return; } } const reqs = this.batchShippingRows.map(r => ({ salesLedgerId: r.salesLedgerId, salesLedgerProductId: r.id, quantity: r.shipQuantity, type: '批量发货', approveUserIds: [], })); batchAdd(reqs).then(res => { if (res.code === 200) { this.$modal.msgSuccess('批量发货成功,已发起审批'); this.batchShippingOpen = false; this.getList(); } }); } ``` 批量发货弹窗表格: ```html ``` ### 4. 发货信息管理页展示 `deliveryLedger/index.vue` 列表建议展示本次发货数量列(对应后端 `shipping_info.quantity`),区分于订单数量: ```html ``` ## 注意事项 - **剩余可发数量口径**:仅"审核通过 + 已发货"状态发货单占用数量;"待审核 / 审核中 / 审核拒绝"不占数量,防止超发。 - **批量发货的客户校验**:前端与后端均有校验,仅支持同一客户多条订单。 - **部分发货扣库存**:审批通过后按"本次发货数量"扣减库存,而非订单全量。 - **删除产品明细**:会级联删除其发货单,剩余可发数量随之恢复。 - **历史数据**:存量发货单已由数据库迁移回填 `quantity`(等于对应产品明细数量),旧数据剩余可发数量显示不受影响。