提交
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
@@ -0,0 +1,90 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1800" height="1280" viewBox="0 0 1800 1280">
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0,0 L10,3 L0,6 Z" fill="#335c81"/></marker>
|
||||||
|
<marker id="greenArrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0,0 L10,3 L0,6 Z" fill="#18865b"/></marker>
|
||||||
|
<marker id="redArrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0,0 L10,3 L0,6 Z" fill="#c94d4d"/></marker>
|
||||||
|
<style>
|
||||||
|
.title{font:700 34px 'Microsoft YaHei','Noto Sans SC',sans-serif;fill:#17324d}
|
||||||
|
.sub{font:400 17px 'Microsoft YaHei','Noto Sans SC',sans-serif;fill:#547086}
|
||||||
|
.lane{font:700 20px 'Microsoft YaHei','Noto Sans SC',sans-serif;fill:#17324d}
|
||||||
|
.boxTitle{font:700 19px 'Microsoft YaHei','Noto Sans SC',sans-serif;fill:#17324d}
|
||||||
|
.boxText{font:400 16px 'Microsoft YaHei','Noto Sans SC',sans-serif;fill:#36566d}
|
||||||
|
.small{font:400 14px 'Microsoft YaHei','Noto Sans SC',sans-serif;fill:#547086}
|
||||||
|
.assert{font:500 16px 'Microsoft YaHei','Noto Sans SC',sans-serif;fill:#23445a}
|
||||||
|
.line{stroke:#335c81;stroke-width:3;fill:none;marker-end:url(#arrow)}
|
||||||
|
.gline{stroke:#18865b;stroke-width:3;fill:none;marker-end:url(#greenArrow)}
|
||||||
|
.rline{stroke:#c94d4d;stroke-width:3;fill:none;marker-end:url(#redArrow)}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect width="1800" height="1280" fill="#f6f9fc"/>
|
||||||
|
<text x="70" y="62" class="title">海外业务|库存变动测试总流程图</text>
|
||||||
|
<text x="70" y="94" class="sub">测试看库存:先看库存状态,再看数量、位置、流水和幂等;灰色节点只改状态/资源,不直接改可用库存。</text>
|
||||||
|
|
||||||
|
<g transform="translate(70,125)">
|
||||||
|
<rect width="1660" height="58" rx="12" fill="#e9f1f8"/>
|
||||||
|
<circle cx="28" cy="29" r="10" fill="#18865b"/><text x="50" y="35" class="small">绿色:库存增加/恢复</text>
|
||||||
|
<circle cx="250" cy="29" r="10" fill="#c94d4d"/><text x="272" y="35" class="small">红色:库存扣减</text>
|
||||||
|
<circle cx="450" cy="29" r="10" fill="#e19a28"/><text x="472" y="35" class="small">橙色:冻结/释放</text>
|
||||||
|
<circle cx="690" cy="29" r="10" fill="#7657a8"/><text x="712" y="35" class="small">紫色:SKU/箱唛变化</text>
|
||||||
|
<circle cx="960" cy="29" r="10" fill="#78909c"/><text x="982" y="35" class="small">灰色:状态/资源变化</text>
|
||||||
|
<text x="1260" y="35" class="small">所有写操作:租户隔离 + 版本/锁 + 幂等键</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(70,220)">
|
||||||
|
<rect width="1660" height="196" rx="16" fill="#edf5ee" stroke="#b7d9c2" stroke-width="2"/>
|
||||||
|
<text x="24" y="34" class="lane">A. 库存来源与入仓</text>
|
||||||
|
<rect x="24" y="58" width="240" height="96" rx="12" fill="#fff" stroke="#6ea984" stroke-width="2"/>
|
||||||
|
<text x="44" y="88" class="boxTitle">头程装柜完成事件</text><text x="44" y="116" class="boxText">生成尾程单 / 冻结明细</text><text x="44" y="140" class="small">在途:不可用库存</text>
|
||||||
|
<path d="M264 106 H330" class="line"/>
|
||||||
|
<rect x="330" y="58" width="250" height="96" rx="12" fill="#fff" stroke="#6ea984" stroke-width="2"/>
|
||||||
|
<text x="350" y="88" class="boxTitle">平台转换入仓计划</text><text x="350" y="116" class="boxText">按入仓单 / 推荐托数建计划</text><text x="350" y="140" class="small">仍未形成可用库存</text>
|
||||||
|
<path d="M580 106 H646" class="line"/>
|
||||||
|
<rect x="646" y="58" width="290" height="96" rx="12" fill="#fff" stroke="#18865b" stroke-width="3"/>
|
||||||
|
<text x="666" y="88" class="boxTitle">PDA 签收 → 实收 → 上架</text><text x="666" y="116" class="boxText">完成后入仓库存入账</text><text x="666" y="140" class="small">AVAILABLE + 位置 + 库存流水</text>
|
||||||
|
<path d="M936 106 H1002" class="gline"/>
|
||||||
|
<rect x="1002" y="58" width="280" height="96" rx="12" fill="#e6f6ed" stroke="#18865b" stroke-width="2"/>
|
||||||
|
<text x="1022" y="88" class="boxTitle">可用库存池</text><text x="1022" y="116" class="boxText">客户 × 海外仓 × 目的仓</text><text x="1022" y="140" class="small">入仓单汇总;托盘只做作业维度</text>
|
||||||
|
<rect x="1316" y="58" width="310" height="96" rx="12" fill="#f0f2f4" stroke="#9baab4" stroke-width="2"/>
|
||||||
|
<text x="1336" y="88" class="boxTitle">门禁与失败</text><text x="1336" y="116" class="boxText">照片、仓绑定、客户/柜号</text><text x="1336" y="140" class="small">任一失败:库存与状态整体回滚</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(70,450)">
|
||||||
|
<rect width="1660" height="222" rx="16" fill="#fff8ec" stroke="#efd39a" stroke-width="2"/>
|
||||||
|
<text x="24" y="34" class="lane">B. 从可用库存出发的四类作业</text>
|
||||||
|
<rect x="24" y="60" width="360" height="126" rx="12" fill="#fff" stroke="#dfa43c" stroke-width="2"/>
|
||||||
|
<text x="44" y="88" class="boxTitle">排车 / 一件代发拣货</text><text x="44" y="116" class="boxText">AVAILABLE → 冻结 → PICKED</text><text x="44" y="144" class="boxText">发车/出库 → 扣减</text><text x="44" y="170" class="small">整托扫描也按入仓单组成核平</text>
|
||||||
|
<rect x="430" y="60" width="360" height="126" rx="12" fill="#fff" stroke="#7657a8" stroke-width="2"/>
|
||||||
|
<text x="450" y="88" class="boxTitle">换标</text><text x="450" y="116" class="boxText">冻结 hold_packages</text><text x="450" y="144" class="boxText">旧 SKU 转出 → 新 SKU 转入</text><text x="450" y="170" class="small">箱唛/托盘绑定变化,箱数守恒</text>
|
||||||
|
<rect x="836" y="60" width="360" height="126" rx="12" fill="#fff" stroke="#dfa43c" stroke-width="2"/>
|
||||||
|
<text x="856" y="88" class="boxTitle">仓储转运</text><text x="856" y="116" class="boxText">来源仓冻结/扣减</text><text x="856" y="144" class="boxText">目标仓收货 → 增加</text><text x="856" y="170" class="small">可联动换标;来源/目标核对</text>
|
||||||
|
<rect x="1242" y="60" width="384" height="126" rx="12" fill="#fff" stroke="#c94d4d" stroke-width="2"/>
|
||||||
|
<text x="1262" y="88" class="boxTitle">废弃</text><text x="1262" y="116" class="boxText">申请仅锁箱:DISCARD_PENDING</text><text x="1262" y="144" class="boxText">PDA 实际确认 → 扣减 → DISCARDED</text><text x="1262" y="170" class="small">驳回释放;平台结案不再扣一次</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(70,706)">
|
||||||
|
<rect width="1660" height="222" rx="16" fill="#eef3fa" stroke="#b7cae2" stroke-width="2"/>
|
||||||
|
<text x="24" y="34" class="lane">C. 出库后的结果与恢复</text>
|
||||||
|
<rect x="24" y="60" width="350" height="126" rx="12" fill="#fff" stroke="#c94d4d" stroke-width="2"/>
|
||||||
|
<text x="44" y="88" class="boxTitle">OUTBOUNDED / 派送中</text><text x="44" y="116" class="boxText">库存已扣减,保留出库事实</text><text x="44" y="144" class="boxText">签收:不再二次扣库存</text><text x="44" y="170" class="small">派送费用与库存动作分开核对</text>
|
||||||
|
<path d="M374 123 H446" class="line"/>
|
||||||
|
<rect x="446" y="60" width="380" height="126" rx="12" fill="#fff" stroke="#18865b" stroke-width="3"/>
|
||||||
|
<text x="466" y="88" class="boxTitle">异常退回入仓</text><text x="466" y="116" class="boxText">只能选原出库拣货事实</text><text x="466" y="144" class="boxText">RETURN_STOCK_IN → 目标托盘/库位</text><text x="466" y="170" class="small">库存恢复 + 异常完成 + 派送 RETURNED</text>
|
||||||
|
<rect x="900" y="60" width="330" height="126" rx="12" fill="#fff" stroke="#78909c" stroke-width="2"/>
|
||||||
|
<text x="920" y="88" class="boxTitle">盘点调整</text><text x="920" y="116" class="boxText">实盘数量 − 账面数量 = 差异</text><text x="920" y="144" class="boxText">按正/负生成调整流水</text><text x="920" y="170" class="small">无差异不造流水;需 adjust 权限</text>
|
||||||
|
<rect x="1270" y="60" width="356" height="126" rx="12" fill="#fff" stroke="#78909c" stroke-width="2"/>
|
||||||
|
<text x="1290" y="88" class="boxTitle">移库 / 拆合托 / 换绑</text><text x="1290" y="116" class="boxText">同仓位置或托盘组成变化</text><text x="1290" y="144" class="boxText">数量净额 = 0</text><text x="1290" y="170" class="small">记录位置流水;资源锁防并发</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path d="M1142 416 V450" class="gline"/><path d="M120 638 V706" class="rline"/><path d="M610 638 V706" class="gline"/>
|
||||||
|
<path d="M1020 638 V706" class="line"/><path d="M1435 638 V706" class="rline"/>
|
||||||
|
|
||||||
|
<g transform="translate(70,965)">
|
||||||
|
<rect width="1660" height="250" rx="16" fill="#ffffff" stroke="#d5e0e8" stroke-width="2"/>
|
||||||
|
<text x="24" y="36" class="lane">D. 测试统一断言(每个库存场景都要检查)</text>
|
||||||
|
<text x="42" y="78" class="assert">① 数量守恒:客户 / 海外仓 / 目的仓 / 入仓单 / 托盘组成逐层核平</text>
|
||||||
|
<text x="42" y="114" class="assert">② 三账一致:库存余额 = 位置明细汇总 = 库存流水结果</text>
|
||||||
|
<text x="42" y="150" class="assert">③ 幂等并发:重复事件、重复请求、并发锁定不得产生第二笔变动</text>
|
||||||
|
<text x="42" y="186" class="assert">④ 原子回滚:权限、门禁、资源锁、跨服务失败时,库存/单据/流水一起回滚</text>
|
||||||
|
<text x="42" y="222" class="assert">⑤ 隔离校验:客户、仓、来源入仓单、原出库事实不可伪造或串用</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
# 海外业务库存变动测试整理
|
||||||
|
|
||||||
|
范围依据:`lastleg-service/AGENTS.md`、`docs/backend-business-index.md` 及当前海外仓库存、入仓、出库、换标、转运、废弃、异常退回和 PDA 作业入口。
|
||||||
|
|
||||||
|
## 先统一库存口径
|
||||||
|
|
||||||
|
- **可用库存**:按客户、海外仓、目的仓、入仓单汇总箱数;SKU、箱唛和托盘是明细快照/作业维度。
|
||||||
|
- **冻结库存**:拣货、换标、废弃、转运等处理中暂时不可重复使用;冻结不等于已经扣减。
|
||||||
|
- **库存流水**:每一次入账、冻结、释放、扣减、恢复、SKU 转换和位置变化都应能按业务单号、幂等键追溯。
|
||||||
|
- **托盘不是库存归属**:合托后一个物理托盘可以包含多个入仓单,出库、换标和退回必须按托盘组成明细核平。
|
||||||
|
|
||||||
|
## 测试场景矩阵
|
||||||
|
|
||||||
|
| 场景 | 触发与前置 | 库存变动 | 测试重点 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 头程接柜/海外入仓 | 装柜完成事件 → 生成尾程单/入仓计划;PDA 签收、实收、上架、完成 | 在途冻结 → 入仓后 `AVAILABLE` 增加;写库存位置和流水 | 柜号/客户/入仓单隔离;卸柜照片门禁;重复事件不重复入账;失败整事务回滚 |
|
||||||
|
| 普通排车出库/派送 | 计划已入仓;排车审核、拣货、箱数核平、发车 | `AVAILABLE` → 冻结/已拣;原子出库后可用库存扣减 | 整托/部分托按组成明细核平;未审核、数量不平、车辆门禁时不得出库;同键重放幂等 |
|
||||||
|
| 一件代发 | 已完成入库且有可用箱唛/SKU;创建 `WAIT_PICK` 出库单 | SKU/箱唛库存冻结 → PDA 拣货 → `OUTBOUNDED` 扣减 | 候选只能来自真实可用库存;一单不得跨客户/跨仓;整托扫描任一明细失败必须整体回滚 |
|
||||||
|
| 客户换标 | 历史入仓库存可用;客户申请、PDA 扫旧 SKU/箱唛、完工、客户确认 | 旧 SKU/箱唛冻结;换标完成后旧库存转出、新 SKU/箱唛转入;库存总箱数守恒 | 同箱多 SKU 不重复冻结;只移动本单 `hold_packages`;已生成出库单禁止再次拆托;照片/复核/客户确认门禁 |
|
||||||
|
| 仓储转运 | 来源库存可用;创建转运、测量、换标(可选)、出库、目标仓收货 | 来源仓冻结/扣减 → 目标仓入账;换标时 SKU 转换 | 来源客户、入仓单和箱数核对;部分转运数量守恒;目标仓与来源仓不能串;重复完成不重复入账 |
|
||||||
|
| 货物废弃 | 一件代发/库存货物申请废弃;审批后由绑定海外 PDA 实际确认 | `AVAILABLE` → `DISCARD_PENDING`;实际确认后 SKU、余额、位置扣减并记 `DISCARDED` | 申请阶段只锁箱不扣库存;驳回释放;批准不是实际扣减;实际数量与逐箱流水一致;失败整批回滚 |
|
||||||
|
| 异常退回入仓 | 只能选择原出库拣货事实;异常单受理并完成退回 | 原出库来源核验 → `RETURN_STOCK_IN` 恢复到真实目标托盘/库位 | 不能伪造出库来源、借用其他客户主单;退回数量与原实发核平;异常单完成与派送 `RETURNED` 同事务 |
|
||||||
|
| PDA 盘点调整 | 绑定海外仓;盘点锁定库存位置并提交实盘数量 | 按差异产生增加/减少调整流水;无差异不产生虚假变动 | 需要 `inventory:adjust`;并发版本校验;差异正负方向正确;盘点重复提交幂等 |
|
||||||
|
| PDA 移库/托盘拆合/换绑 | 同仓有效托盘/库位;资源锁成功 | 数量净额不变,仅位置/托盘组成变化,写位置流水 | 不跨仓;托盘组成明细保留;资源锁释放;出库/换标占用时禁止绕过协调单独移动 |
|
||||||
|
|
||||||
|
## 不应误判为库存变动
|
||||||
|
|
||||||
|
- 目的仓修改、暂扣/解除、报价、排车预排、外接预约、车辆/司机资料:只改变状态、资格、资源占用或费用快照。
|
||||||
|
- 海外采购样品履约:按当前契约走 Admin 台账,**不接入 PDA,也不写 WMS 可用库存**。
|
||||||
|
- 退货申请/废弃申请/转派送建单:申请或建单阶段只形成冻结/候选/出库单,必须到实际收货、实际扣减或实际出库节点才确认数量变化。
|
||||||
|
|
||||||
|
## 每条库存用例的统一断言
|
||||||
|
|
||||||
|
1. 变动前后按客户、海外仓、目的仓、入仓单、托盘组成核对数量守恒。
|
||||||
|
2. 查询余额、位置明细和库存流水三处结果一致,且业务单号/幂等键可追溯。
|
||||||
|
3. 重复请求、重复事件、并发操作不得新增第二笔库存变动。
|
||||||
|
4. 任一校验、权限、资源锁或跨服务依赖失败时,库存、单据状态和流水一起回滚。
|
||||||
|
5. 客户越权、跨仓、跨客户、伪造来源入仓单/出库单均应拒绝。
|
||||||
|
|
||||||
|
流程图:`overseas-inventory-mutation-flow.svg` / `overseas-inventory-mutation-flow.png`
|
||||||
+23
-7
@@ -13,6 +13,15 @@ import java.util.List;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface CargoDiscardInventoryMapper {
|
public interface CargoDiscardInventoryMapper {
|
||||||
|
|
||||||
|
// 箱唛状态可能保留入库快照;废弃必须同时存在当前客户、同托盘的真实可用库存。
|
||||||
|
String AVAILABLE_STOCK = " AND EXISTS (SELECT 1 FROM ll_dropship_order_profile dp " +
|
||||||
|
"JOIN ll_inventory_position pos ON pos.id=dp.inventory_position_id AND pos.customer_id=dp.customer_id " +
|
||||||
|
"AND pos.pallet_id=dp.warehouse_pallet_id " +
|
||||||
|
"JOIN ll_inventory_balance bal ON bal.id=pos.balance_id AND bal.customer_id=pos.customer_id " +
|
||||||
|
"WHERE dp.customer_id=c.customer_id AND dp.order_id=c.order_id AND dp.id=c.profile_id " +
|
||||||
|
"AND dp.status='ACTIVE' AND pos.status='OCCUPIED' AND pos.available_packages>0 " +
|
||||||
|
"AND bal.available_packages>0) ";
|
||||||
|
|
||||||
@Select("SELECT p.* FROM ll_inventory_position p WHERE p.customer_id=#{customerId} AND p.id=#{positionId} FOR UPDATE")
|
@Select("SELECT p.* FROM ll_inventory_position p WHERE p.customer_id=#{customerId} AND p.id=#{positionId} FOR UPDATE")
|
||||||
com.baiyue.lastleg.inventory.entity.InventoryPosition lockGenericPosition(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
com.baiyue.lastleg.inventory.entity.InventoryPosition lockGenericPosition(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
||||||
|
|
||||||
@@ -50,10 +59,12 @@ public interface CargoDiscardInventoryMapper {
|
|||||||
int changeGenericCartonStatus(@Param("customerId") String customerId, @Param("cartonId") Long cartonId,
|
int changeGenericCartonStatus(@Param("customerId") String customerId, @Param("cartonId") Long cartonId,
|
||||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
||||||
|
|
||||||
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} AND c.status='AVAILABLE' ORDER BY c.profile_id,c.id")
|
/** 仅列出仍有实际可用库存的箱子,排除已出库托盘的残留箱唛。 */
|
||||||
|
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} AND c.status='AVAILABLE' " + AVAILABLE_STOCK + " ORDER BY c.profile_id,c.id")
|
||||||
List<DropshipCarton> availableOrderCartons(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
List<DropshipCarton> availableOrderCartons(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||||
|
|
||||||
@Select("<script>SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
/** 提交选箱时再次核对实际可用库存,避免旧页面绕过候选过滤。 */
|
||||||
|
@Select("<script>SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||||
"AND c.status='AVAILABLE' AND c.id IN <foreach collection='cartonIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
"AND c.status='AVAILABLE' AND c.id IN <foreach collection='cartonIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||||
"AND EXISTS(SELECT 1 FROM ll_dropship_carton_sku cs WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode}) ORDER BY c.profile_id,c.id FOR UPDATE</script>")
|
"AND EXISTS(SELECT 1 FROM ll_dropship_carton_sku cs WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode}) ORDER BY c.profile_id,c.id FOR UPDATE</script>")
|
||||||
List<DropshipCarton> lockSelected(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
List<DropshipCarton> lockSelected(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||||
@@ -72,6 +83,7 @@ public interface CargoDiscardInventoryMapper {
|
|||||||
@Select("SELECT DISTINCT carton_mark FROM ll_inventory_ledger WHERE customer_id=#{customerId} AND position_id=#{positionId} " +
|
@Select("SELECT DISTINCT carton_mark FROM ll_inventory_ledger WHERE customer_id=#{customerId} AND position_id=#{positionId} " +
|
||||||
"AND business_type IN ('STOCK_IN','RETURN_STOCK_IN') AND carton_mark IS NOT NULL AND carton_mark<>'' ORDER BY carton_mark")
|
"AND business_type IN ('STOCK_IN','RETURN_STOCK_IN') AND carton_mark IS NOT NULL AND carton_mark<>'' ORDER BY carton_mark")
|
||||||
List<String> looseCartonMarks(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
List<String> looseCartonMarks(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
||||||
|
/** 按当前可用仓位筛选入库单,不能只依赖箱唛的入库状态。 */
|
||||||
@Select("<script>SELECT o.id order_id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id," +
|
@Select("<script>SELECT o.id order_id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id," +
|
||||||
"o.overseas_warehouse_name,o.created_at,p.id profile_id,s.id sku_inventory_id,s.sku_code,s.product_name," +
|
"o.overseas_warehouse_name,o.created_at,p.id profile_id,s.id sku_inventory_id,s.sku_code,s.product_name," +
|
||||||
"COUNT(c.id) available_packages FROM ll_order o " +
|
"COUNT(c.id) available_packages FROM ll_order o " +
|
||||||
@@ -80,33 +92,37 @@ public interface CargoDiscardInventoryMapper {
|
|||||||
"JOIN ll_dropship_carton c ON c.customer_id=o.customer_id AND c.order_id=o.id " +
|
"JOIN ll_dropship_carton c ON c.customer_id=o.customer_id AND c.order_id=o.id " +
|
||||||
"AND c.profile_id=p.id AND c.status='AVAILABLE' " +
|
"AND c.profile_id=p.id AND c.status='AVAILABLE' " +
|
||||||
"JOIN ll_dropship_carton_sku cs ON cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=s.sku_code " +
|
"JOIN ll_dropship_carton_sku cs ON cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=s.sku_code " +
|
||||||
"WHERE o.customer_id=#{customerId} AND o.order_type='ONE_CLICK_DROPSHIP' " +
|
"WHERE o.customer_id=#{customerId} AND o.order_type='ONE_CLICK_DROPSHIP' " + AVAILABLE_STOCK +
|
||||||
"<if test='inboundCode != null and inboundCode != \"\"'>AND o.inbound_code LIKE CONCAT('%',#{inboundCode},'%') </if>" +
|
"<if test='inboundCode != null and inboundCode != \"\"'>AND o.inbound_code LIKE CONCAT('%',#{inboundCode},'%') </if>" +
|
||||||
"GROUP BY o.id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id,o.overseas_warehouse_name,o.created_at," +
|
"GROUP BY o.id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id,o.overseas_warehouse_name,o.created_at," +
|
||||||
"p.id,s.id,s.sku_code,s.product_name HAVING COUNT(c.id)>0 ORDER BY o.created_at DESC,o.id DESC,p.id,s.sku_code LIMIT 500</script>")
|
"p.id,s.id,s.sku_code,s.product_name HAVING COUNT(c.id)>0 ORDER BY o.created_at DESC,o.id DESC,p.id,s.sku_code LIMIT 500</script>")
|
||||||
List<CargoDiscardCandidateRow> candidates(@Param("customerId") String customerId,
|
List<CargoDiscardCandidateRow> candidates(@Param("customerId") String customerId,
|
||||||
@Param("inboundCode") String inboundCode);
|
@Param("inboundCode") String inboundCode);
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
/** SKU 可选数量仅统计实际仍在可用仓位的箱子。 */
|
||||||
|
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||||
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
||||||
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
|
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
|
||||||
long countAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
long countAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||||
@Param("skuCode") String skuCode);
|
@Param("skuCode") String skuCode);
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
/** 托盘 SKU 数量与候选及提交使用相同库存门禁。 */
|
||||||
|
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||||
"AND c.profile_id=#{profileId} AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
"AND c.profile_id=#{profileId} AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
||||||
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
|
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
|
||||||
long countAvailableProfile(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
long countAvailableProfile(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||||
@Param("profileId") Long profileId, @Param("skuCode") String skuCode);
|
@Param("profileId") Long profileId, @Param("skuCode") String skuCode);
|
||||||
|
|
||||||
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
/** 全部废弃锁箱时排除已出库或无实际可用库存的托盘。 */
|
||||||
|
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||||
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
||||||
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode}) " +
|
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode}) " +
|
||||||
"ORDER BY c.profile_id,c.carton_sequence,c.id LIMIT #{limit} FOR UPDATE")
|
"ORDER BY c.profile_id,c.carton_sequence,c.id LIMIT #{limit} FOR UPDATE")
|
||||||
List<DropshipCarton> lockAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
List<DropshipCarton> lockAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||||
@Param("skuCode") String skuCode, @Param("limit") int limit);
|
@Param("skuCode") String skuCode, @Param("limit") int limit);
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} AND status='AVAILABLE'")
|
/** 入库单按实际可用托盘统计物理箱数,不重复累计混装 SKU。 */
|
||||||
|
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} AND c.status='AVAILABLE' " + AVAILABLE_STOCK)
|
||||||
long countAvailableOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
long countAvailableOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||||
|
|
||||||
@Update("UPDATE ll_dropship_carton SET status=#{toStatus},version=version+1,updated_at=NOW(3) " +
|
@Update("UPDATE ll_dropship_carton SET status=#{toStatus},version=version+1,updated_at=NOW(3) " +
|
||||||
|
|||||||
+10
@@ -6,6 +6,16 @@ import org.apache.ibatis.annotations.Mapper;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface RelabelItemMapper extends BaseMapper<RelabelItem> {
|
public interface RelabelItemMapper extends BaseMapper<RelabelItem> {
|
||||||
|
/** 批量查询退货箱已完成的换标事实,避免逐箱查询。 */
|
||||||
|
@org.apache.ibatis.annotations.Select("<script>SELECT DISTINCT i.return_carton_id FROM ll_relabel_item i " +
|
||||||
|
"JOIN ll_relabel_application a ON a.id=i.application_id AND a.customer_id=i.customer_id " +
|
||||||
|
"WHERE i.customer_id=#{customerId} AND i.return_carton_id IN " +
|
||||||
|
"<foreach collection='cartonIds' item='cartonId' open='(' separator=',' close=')'>#{cartonId}</foreach> " +
|
||||||
|
"AND a.status NOT IN ('CANCELLED','REJECTED') AND a.customer_confirmed_at IS NOT NULL " +
|
||||||
|
"AND EXISTS(SELECT 1 FROM ll_relabel_pda_unit_fact f WHERE f.item_id=i.id AND f.result='RELABELLED')</script>")
|
||||||
|
java.util.List<Long> completedReturnCartonIds(@org.apache.ibatis.annotations.Param("customerId") String customerId,
|
||||||
|
@org.apache.ibatis.annotations.Param("cartonIds") java.util.List<Long> cartonIds);
|
||||||
|
|
||||||
@org.apache.ibatis.annotations.Select("SELECT ri.* FROM ll_relabel_item ri JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
|
@org.apache.ibatis.annotations.Select("SELECT ri.* FROM ll_relabel_item ri JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
|
||||||
"WHERE ri.customer_id=#{customerId} AND ri.return_carton_id=#{cartonId} AND ri.application_id<>#{currentApplicationId} " +
|
"WHERE ri.customer_id=#{customerId} AND ri.return_carton_id=#{cartonId} AND ri.application_id<>#{currentApplicationId} " +
|
||||||
"AND ra.status NOT IN ('CANCELLED','REJECTED') AND ra.customer_confirmed_at IS NOT NULL " +
|
"AND ra.status NOT IN ('CANCELLED','REJECTED') AND ra.customer_confirmed_at IS NOT NULL " +
|
||||||
|
|||||||
+22
@@ -5,11 +5,22 @@ import org.apache.ibatis.annotations.Select;
|
|||||||
@Mapper public interface InboundSnapshotMapper {
|
@Mapper public interface InboundSnapshotMapper {
|
||||||
record SourceCarton(String cartonMark, String skuCode, String productNameZh, Long quantity,
|
record SourceCarton(String cartonMark, String skuCode, String productNameZh, Long quantity,
|
||||||
String destinationWarehouseCode, String destinationWarehouseName, String skuItemsJson) {}
|
String destinationWarehouseCode, String destinationWarehouseName, String skuItemsJson) {}
|
||||||
|
/** 查询来源单当前仍可转运的箱级明细,排除已经存在有效换标记录的原箱。 */
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT c.carton_mark cartonMark,c.sku_code skuCode,NULL productNameZh,NULL quantity,
|
SELECT c.carton_mark cartonMark,c.sku_code skuCode,NULL productNameZh,NULL quantity,
|
||||||
c.destination_warehouse_code destinationWarehouseCode,c.destination_warehouse_name destinationWarehouseName,NULL skuItemsJson
|
c.destination_warehouse_code destinationWarehouseCode,c.destination_warehouse_name destinationWarehouseName,NULL skuItemsJson
|
||||||
FROM ll_inbound_plan_carton c JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
|
FROM ll_inbound_plan_carton c JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
|
||||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND o.overseas_warehouse_id=#{warehouseId}
|
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND o.overseas_warehouse_id=#{warehouseId}
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM ll_relabel_item ri
|
||||||
|
JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id
|
||||||
|
WHERE ri.customer_id=c.customer_id AND ri.source_inbound_code=o.inbound_code
|
||||||
|
AND ra.overseas_warehouse_id=#{warehouseId}
|
||||||
|
AND UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(c.carton_mark))
|
||||||
|
AND ra.status NOT IN ('CANCELLED','REJECTED')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT c.carton_mark,i.sku_code,i.product_name_zh,c.counted_quantity,
|
SELECT c.carton_mark,i.sku_code,i.product_name_zh,c.counted_quantity,
|
||||||
t.destination_warehouse_code,t.destination_warehouse_name,c.sku_items_json
|
t.destination_warehouse_code,t.destination_warehouse_name,c.sku_items_json
|
||||||
@@ -18,6 +29,17 @@ import org.apache.ibatis.annotations.Select;
|
|||||||
JOIN ll_order o ON o.id=t.lastleg_order_id AND o.customer_id=t.customer_id
|
JOIN ll_order o ON o.id=t.lastleg_order_id AND o.customer_id=t.customer_id
|
||||||
JOIN ll_warehouse_transfer_item i ON i.id=c.transfer_item_id AND i.customer_id=c.customer_id AND i.transfer_order_id=t.id
|
JOIN ll_warehouse_transfer_item i ON i.id=c.transfer_item_id AND i.customer_id=c.customer_id AND i.transfer_order_id=t.id
|
||||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND t.operating_warehouse_id=#{warehouseId} AND c.status='COUNTED'
|
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND t.operating_warehouse_id=#{warehouseId} AND c.status='COUNTED'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM ll_relabel_item ri
|
||||||
|
JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id
|
||||||
|
WHERE ri.customer_id=c.customer_id AND ri.source_inbound_code=o.inbound_code
|
||||||
|
AND ra.overseas_warehouse_id=#{warehouseId}
|
||||||
|
AND (UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(c.carton_mark))
|
||||||
|
OR UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(NULLIF(c.customer_carton_mark,''))))
|
||||||
|
AND ra.status NOT IN ('CANCELLED','REJECTED')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ORDER BY cartonMark
|
ORDER BY cartonMark
|
||||||
""")
|
""")
|
||||||
java.util.List<SourceCarton> sourceCartons(@Param("customerId") String customerId,
|
java.util.List<SourceCarton> sourceCartons(@Param("customerId") String customerId,
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.baiyue.lastleg.transfer.mapper;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 使用提交单据的原箱快照表达业务占用,不提前扣减实物库存。 */
|
||||||
|
@Mapper
|
||||||
|
public interface TransferSourceReservationMapper {
|
||||||
|
/** 按同一入仓快照串行提交,防止不同草稿同时抢占原箱。 */
|
||||||
|
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND id=#{inboundOrderId} FOR UPDATE")
|
||||||
|
Long lockInbound(@Param("customerId") String customerId, @Param("inboundOrderId") Long inboundOrderId);
|
||||||
|
|
||||||
|
/** 读取已提交转运的原箱;驳回、取消和草稿不占用,可重新选择。 */
|
||||||
|
@Select("SELECT COALESCE(NULLIF(c.customer_carton_mark,''), c.carton_mark) FROM ll_warehouse_transfer_carton c "
|
||||||
|
+ "JOIN ll_warehouse_transfer_order t ON t.id=c.transfer_order_id AND t.customer_id=c.customer_id "
|
||||||
|
+ "WHERE t.customer_id=#{customerId} AND t.operating_warehouse_id=#{warehouseId} "
|
||||||
|
+ "AND t.inbound_code=#{sourceCode} AND t.transfer_no <> t.inbound_code "
|
||||||
|
+ "AND t.status NOT IN ('DRAFT','REJECTED','CANCELLED') "
|
||||||
|
+ "AND COALESCE(NULLIF(c.customer_carton_mark,''), c.carton_mark) IS NOT NULL")
|
||||||
|
List<String> occupiedMarks(@Param("customerId") String customerId,
|
||||||
|
@Param("warehouseId") String warehouseId, @Param("sourceCode") String sourceCode);
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package com.baiyue.lastleg.transfer.repository;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 封装库存转运来源箱查询,SQL和客户隔离由Mapper统一执行。 */
|
||||||
|
@Repository
|
||||||
|
public class TransferInventorySourceRepository {
|
||||||
|
@Resource private InboundSnapshotMapper snapshots;
|
||||||
|
@Resource private com.baiyue.lastleg.transfer.mapper.TransferSourceReservationMapper reservations;
|
||||||
|
|
||||||
|
/** 同一来源入仓单的所有库存转运先取相同的行锁。 */
|
||||||
|
public Long lockSource(String customerId, Long inboundOrderId) {
|
||||||
|
return reservations.lockInbound(customerId, inboundOrderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取当前客户及操作仓中已提交转运的原箱快照。 */
|
||||||
|
public List<String> occupiedMarks(String customerId, String warehouseId, String sourceCode) {
|
||||||
|
return reservations.occupiedMarks(customerId, warehouseId, sourceCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取当前来源单未被有效换标申请使用的箱级快照。 */
|
||||||
|
public List<InboundSnapshotMapper.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||||
|
return snapshots.sourceCartons(customerId, warehouseId, inboundCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -6,6 +6,7 @@ import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
|||||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
||||||
|
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ public class TransferReturnSourceRepository {
|
|||||||
@Resource private ReturnOrderMapper orders;
|
@Resource private ReturnOrderMapper orders;
|
||||||
@Resource private ReturnInventoryCartonMapper cartons;
|
@Resource private ReturnInventoryCartonMapper cartons;
|
||||||
@Resource private ReturnInventoryCartonItemMapper items;
|
@Resource private ReturnInventoryCartonItemMapper items;
|
||||||
|
@Resource private RelabelItemMapper relabelItems;
|
||||||
|
|
||||||
/** 按当前客户和退货单号查询来源单据。 */
|
/** 按当前客户和退货单号查询来源单据。 */
|
||||||
public ReturnOrder findOrder(String customerId, String returnNo) {
|
public ReturnOrder findOrder(String customerId, String returnNo) {
|
||||||
@@ -25,10 +27,15 @@ public class TransferReturnSourceRepository {
|
|||||||
|
|
||||||
/** 查询当前仓内已完成清点且可用的退货箱,不影响其他页面的历史库存查询。 */
|
/** 查询当前仓内已完成清点且可用的退货箱,不影响其他页面的历史库存查询。 */
|
||||||
public List<ReturnInventoryCarton> availableCartons(String customerId, String warehouseId, String returnNo) {
|
public List<ReturnInventoryCarton> availableCartons(String customerId, String warehouseId, String returnNo) {
|
||||||
return cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream()
|
var available = cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream()
|
||||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus())
|
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus())
|
||||||
&& carton.getRelabelApplicationId() == null)
|
&& carton.getRelabelApplicationId() == null)
|
||||||
.toList();
|
.toList();
|
||||||
|
if (available.isEmpty()) return available;
|
||||||
|
// 已完成换标的箱虽然释放了当前占用字段,但不能再次作为原箱来源转运。
|
||||||
|
var completed = new java.util.HashSet<>(relabelItems.completedReturnCartonIds(customerId,
|
||||||
|
available.stream().map(ReturnInventoryCarton::getId).toList()));
|
||||||
|
return available.stream().filter(carton -> !completed.contains(carton.getId())).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量读取已通过客户及仓库隔离校验的来源箱明细。 */
|
/** 批量读取已通过客户及仓库隔离校验的来源箱明细。 */
|
||||||
|
|||||||
+51
-6
@@ -149,6 +149,7 @@ public class WarehouseTransferService {
|
|||||||
@Resource private InventoryCommandMapper inventoryCommands;
|
@Resource private InventoryCommandMapper inventoryCommands;
|
||||||
@Resource private ObjectMapper objectMapper;
|
@Resource private ObjectMapper objectMapper;
|
||||||
@Resource private TransferReturnSourceRepository returnSources;
|
@Resource private TransferReturnSourceRepository returnSources;
|
||||||
|
@Resource private com.baiyue.lastleg.transfer.repository.TransferInventorySourceRepository inventorySources;
|
||||||
|
|
||||||
/** 查询来源单据当前可用的箱唛及箱内 SKU,兼容普通入仓与退货库存。 */
|
/** 查询来源单据当前可用的箱唛及箱内 SKU,兼容普通入仓与退货库存。 */
|
||||||
public List<WarehouseTransferResponses.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
public List<WarehouseTransferResponses.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||||
@@ -164,22 +165,36 @@ public class WarehouseTransferService {
|
|||||||
List<Long> cartonIds = cartons.stream().map(ReturnInventoryCarton::getId).filter(Objects::nonNull).toList();
|
List<Long> cartonIds = cartons.stream().map(ReturnInventoryCarton::getId).filter(Objects::nonNull).toList();
|
||||||
Map<Long, List<ReturnInventoryCartonItem>> items = cartonIds.isEmpty() ? Map.of()
|
Map<Long, List<ReturnInventoryCartonItem>> items = cartonIds.isEmpty() ? Map.of()
|
||||||
: returnSources.cartonItems(cartonIds).stream().collect(Collectors.groupingBy(ReturnInventoryCartonItem::getCartonId));
|
: returnSources.cartonItems(cartonIds).stream().collect(Collectors.groupingBy(ReturnInventoryCartonItem::getCartonId));
|
||||||
return cartons.stream().map(carton -> {
|
return unreservedSourceCartons(customerId, warehouseId.trim(), source, cartons.stream().map(carton -> {
|
||||||
List<ReturnInventoryCartonItem> rows = items.getOrDefault(carton.getId(), List.of());
|
List<ReturnInventoryCartonItem> rows = items.getOrDefault(carton.getId(), List.of());
|
||||||
List<WarehouseTransferResponses.SourceCartonSku> contents = rows.stream()
|
List<WarehouseTransferResponses.SourceCartonSku> contents = rows.stream()
|
||||||
.filter(item -> item.getQuantity() != null && item.getQuantity() > 0)
|
.filter(item -> item.getQuantity() != null && item.getQuantity() > 0)
|
||||||
.map(item -> new WarehouseTransferResponses.SourceCartonSku(item.getSkuCode(), item.getQuantity())).toList();
|
.map(item -> new WarehouseTransferResponses.SourceCartonSku(item.getSkuCode(), item.getQuantity())).toList();
|
||||||
return new WarehouseTransferResponses.SourceCarton(carton.getCartonMark(), null, carton.getTotalQuantity(),
|
return new WarehouseTransferResponses.SourceCarton(carton.getCartonMark(), null, carton.getTotalQuantity(),
|
||||||
carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), contents);
|
carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), contents);
|
||||||
}).toList();
|
}).toList());
|
||||||
}
|
}
|
||||||
return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source).stream().map(row -> {
|
var sourceRows = inventorySources != null
|
||||||
|
? inventorySources.sourceCartons(customerId, warehouseId.trim(), source)
|
||||||
|
: inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source);
|
||||||
|
return unreservedSourceCartons(customerId, warehouseId.trim(), source, sourceRows.stream().map(row -> {
|
||||||
var carton = new WarehouseTransferCarton(); carton.setSkuItemsJson(row.skuItemsJson());
|
var carton = new WarehouseTransferCarton(); carton.setSkuItemsJson(row.skuItemsJson());
|
||||||
var contents = cartonContents(carton).stream().map(item -> new WarehouseTransferResponses.SourceCartonSku(item.skuCode(),item.quantity())).toList();
|
var contents = cartonContents(carton).stream().map(item -> new WarehouseTransferResponses.SourceCartonSku(item.skuCode(),item.quantity())).toList();
|
||||||
if (contents.isEmpty()) contents = List.of(new WarehouseTransferResponses.SourceCartonSku(row.skuCode(),row.quantity()));
|
if (contents.isEmpty()) contents = List.of(new WarehouseTransferResponses.SourceCartonSku(row.skuCode(),row.quantity()));
|
||||||
return new WarehouseTransferResponses.SourceCarton(row.cartonMark(),row.productNameZh(),row.quantity(),
|
return new WarehouseTransferResponses.SourceCarton(row.cartonMark(),row.productNameZh(),row.quantity(),
|
||||||
row.destinationWarehouseCode(),row.destinationWarehouseName(),contents);
|
row.destinationWarehouseCode(),row.destinationWarehouseName(),contents);
|
||||||
}).toList();
|
}).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待审核即按原箱快照占用;所有来源查询复用此过滤,避免重复提交同一实物箱。 */
|
||||||
|
private List<WarehouseTransferResponses.SourceCarton> unreservedSourceCartons(String customerId,
|
||||||
|
String warehouseId, String source, List<WarehouseTransferResponses.SourceCarton> cartons) {
|
||||||
|
if (inventorySources == null) return cartons;
|
||||||
|
Set<String> occupied = inventorySources.occupiedMarks(customerId, warehouseId, source).stream()
|
||||||
|
.filter(StringUtils::hasText).map(mark -> mark.trim().toUpperCase(java.util.Locale.ROOT))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return cartons.stream().filter(box -> !occupied.contains(box.cartonMark().trim()
|
||||||
|
.toUpperCase(java.util.Locale.ROOT))).toList();
|
||||||
}
|
}
|
||||||
private static LocalDateTime arrivalTime(WarehouseTransferRequests.Create request) {
|
private static LocalDateTime arrivalTime(WarehouseTransferRequests.Create request) {
|
||||||
return StringUtils.hasText(request.inboundCode()) ? null : request.estimatedArrivalAt();
|
return StringUtils.hasText(request.inboundCode()) ? null : request.estimatedArrivalAt();
|
||||||
@@ -334,7 +349,7 @@ public class WarehouseTransferService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Platform-created transfers keep the existing create-and-submit behavior. */
|
/** Platform-created transfers keep the existing create-and-submit behavior. */
|
||||||
@Transactional
|
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||||
public WarehouseTransferResponses.Detail createAndSubmit(String customerId, String operatorId,
|
public WarehouseTransferResponses.Detail createAndSubmit(String customerId, String operatorId,
|
||||||
WarehouseTransferRequests.Create request) {
|
WarehouseTransferRequests.Create request) {
|
||||||
WarehouseTransferResponses.Detail draft = create(customerId, operatorId, request);
|
WarehouseTransferResponses.Detail draft = create(customerId, operatorId, request);
|
||||||
@@ -456,7 +471,7 @@ public class WarehouseTransferService {
|
|||||||
return detail(customerId, transferNo);
|
return detail(customerId, transferNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||||
/** 提交草稿前重新校验来源箱归属,防止草稿保存后库存已发生变化。 */
|
/** 提交草稿前重新校验来源箱归属,防止草稿保存后库存已发生变化。 */
|
||||||
public WarehouseTransferResponses.Detail submit(String customerId, String transferNo, String operatorId) {
|
public WarehouseTransferResponses.Detail submit(String customerId, String transferNo, String operatorId) {
|
||||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||||
@@ -464,6 +479,13 @@ public class WarehouseTransferService {
|
|||||||
if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo);
|
if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo);
|
||||||
if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted");
|
if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted");
|
||||||
|
|
||||||
|
if (!transfer.getTransferNo().equals(transfer.getInboundCode())) {
|
||||||
|
// 所有同源提交锁定同一入仓记录;取锁后按最新可用箱校验并保存逐箱占用快照。
|
||||||
|
if (inventorySources != null && inventorySources.lockSource(customerId, transfer.getInboundOrderId()) == null)
|
||||||
|
throw conflict("库存来源已失效,请重新选择来源单");
|
||||||
|
if (inventorySources != null) reserveSourceCartons(customerId, transfer);
|
||||||
|
}
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||||
Set<String> allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode())
|
Set<String> allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode())
|
||||||
? sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
|
? sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
|
||||||
@@ -500,6 +522,29 @@ public class WarehouseTransferService {
|
|||||||
return detail(customerId, transferNo);
|
return detail(customerId, transferNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提交时逐箱核对来源快照并记录占用,禁止并发提交时静默替换用户选择。 */
|
||||||
|
private void reserveSourceCartons(String customerId, WarehouseTransferOrder transfer) {
|
||||||
|
var available = new java.util.LinkedHashMap<String, String>();
|
||||||
|
for (var box : sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())) {
|
||||||
|
available.put(box.cartonMark().trim().toUpperCase(java.util.Locale.ROOT), box.cartonMark());
|
||||||
|
}
|
||||||
|
var selected = transferCartons.listOrder(customerId, transfer.getId());
|
||||||
|
if (selected.size() != transfer.getPackageCount() || selected.isEmpty())
|
||||||
|
throw conflict("转运箱数与来源箱明细不一致,请刷新后重新提交");
|
||||||
|
var reserved = new java.util.LinkedHashMap<Long, String>();
|
||||||
|
for (var carton : selected) {
|
||||||
|
String mark = StringUtils.hasText(carton.getCustomerCartonMark())
|
||||||
|
? carton.getCustomerCartonMark() : carton.getCartonMark();
|
||||||
|
if (!StringUtils.hasText(mark)) throw conflict("转运箱缺少来源箱号,请刷新来源单");
|
||||||
|
String canonical = available.remove(mark.trim().toUpperCase(java.util.Locale.ROOT));
|
||||||
|
if (canonical == null) throw conflict("来源箱已被提交或不可用,请刷新来源单:" + mark);
|
||||||
|
reserved.put(carton.getId(), canonical);
|
||||||
|
}
|
||||||
|
for (var carton : selected) {
|
||||||
|
carton.setCustomerCartonMark(reserved.get(carton.getId()));
|
||||||
|
transferCartons.updateById(carton);
|
||||||
|
}
|
||||||
|
}
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public PageResult<WarehouseTransferResponses.Summary> page(String customerId, WarehouseTransferRequests.Query query) {
|
public PageResult<WarehouseTransferResponses.Summary> page(String customerId, WarehouseTransferRequests.Query query) {
|
||||||
return page(customerId, query, true);
|
return page(customerId, query, true);
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
package com.baiyue.lastleg.discard;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||||
|
import org.apache.ibatis.mapping.Environment;
|
||||||
|
import org.apache.ibatis.session.Configuration;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
||||||
|
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/** 使用真实 Mapper SQL 验证已出库但箱唛状态残留时的废弃门禁。 */
|
||||||
|
class CargoDiscardAvailableStockMapperTest {
|
||||||
|
/** 全部出库不可选;部分托盘仍有库存时只返回该托盘,提交旧箱号同样失败。 */
|
||||||
|
@Test
|
||||||
|
void filtersShippedStockFromCandidatesCountsAndSelection() {
|
||||||
|
var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||||
|
var db = new JdbcTemplate(source);
|
||||||
|
db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(20),inbound_code VARCHAR(50),inbound_order_no VARCHAR(50),overseas_warehouse_id VARCHAR(20),overseas_warehouse_name VARCHAR(50),created_at TIMESTAMP,order_type VARCHAR(30))");
|
||||||
|
db.execute("CREATE TABLE ll_dropship_order_profile(id BIGINT,customer_id VARCHAR(20),order_id BIGINT,inventory_position_id BIGINT,warehouse_pallet_id BIGINT,status VARCHAR(20))");
|
||||||
|
db.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(20),balance_id BIGINT,pallet_id BIGINT,status VARCHAR(20),available_packages BIGINT)");
|
||||||
|
db.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR(20),available_packages BIGINT)");
|
||||||
|
db.execute("CREATE TABLE ll_dropship_carton(id BIGINT,customer_id VARCHAR(20),order_id BIGINT,profile_id BIGINT,status VARCHAR(20),carton_sequence INT)");
|
||||||
|
db.execute("CREATE TABLE ll_dropship_carton_sku(customer_id VARCHAR(20),carton_id BIGINT,sku_code VARCHAR(20))");
|
||||||
|
db.execute("CREATE TABLE ll_dropship_sku_inventory(id BIGINT,customer_id VARCHAR(20),order_id BIGINT,profile_id BIGINT,sku_code VARCHAR(20),product_name VARCHAR(50))");
|
||||||
|
db.update("INSERT INTO ll_order VALUES(1,'C','EXXHWK2809191130','IP-WH-20260917-979650','WH','测试仓',CURRENT_TIMESTAMP,'ONE_CLICK_DROPSHIP')");
|
||||||
|
db.update("INSERT INTO ll_dropship_order_profile VALUES(10,'C',1,100,1000,'ACTIVE'),(20,'C',1,200,2000,'ACTIVE')");
|
||||||
|
db.update("INSERT INTO ll_inventory_position VALUES(100,'C',10000,1000,'OCCUPIED',0),(200,'C',20000,2000,'OCCUPIED',0)");
|
||||||
|
db.update("INSERT INTO ll_inventory_balance VALUES(10000,'C',0),(20000,'C',0)");
|
||||||
|
// 复现库存已扣完,箱唛仍为 AVAILABLE 的状态,不能因此暴露可废弃库存。
|
||||||
|
db.update("INSERT INTO ll_dropship_carton VALUES(11,'C',1,10,'AVAILABLE',1),(21,'C',1,20,'AVAILABLE',1)");
|
||||||
|
db.update("INSERT INTO ll_dropship_carton_sku VALUES('C',11,'SKU-A'),('C',21,'SKU-A'),('C',21,'SKU-B')");
|
||||||
|
db.update("INSERT INTO ll_dropship_sku_inventory VALUES(1,'C',1,10,'SKU-A','货物A'),(2,'C',1,20,'SKU-A','货物A'),(3,'C',1,20,'SKU-B','货物B')");
|
||||||
|
var configuration = new Configuration(new Environment("test", new JdbcTransactionFactory(), source));
|
||||||
|
configuration.setMapUnderscoreToCamelCase(true);
|
||||||
|
configuration.addMapper(CargoDiscardInventoryMapper.class);
|
||||||
|
try (var session = new SqlSessionFactoryBuilder().build(configuration).openSession(true)) {
|
||||||
|
var mapper = session.getMapper(CargoDiscardInventoryMapper.class);
|
||||||
|
assertTrue(mapper.candidates("C", null).isEmpty());
|
||||||
|
assertEquals(0, mapper.countAvailableOrder("C", 1L));
|
||||||
|
assertTrue(mapper.availableOrderCartons("C", 1L).isEmpty());
|
||||||
|
assertTrue(mapper.lockSelected("C", 1L, "SKU-A", List.of(11L)).isEmpty());
|
||||||
|
assertTrue(mapper.lockAvailable("C", 1L, "SKU-A", 20).isEmpty());
|
||||||
|
|
||||||
|
db.update("UPDATE ll_inventory_position SET available_packages=1 WHERE id=200");
|
||||||
|
db.update("UPDATE ll_inventory_balance SET available_packages=1 WHERE id=20000");
|
||||||
|
session.clearCache();
|
||||||
|
assertEquals(2, mapper.candidates("C", "EXXHWK").size());
|
||||||
|
assertEquals(1, mapper.countAvailableOrder("C", 1L));
|
||||||
|
assertEquals(1, mapper.countAvailable("C", 1L, "SKU-A"));
|
||||||
|
assertEquals(0, mapper.countAvailableProfile("C", 1L, 10L, "SKU-A"));
|
||||||
|
assertEquals(1, mapper.countAvailableProfile("C", 1L, 20L, "SKU-B"));
|
||||||
|
assertEquals(List.of(21L), mapper.availableOrderCartons("C", 1L).stream().map(c -> c.getId()).toList());
|
||||||
|
assertEquals(List.of(21L), mapper.lockSelected("C", 1L, "SKU-A", List.of(11L, 21L)).stream().map(c -> c.getId()).toList());
|
||||||
|
assertEquals(List.of(21L), mapper.lockAvailable("C", 1L, "SKU-A", 20).stream().map(c -> c.getId()).toList());
|
||||||
|
assertTrue(mapper.candidates("OTHER", null).isEmpty());
|
||||||
|
assertEquals(0, mapper.countAvailableOrder("OTHER", 1L));
|
||||||
|
|
||||||
|
// 仓位与余额必须属于相同客户,不能借用其他客户的正库存。
|
||||||
|
db.update("UPDATE ll_inventory_balance SET customer_id='OTHER' WHERE id=20000");
|
||||||
|
session.clearCache();
|
||||||
|
assertTrue(mapper.candidates("C", null).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-6
@@ -24,23 +24,30 @@ class TransferInventorySourceTest {
|
|||||||
assertFalse(request(null,null,null,null).isInboundModeValid());
|
assertFalse(request(null,null,null,null).isInboundModeValid());
|
||||||
assertTrue(request(null,null,"CUSTOMER_DELIVERY",LocalDateTime.now()).isInboundModeValid());
|
assertTrue(request(null,null,"CUSTOMER_DELIVERY",LocalDateTime.now()).isInboundModeValid());
|
||||||
}
|
}
|
||||||
|
/** 已换标或正在换标的原箱不再展示;撤销、驳回及其他客户的记录不能误删当前箱。 */
|
||||||
@Test void originalBoxesIncludeBothSourcesButNeverAnotherCustomerWarehouseOrInbound() {
|
@Test void originalBoxesIncludeBothSourcesButNeverAnotherCustomerWarehouseOrInbound() {
|
||||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
var ds = new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||||
var db=new JdbcTemplate(ds);
|
var db=new JdbcTemplate(ds);
|
||||||
db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR,inbound_code VARCHAR,overseas_warehouse_id VARCHAR)");
|
db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR,inbound_code VARCHAR,overseas_warehouse_id VARCHAR)");
|
||||||
db.execute("CREATE TABLE ll_inbound_plan_carton(customer_id VARCHAR,order_id BIGINT,carton_mark VARCHAR,sku_code VARCHAR,destination_warehouse_code VARCHAR,destination_warehouse_name VARCHAR)");
|
db.execute("CREATE TABLE ll_inbound_plan_carton(customer_id VARCHAR,order_id BIGINT,carton_mark VARCHAR,sku_code VARCHAR,destination_warehouse_code VARCHAR,destination_warehouse_name VARCHAR)");
|
||||||
db.execute("CREATE TABLE ll_warehouse_transfer_order(id BIGINT,customer_id VARCHAR,lastleg_order_id BIGINT,operating_warehouse_id VARCHAR,destination_warehouse_code VARCHAR,destination_warehouse_name VARCHAR)");
|
db.execute("CREATE TABLE ll_warehouse_transfer_order(id BIGINT,customer_id VARCHAR,lastleg_order_id BIGINT,operating_warehouse_id VARCHAR,destination_warehouse_code VARCHAR,destination_warehouse_name VARCHAR,inbound_code VARCHAR,status VARCHAR)");
|
||||||
db.execute("CREATE TABLE ll_warehouse_transfer_item(id BIGINT,customer_id VARCHAR,transfer_order_id BIGINT,sku_code VARCHAR,product_name_zh VARCHAR)");
|
db.execute("CREATE TABLE ll_warehouse_transfer_item(id BIGINT,customer_id VARCHAR,transfer_order_id BIGINT,sku_code VARCHAR,product_name_zh VARCHAR)");
|
||||||
db.execute("CREATE TABLE ll_warehouse_transfer_carton(customer_id VARCHAR,transfer_order_id BIGINT,transfer_item_id BIGINT,carton_mark VARCHAR,counted_quantity BIGINT,sku_items_json VARCHAR,status VARCHAR)");
|
db.execute("CREATE TABLE ll_warehouse_transfer_carton(customer_id VARCHAR,transfer_order_id BIGINT,transfer_item_id BIGINT,carton_mark VARCHAR,customer_carton_mark VARCHAR,counted_quantity BIGINT,sku_items_json VARCHAR,status VARCHAR)");
|
||||||
|
db.execute("CREATE TABLE ll_relabel_application(id BIGINT,customer_id VARCHAR,overseas_warehouse_id VARCHAR,status VARCHAR,customer_confirmed_at TIMESTAMP)");
|
||||||
|
db.execute("CREATE TABLE ll_relabel_item(application_id BIGINT,customer_id VARCHAR,source_inbound_code VARCHAR,source_carton_mark VARCHAR,carton_mark VARCHAR)");
|
||||||
|
db.execute("CREATE TABLE ll_relabel_pda_unit_fact(item_id BIGINT,result VARCHAR)");
|
||||||
db.update("INSERT INTO ll_order VALUES(1,'A','IN','WH'),(2,'B','IN','WH'),(3,'A','IN','OTHER'),(4,'A','OTHER','WH')");
|
db.update("INSERT INTO ll_order VALUES(1,'A','IN','WH'),(2,'B','IN','WH'),(3,'A','IN','OTHER'),(4,'A','OTHER','WH')");
|
||||||
db.update("INSERT INTO ll_inbound_plan_carton VALUES('A',1,'FIRST','S','ONT8','Amazon'),('B',2,'FOREIGN','S','ONT8','Amazon'),('A',3,'WRONG-WH','S','ONT8','Amazon'),('A',4,'WRONG-IN','S','ONT8','Amazon'),('B',1,'FORGED','S','ONT8','Amazon')");
|
db.update("INSERT INTO ll_inbound_plan_carton VALUES('A',1,'FIRST','S','ONT8','Amazon'),('A',1,'RELABELED','S','ONT8','Amazon'),('B',2,'FOREIGN','S','ONT8','Amazon'),('A',3,'WRONG-WH','S','ONT8','Amazon'),('A',4,'WRONG-IN','S','ONT8','Amazon'),('B',1,'FORGED','S','ONT8','Amazon')");
|
||||||
db.update("INSERT INTO ll_warehouse_transfer_order VALUES(10,'A',1,'WH','ONT8','Amazon')");
|
db.update("INSERT INTO ll_relabel_application VALUES(51,'A','WH','COMPLETED',CURRENT_TIMESTAMP()),(52,'A','WH','PROCESSING',NULL),(53,'A','WH','CANCELLED',NULL),(54,'A','WH','REJECTED',NULL),(55,'B','WH','COMPLETED',CURRENT_TIMESTAMP()),(56,'A','OTHER','COMPLETED',CURRENT_TIMESTAMP())");
|
||||||
|
db.update("INSERT INTO ll_relabel_item VALUES(51,'A','IN','RELABELED','RELABELED'),(52,'A','IN',NULL,'CUSTOMER-OLD'),(53,'A','IN',NULL,'FIRST'),(54,'A','IN',NULL,'FIRST'),(55,'B','IN',NULL,'FIRST'),(56,'A','IN',NULL,'FIRST')");
|
||||||
|
db.update("INSERT INTO ll_relabel_pda_unit_fact VALUES(51,'RELABELLED')");
|
||||||
|
db.update("INSERT INTO ll_warehouse_transfer_order VALUES(10,'A',1,'WH','ONT8','Amazon','IN','DRAFT'),(20,'A',1,'WH','ONT8','Amazon','IN','PENDING_REVIEW')");
|
||||||
db.update("INSERT INTO ll_warehouse_transfer_item VALUES(11,'A',10,'SKU','Product')");
|
db.update("INSERT INTO ll_warehouse_transfer_item VALUES(11,'A',10,'SKU','Product')");
|
||||||
db.update("INSERT INTO ll_warehouse_transfer_carton VALUES('A',10,11,'MEASURED',20,NULL,'COUNTED'),('A',10,11,'EXPECTED',NULL,NULL,'EXPECTED'),('B',10,11,'FOREIGN2',20,NULL,'COUNTED')");
|
db.update("INSERT INTO ll_warehouse_transfer_carton VALUES('A',10,11,'MEASURED',NULL,20,NULL,'COUNTED'),('A',10,11,'RELABELED-TRANSFER','CUSTOMER-OLD',20,NULL,'COUNTED'),('A',10,11,'EXPECTED',NULL,NULL,NULL,'EXPECTED'),('A',20,11,'FIRST',NULL,20,NULL,'COUNTED'),('B',10,11,'FOREIGN2',NULL,20,NULL,'COUNTED')");
|
||||||
var config=new Configuration(new Environment("test",new JdbcTransactionFactory(),ds)); config.addMapper(InboundSnapshotMapper.class);
|
var config=new Configuration(new Environment("test",new JdbcTransactionFactory(),ds)); config.addMapper(InboundSnapshotMapper.class);
|
||||||
try(var session=new SqlSessionFactoryBuilder().build(config).openSession()) {
|
try(var session=new SqlSessionFactoryBuilder().build(config).openSession()) {
|
||||||
var result=session.getMapper(InboundSnapshotMapper.class).sourceCartons("A","WH","IN");
|
var result=session.getMapper(InboundSnapshotMapper.class).sourceCartons("A","WH","IN");
|
||||||
assertEquals(List.of("FIRST","MEASURED"),result.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).toList());
|
assertEquals(List.of("FIRST", "MEASURED"),result.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).toList());
|
||||||
assertEquals("ONT8",result.get(0).destinationWarehouseCode());
|
assertEquals("ONT8",result.get(0).destinationWarehouseCode());
|
||||||
assertNull(result.get(0).quantity()); assertEquals(20L,result.get(1).quantity());
|
assertNull(result.get(0).quantity()); assertEquals(20L,result.get(1).quantity());
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -6,6 +6,7 @@ import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
|||||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
||||||
|
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||||
import com.baiyue.lastleg.transfer.repository.TransferReturnSourceRepository;
|
import com.baiyue.lastleg.transfer.repository.TransferReturnSourceRepository;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
@@ -23,12 +24,15 @@ class TransferReturnSourceTest {
|
|||||||
var orders = mock(ReturnOrderMapper.class);
|
var orders = mock(ReturnOrderMapper.class);
|
||||||
var cartons = mock(ReturnInventoryCartonMapper.class);
|
var cartons = mock(ReturnInventoryCartonMapper.class);
|
||||||
var items = mock(ReturnInventoryCartonItemMapper.class);
|
var items = mock(ReturnInventoryCartonItemMapper.class);
|
||||||
|
var relabelItems = mock(RelabelItemMapper.class);
|
||||||
var repository = new TransferReturnSourceRepository();
|
var repository = new TransferReturnSourceRepository();
|
||||||
ReflectionTestUtils.setField(repository, "orders", orders);
|
ReflectionTestUtils.setField(repository, "orders", orders);
|
||||||
ReflectionTestUtils.setField(repository, "cartons", cartons);
|
ReflectionTestUtils.setField(repository, "cartons", cartons);
|
||||||
ReflectionTestUtils.setField(repository, "items", items);
|
ReflectionTestUtils.setField(repository, "items", items);
|
||||||
|
ReflectionTestUtils.setField(repository, "relabelItems", relabelItems);
|
||||||
var service = new WarehouseTransferService();
|
var service = new WarehouseTransferService();
|
||||||
ReflectionTestUtils.setField(service, "returnSources", repository);
|
ReflectionTestUtils.setField(service, "returnSources", repository);
|
||||||
|
ReflectionTestUtils.setField(service, "inventorySources", mock(com.baiyue.lastleg.transfer.repository.TransferInventorySourceRepository.class));
|
||||||
var order = new ReturnOrder();
|
var order = new ReturnOrder();
|
||||||
order.setOverseasWarehouseId("WH-1");
|
order.setOverseasWarehouseId("WH-1");
|
||||||
when(orders.findNo("C-1", "RET-1")).thenReturn(order);
|
when(orders.findNo("C-1", "RET-1")).thenReturn(order);
|
||||||
@@ -38,6 +42,7 @@ class TransferReturnSourceTest {
|
|||||||
var sku = new ReturnInventoryCartonItem();
|
var sku = new ReturnInventoryCartonItem();
|
||||||
sku.setCartonId(1L); sku.setSkuCode("SKU-1"); sku.setQuantity(3L);
|
sku.setCartonId(1L); sku.setSkuCode("SKU-1"); sku.setQuantity(3L);
|
||||||
when(items.listCartons(List.of(1L))).thenReturn(List.of(sku));
|
when(items.listCartons(List.of(1L))).thenReturn(List.of(sku));
|
||||||
|
when(relabelItems.completedReturnCartonIds("C-1", List.of(1L))).thenReturn(List.of());
|
||||||
|
|
||||||
var result = service.sourceCartons("C-1", "WH-1", "RETURN_ORDER:RET-1");
|
var result = service.sourceCartons("C-1", "WH-1", "RETURN_ORDER:RET-1");
|
||||||
|
|
||||||
@@ -45,6 +50,9 @@ class TransferReturnSourceTest {
|
|||||||
assertEquals("BOX-1", result.get(0).cartonMark());
|
assertEquals("BOX-1", result.get(0).cartonMark());
|
||||||
assertEquals("SKU-1", result.get(0).items().get(0).skuCode());
|
assertEquals("SKU-1", result.get(0).items().get(0).skuCode());
|
||||||
assertEquals(3L, result.get(0).items().get(0).quantity());
|
assertEquals(3L, result.get(0).items().get(0).quantity());
|
||||||
|
when(relabelItems.completedReturnCartonIds("C-1", List.of(1L))).thenReturn(List.of(1L));
|
||||||
|
assertTrue(service.sourceCartons("C-1", "WH-1", "RETURN_ORDER:RET-1").isEmpty(),
|
||||||
|
"释放占用后的已换标退货箱不再回到原单明细");
|
||||||
assertThrows(ResponseStatusException.class,
|
assertThrows(ResponseStatusException.class,
|
||||||
() -> service.sourceCartons("C-1", "OTHER-WH", "RETURN_ORDER:RET-1"));
|
() -> service.sourceCartons("C-1", "OTHER-WH", "RETURN_ORDER:RET-1"));
|
||||||
verify(cartons, never()).currentReturnCartons("C-1", "OTHER-WH", "RET-1");
|
verify(cartons, never()).currentReturnCartons("C-1", "OTHER-WH", "RET-1");
|
||||||
|
|||||||
+7
@@ -80,6 +80,7 @@ import static org.mockito.Mockito.doThrow;
|
|||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.clearInvocations;
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@@ -247,6 +248,12 @@ class WarehouseTransferServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "warehouseResources", warehouseResources);
|
ReflectionTestUtils.setField(service, "warehouseResources", warehouseResources);
|
||||||
ReflectionTestUtils.setField(service, "measurementPhotoFiles", measurementPhotoFiles);
|
ReflectionTestUtils.setField(service, "measurementPhotoFiles", measurementPhotoFiles);
|
||||||
ReflectionTestUtils.setField(service, "inboundSnapshots", inboundSnapshots);
|
ReflectionTestUtils.setField(service, "inboundSnapshots", inboundSnapshots);
|
||||||
|
var inventorySourceRepository = new com.baiyue.lastleg.transfer.repository.TransferInventorySourceRepository();
|
||||||
|
ReflectionTestUtils.setField(inventorySourceRepository, "snapshots", inboundSnapshots);
|
||||||
|
var reservations = mock(com.baiyue.lastleg.transfer.mapper.TransferSourceReservationMapper.class);
|
||||||
|
lenient().when(reservations.lockInbound(anyString(), any())).thenReturn(101L);
|
||||||
|
ReflectionTestUtils.setField(inventorySourceRepository, "reservations", reservations);
|
||||||
|
ReflectionTestUtils.setField(service, "inventorySources", inventorySourceRepository);
|
||||||
ReflectionTestUtils.setField(service, "inboundOrders", inboundOrders);
|
ReflectionTestUtils.setField(service, "inboundOrders", inboundOrders);
|
||||||
ReflectionTestUtils.setField(service, "inboundPlans", inboundPlans);
|
ReflectionTestUtils.setField(service, "inboundPlans", inboundPlans);
|
||||||
ReflectionTestUtils.setField(service, "inboundPlanLines", inboundPlanLines);
|
ReflectionTestUtils.setField(service, "inboundPlanLines", inboundPlanLines);
|
||||||
|
|||||||
Reference in New Issue
Block a user