Merge branch 'dev-ty1.2' into test
# Conflicts: # mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WmsServiceFeign.java
This commit is contained in:
@@ -21,8 +21,7 @@ import java.util.Map;
|
||||
* @description: TODO
|
||||
* @date 2024/5/10 8:58
|
||||
**/
|
||||
// 目标地址由 nacos 配置项 feign.url.mhd-wms-service 指定,未配置时按服务名走服务发现
|
||||
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,url = "http://10.102.192.32:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
|
||||
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,url = "http://10.33.0.109:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
|
||||
public interface WmsServiceFeign {
|
||||
|
||||
/**
|
||||
@@ -113,11 +112,46 @@ public interface WmsServiceFeign {
|
||||
|
||||
/**
|
||||
* oms下发前库存校验:可用库存不足时返回错误,OMS据此在推送前拦截下发,
|
||||
* 避免单据先落库、分配才失败而在WMS残留"已审核"状态的垃圾单
|
||||
* 避免单据先落库、分配才失败而在WMS残留"已审核"状态的垃圾单。
|
||||
* businessOrderNumber(可空):OMS出库业务单号,下单冻结模式下校验可用量=库存可用+本单冻结记录量
|
||||
*/
|
||||
@ApiOperation("oms下发前库存校验")
|
||||
@PostMapping(value = "/stockOutOrderApi/checkOmsAssignInventory")
|
||||
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails);
|
||||
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails,
|
||||
@RequestParam(value = "businessOrderNumber", required = false) String businessOrderNumber);
|
||||
|
||||
/**
|
||||
* oms出库业务单下单冻结库存(防超卖):WMS按明细冻结库存(可用-、冻结+,库存数量不变),
|
||||
* 条件更新防并发超卖,任一行可用不足整体失败并返回错误;同业务单号幂等。
|
||||
* 冻结失败必须阻断OMS下单(返回非200由OMS整体回滚)。
|
||||
*/
|
||||
@ApiOperation("oms出库业务单下单冻结库存")
|
||||
@PostMapping(value = "/stockOutOrderApi/omsFreezeInventory")
|
||||
public AjaxResult omsFreezeInventory(@RequestBody OmsFreezeInventoryParam param);
|
||||
|
||||
/**
|
||||
* oms出库业务单取消/删除-释放冻结库存:可用+、冻结-(库存数量不变);按业务单号幂等;
|
||||
* 冻结已被执行单承接时拒绝(须先取消下发)
|
||||
*/
|
||||
@ApiOperation("oms出库业务单释放冻结库存")
|
||||
@PostMapping(value = "/stockOutOrderApi/omsUnfreezeInventory")
|
||||
public AjaxResult omsUnfreezeInventory(@RequestBody OmsFreezeInventoryParam param);
|
||||
|
||||
/**
|
||||
* oms出库业务单编辑-原子重锁:WMS同一事务内先释放原冻结再按新明细重新冻结,
|
||||
* 任一新明细可用不足整体回滚(旧冻结同样还原),满足"先释放再重新锁定"原子性要求
|
||||
*/
|
||||
@ApiOperation("oms出库业务单编辑重锁库存")
|
||||
@PostMapping(value = "/stockOutOrderApi/omsRelockInventory")
|
||||
public AjaxResult omsRelockInventory(@RequestBody OmsFreezeInventoryParam param);
|
||||
|
||||
/**
|
||||
* 仓库是否存在活跃冻结记录(status in 1-业务单冻结 2-执行单承接):
|
||||
* 仓库"库存锁定模式"(stock_lock_mode)切换守卫,存在在途冻结单时禁止切换
|
||||
*/
|
||||
@ApiOperation("仓库是否存在活跃冻结记录")
|
||||
@GetMapping(value = "/stockOutOrderApi/hasActiveFreezeByWarehouse/{warehouseId}")
|
||||
public AjaxResult hasActiveFreezeByWarehouse(@PathVariable("warehouseId") Long warehouseId);
|
||||
|
||||
/**
|
||||
* oms下发单明细保存(返回执行结果,明细保存含自动审核链路,失败时OMS据此回滚)
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.mhd.system.api.domain;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OMS出库业务单冻结库存请求参数
|
||||
*
|
||||
* @description OMS下单冻结库存模式(防超卖):OMS出库业务单「下单」即冻结WMS库存
|
||||
* (可用-、冻结+,库存数量不变,库存数量=可用数量+冻结数量);
|
||||
* 「编辑」原子释放旧冻结+按新明细重锁;「取消」释放冻结。
|
||||
* 冻结/解冻以业务单号为幂等键。
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
@Data
|
||||
public class OmsFreezeInventoryParam implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("OMS出库业务单号(幂等键)")
|
||||
private String outOrderNumber;
|
||||
|
||||
@ApiModelProperty("执行出库单号(预留,冻结/解冻/重锁时不使用)")
|
||||
private String executionOrderNumber;
|
||||
|
||||
@ApiModelProperty("库存行ID集合(预留)")
|
||||
private List<Long> materialInventoryIds;
|
||||
|
||||
@ApiModelProperty("出库明细列表(须携带 materialInventoryId 定位库存行)")
|
||||
private List<OutMaterialDetail> details;
|
||||
|
||||
@ApiModelProperty("操作人ID(Feign调用可能无登录上下文时的兜底)")
|
||||
private Long operatorId;
|
||||
|
||||
@ApiModelProperty("操作人姓名(Feign调用可能无登录上下文时的兜底)")
|
||||
private String operatorName;
|
||||
|
||||
@ApiModelProperty("备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -124,6 +124,10 @@ public class WarehouseFeignPO extends BaseVOEntity{
|
||||
@Excel(name = "作业模式")
|
||||
private String workMode;
|
||||
|
||||
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
private Integer stockLockMode;
|
||||
|
||||
@ApiModelProperty("订单号生成规则(0-日期累加 1-历史累加)")
|
||||
@Excel(name = "单号生成规则")
|
||||
private String orderNoGenerateRule;
|
||||
|
||||
+25
-1
@@ -102,11 +102,35 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails) {
|
||||
public AjaxResult checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails, String businessOrderNumber) {
|
||||
//校验接口本身不可用时返回错误,OMS会阻止下发(不能在WMS状态未知时盲目推送)
|
||||
return AjaxResult.error("WMS库存校验服务不可用:" + cause.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult omsFreezeInventory(OmsFreezeInventoryParam param) {
|
||||
//返回错误而非静默成功:下单冻结失败必须阻断OMS下单落库(冻结成功与否状态未知时不允许继续)
|
||||
return AjaxResult.error("WMS下单冻结库存失败(服务降级):" + cause.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult omsUnfreezeInventory(OmsFreezeInventoryParam param) {
|
||||
//返回错误而非静默成功:解冻失败让OMS取消/删除感知(解冻可重试,WMS解冻幂等)
|
||||
return AjaxResult.error("WMS释放冻结库存失败(服务降级):" + cause.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult omsRelockInventory(OmsFreezeInventoryParam param) {
|
||||
//返回错误而非静默成功:编辑重锁是原子操作,失败必须让OMS编辑整体回滚
|
||||
return AjaxResult.error("WMS编辑重锁库存失败(服务降级):" + cause.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult hasActiveFreezeByWarehouse(Long warehouseId) {
|
||||
//返回错误(fail-closed):模式切换守卫校验不可用时拒绝切换,避免在途冻结单语义错乱
|
||||
return AjaxResult.error("WMS冻结记录校验服务不可用:" + cause.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult omsInOrderAddDetail(List<InMaterialDetail> inMaterialDetails) {
|
||||
//返回错误而非静默成功:明细保存(含自动审核)失败必须让OMS感知并回滚下发标记
|
||||
|
||||
+3
@@ -35,4 +35,7 @@ public class WarehousePO implements Serializable {
|
||||
|
||||
@Excel(name = "是否启用: 1-否 2-是")
|
||||
private Integer isEnable;
|
||||
|
||||
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
private Integer stockLockMode;
|
||||
}
|
||||
|
||||
+70
@@ -12,7 +12,9 @@ import com.mhd.basic.domain.warehouse.repository.todo.WarehouseDO;
|
||||
import com.mhd.basic.domain.warehouse.service.WarehouseDomainService;
|
||||
import com.mhd.common.core.enums.DictCode;
|
||||
import com.mhd.common.core.exception.ServiceException;
|
||||
import com.mhd.common.core.web.domain.AjaxResult;
|
||||
import com.mhd.common.security.utils.SecurityUtils;
|
||||
import com.mhd.system.api.WmsServiceFeign;
|
||||
import com.mhd.system.api.domain.SysDictData;
|
||||
import com.mhd.system.api.model.LoginUser;
|
||||
import com.mhd.system.service.ISysDictTypeService;
|
||||
@@ -43,6 +45,8 @@ public class WarehouseApplicationService {
|
||||
private ISysDictTypeService dictTypeService;
|
||||
@Autowired
|
||||
private IAssociationWarehouseService associationWarehouseService;
|
||||
@Autowired
|
||||
private WmsServiceFeign wmsServiceFeign;
|
||||
|
||||
|
||||
/**
|
||||
@@ -239,12 +243,78 @@ public class WarehouseApplicationService {
|
||||
/**
|
||||
* 修改仓库
|
||||
*/
|
||||
/**
|
||||
* @description 查询仓库库存锁定模式(OMS下单冻结库存/防超卖功能的仓库级开关)
|
||||
* 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存(可用-、冻结+,库存数量不变)
|
||||
* 2-分配库存冻结:现有模式,WMS「分配库存」时才冻结
|
||||
* 仓库不存在或未配置(null)时返回默认值 2(与代码侧降级口径一致)
|
||||
* @author gen
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
public Integer getStockLockMode(Long warehouseId) {
|
||||
WarehousePO warehousePO = warehouseDomainService.getInfo(warehouseId);
|
||||
return warehousePO == null ? 2
|
||||
: cn.hutool.core.util.ObjectUtil.defaultIfNull(warehousePO.getStockLockMode(), 2);
|
||||
}
|
||||
|
||||
public Boolean update(WarehouseDO warehouseDO) {
|
||||
// 库存锁定模式切换守卫:模式发生变化时,存在在途冻结单(OMS下单冻结未释放)则禁止切换,
|
||||
// 否则在途单的解冻/承接逻辑会按新模式错乱。fail-closed:校验服务不可用时拒绝切换
|
||||
if (warehouseDO.getWarehouseId() != null && warehouseDO.getStockLockMode() != null) {
|
||||
WarehousePO existingPO = warehouseDomainService.getInfo(warehouseDO.getWarehouseId());
|
||||
if (existingPO != null) {
|
||||
checkStockLockModeSwitchable(warehouseDO.getWarehouseId(), warehouseDO.getStockLockMode(), existingPO);
|
||||
}
|
||||
}
|
||||
//设置数据字典键值
|
||||
setDataDict(warehouseDO);
|
||||
return warehouseDomainService.update(warehouseDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 单独切换仓库库存锁定模式(不改动仓库其他信息)
|
||||
* 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存
|
||||
* 2-分配库存冻结(默认):WMS「分配库存」时才冻结
|
||||
* 切换守卫与通用编辑一致:模式变化且存在在途冻结单时禁止切换,fail-closed
|
||||
* @author gen
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public Boolean switchStockLockMode(Long warehouseId, Integer stockLockMode) {
|
||||
if (warehouseId == null) {
|
||||
throw new ServiceException("仓库ID不能为空");
|
||||
}
|
||||
if (stockLockMode == null || (stockLockMode.intValue() != 1 && stockLockMode.intValue() != 2)) {
|
||||
throw new ServiceException("库存锁定模式不合法,仅支持 1-下单冻结 2-分配库存冻结");
|
||||
}
|
||||
WarehousePO existingPO = warehouseDomainService.getInfo(warehouseId);
|
||||
if (existingPO == null) {
|
||||
throw new ServiceException("仓库不存在");
|
||||
}
|
||||
checkStockLockModeSwitchable(warehouseId, stockLockMode, existingPO);
|
||||
return warehouseDomainService.switchStockLockMode(warehouseId, stockLockMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 库存锁定模式切换守卫:模式未变化直接放行;模式变化时查询WMS是否存在
|
||||
* 在途下单冻结单(freeze_status 1/2),存在则禁止切换。
|
||||
* fail-closed:WMS服务不可用/返回异常时一律拒绝切换,防止切换后冻结语义错乱
|
||||
* @author gen
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
private void checkStockLockModeSwitchable(Long warehouseId, Integer newMode, WarehousePO existingPO) {
|
||||
if (newMode.compareTo(cn.hutool.core.util.ObjectUtil.defaultIfNull(existingPO.getStockLockMode(), 2)) == 0) {
|
||||
return;
|
||||
}
|
||||
AjaxResult freezeCheck = wmsServiceFeign.hasActiveFreezeByWarehouse(warehouseId);
|
||||
if (freezeCheck == null || !"200".equals(String.valueOf(freezeCheck.get("code")))) {
|
||||
throw new ServiceException("库存锁定模式切换校验失败,请稍后重试:"
|
||||
+ (freezeCheck == null ? "WMS服务不可用" : freezeCheck.get("msg")));
|
||||
}
|
||||
if (Boolean.TRUE.equals(freezeCheck.get("data"))) {
|
||||
throw new ServiceException("该仓库存在下单冻结中的出库单,不可切换库存锁定模式");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 设置数据字典键值
|
||||
* @author ZhouGY
|
||||
|
||||
+4
@@ -130,6 +130,10 @@ public class Warehouse extends BaseVOEntity{
|
||||
@Excel(name = "作业模式")
|
||||
private String workMode;
|
||||
|
||||
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
private Integer stockLockMode;
|
||||
|
||||
@ApiModelProperty("订单号生成规则0日期累加1历史累加")
|
||||
@Excel(name = "单号生成规则")
|
||||
private String orderNoGenerateRule;
|
||||
|
||||
+4
@@ -132,6 +132,10 @@ public class WarehousePO extends BaseVOEntity{
|
||||
@Excel(name = "作业模式")
|
||||
private String workMode;
|
||||
|
||||
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
private Integer stockLockMode;
|
||||
|
||||
|
||||
@ApiModelProperty("订单号生成规则")
|
||||
@Excel(name = "单号生成规则")
|
||||
|
||||
+4
@@ -140,6 +140,10 @@ public class WarehouseDO extends BaseVOEntity{
|
||||
@Excel(name = "作业模式")
|
||||
private String workMode;
|
||||
|
||||
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
private Integer stockLockMode;
|
||||
|
||||
|
||||
@ApiModelProperty("订单号生成规则")
|
||||
@Excel(name = "单号生成规则")
|
||||
|
||||
+23
@@ -142,6 +142,29 @@ public class WarehouseDomainService {
|
||||
.eq(Warehouse::getDelFlag, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 单独切换仓库库存锁定模式(仅更新 STOCK_LOCK_MODE 与审计字段,不触碰仓库其他数据)
|
||||
* 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存
|
||||
* 2-分配库存冻结(默认):WMS「分配库存」时才冻结
|
||||
* @author gen
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public Boolean switchStockLockMode(Long warehouseId, Integer stockLockMode) {
|
||||
Long updateBy = null;
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
updateBy = loginUser == null ? null : loginUser.getUserid();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
Long finalUpdateBy = updateBy;
|
||||
return warehouseService.update(new UpdateWrapper<Warehouse>().lambda()
|
||||
.set(Warehouse::getStockLockMode, stockLockMode)
|
||||
.set(finalUpdateBy != null, Warehouse::getUpdateBy, finalUpdateBy)
|
||||
.set(Warehouse::getUpdateTime, new Date())
|
||||
.eq(Warehouse::getWarehouseId, warehouseId)
|
||||
.eq(Warehouse::getDelFlag, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取指定仓库下的所有库区及库区下所有库位
|
||||
* @author ZhouGY
|
||||
|
||||
+4
@@ -145,6 +145,10 @@ public class WarehouseDTO extends BaseVOEntity{
|
||||
@Excel(name = "作业模式")
|
||||
private String workMode;
|
||||
|
||||
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
private Integer stockLockMode;
|
||||
|
||||
|
||||
@ApiModelProperty("订单号生成规则")
|
||||
@Excel(name = "单号生成规则")
|
||||
|
||||
+27
@@ -104,6 +104,33 @@ public class WarehouseApi extends BaseController{
|
||||
return AjaxResult.success(warehouseApplicationService.getInfo(warehouseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仓库库存锁定模式(OMS下单冻结库存/防超卖功能的仓库级开关):
|
||||
* 1-下单冻结(OMS出库业务单「下单」即冻结WMS库存:可用-、冻结+,库存数量不变;
|
||||
* 分配不动数量;编辑=原子重锁;取消=释放;交接=库存-冻结-)
|
||||
* 2-分配库存冻结(默认,现有行为:WMS「分配库存」时才 可用-冻结+)
|
||||
* 仓库不存在或未配置时返回 2(与 OMS/WMS 代码侧降级口径一致)
|
||||
*/
|
||||
@ApiOperation("查询仓库库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
@GetMapping(value = "/getStockLockMode/{warehouseId}")
|
||||
public AjaxResult getStockLockMode(@PathVariable("warehouseId") Long warehouseId)
|
||||
{
|
||||
return AjaxResult.success(warehouseApplicationService.getStockLockMode(warehouseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单独切换仓库库存锁定模式(不改动仓库其他信息):
|
||||
* 模式变化且该仓库存在在途下单冻结出库单(freeze_status 1/2)时拒绝切换;
|
||||
* WMS校验服务不可用时同样拒绝(fail-closed)
|
||||
*/
|
||||
@ApiOperation("切换仓库库存锁定模式: 1-下单冻结 2-分配库存冻结")
|
||||
@PostMapping(value = "/switchStockLockMode/{warehouseId}/{stockLockMode}")
|
||||
public AjaxResult switchStockLockMode(@PathVariable("warehouseId") Long warehouseId,
|
||||
@PathVariable("stockLockMode") Integer stockLockMode)
|
||||
{
|
||||
return toAjax(warehouseApplicationService.switchStockLockMode(warehouseId, stockLockMode));
|
||||
}
|
||||
|
||||
|
||||
@ApiOperation("批量作废仓库")
|
||||
@GetMapping("/nullifyByIds/{warehouseIds}")
|
||||
|
||||
+2
-1
@@ -43,6 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="orderNoGenerateRule" column="ORDER_NO_GENERATE_RULE" />
|
||||
<result property="inOrderNoPrefix" column="IN_ORDER_NO_PREFIX" />
|
||||
<result property="outOrderNoPrefix" column="OUT_ORDER_NO_PREFIX" />
|
||||
<result property="stockLockMode" column="STOCK_LOCK_MODE" />
|
||||
</resultMap>
|
||||
|
||||
|
||||
@@ -51,7 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
warehouse_type, warehouse_type_name, warehouse_structure, warehouse_nature, warm_layer_code, warm_layer_name,
|
||||
director_name, director_phone, region_code, region_name, detail_address, longitude, latitude, building_region,
|
||||
usage_region, remark, opening_up, nullify, create_time, create_by, create_by_name, update_time, update_by,
|
||||
update_by_name, del_flag, IS_ENABLE ,WORK_MODE,TASK_MODE,SAMPLE_OUT_ORDER_NO_PREFIX,ORDER_NO_GENERATE_RULE,IN_ORDER_NO_PREFIX,
|
||||
update_by_name, del_flag, IS_ENABLE ,WORK_MODE,TASK_MODE,STOCK_LOCK_MODE,SAMPLE_OUT_ORDER_NO_PREFIX,ORDER_NO_GENERATE_RULE,IN_ORDER_NO_PREFIX,
|
||||
OUT_ORDER_NO_PREFIX
|
||||
from warehouse
|
||||
</sql>
|
||||
|
||||
+45
@@ -11,6 +11,7 @@ import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
|
||||
import com.mhd.oms.domain.outEntrustOrder.service.OutEntrustOrderDomainService;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.listener.OutEntrustExcelParseService;
|
||||
import com.mhd.system.api.SystemServiceFeign;
|
||||
import com.mhd.system.api.WmsServiceFeign;
|
||||
import com.mhd.system.api.domain.NoticeMaterialDetailDTO;
|
||||
@@ -20,8 +21,10 @@ import com.mhd.system.api.model.LoginUser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
@@ -52,6 +55,9 @@ public class OutEntrustOrderApplicationService {
|
||||
@Resource
|
||||
private WmsServiceFeign wmsServiceFeign;
|
||||
|
||||
@Resource
|
||||
private OutEntrustExcelParseService outEntrustExcelParseService;
|
||||
|
||||
/**
|
||||
* 分页查询出仓委托单列表(不做默认状态过滤,显示全部数据)
|
||||
*/
|
||||
@@ -108,6 +114,45 @@ public class OutEntrustOrderApplicationService {
|
||||
return outEntrustOrderDomainService.insert(outEntrustOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入出仓委托单(模板与WMS出库管理导入一致:前11行表头+第12行起明细)
|
||||
* 解析校验通过后复用新增链路落库:生成CKWT委托单号,状态待审核/未下发
|
||||
*
|
||||
* @return 校验错误信息;空串表示导入成功
|
||||
*/
|
||||
public StringBuilder importOutEntrustOrder(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) throws IOException {
|
||||
//解析+校验(客户解析/物料匹配/字典转换/必填项)
|
||||
OutEntrustExcelParseService.ParseResult parseResult =
|
||||
outEntrustExcelParseService.parseOutEntrustOrderExcel(file, warehouseName, warehouseCode, warehouseId);
|
||||
if (parseResult.getErrorMsg().length() > 0) {
|
||||
return parseResult.getErrorMsg();
|
||||
}
|
||||
OutEntrustOrderDO outEntrustOrderDO = parseResult.getOrder();
|
||||
//审计信息(与WMS出库导入一致,导入人=当前登录人)
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
String userRealName;
|
||||
if (loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
|
||||
&& !loginUser.getUserPo().getUserName().trim().isEmpty()) {
|
||||
userRealName = loginUser.getUserPo().getUserName();
|
||||
} else if (loginUser.getRealname() != null && !loginUser.getRealname().trim().isEmpty()) {
|
||||
userRealName = loginUser.getRealname();
|
||||
} else {
|
||||
userRealName = loginUser.getUsername();
|
||||
}
|
||||
Date now = new Date();
|
||||
outEntrustOrderDO.setCreateBy(loginUser.getUserid());
|
||||
outEntrustOrderDO.setCreateByName(userRealName);
|
||||
outEntrustOrderDO.setCreateTime(now);
|
||||
outEntrustOrderDO.setUpdateBy(loginUser.getUserid());
|
||||
outEntrustOrderDO.setUpdateByName(userRealName);
|
||||
outEntrustOrderDO.setUpdateTime(now);
|
||||
}
|
||||
//复用新增链路:必填校验→默认普通出库→仓库校验→组织信息→汇总申报数量→生成CKWT委托单号(待审核/未下发)
|
||||
insert(outEntrustOrderDO);
|
||||
return new StringBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改出仓委托单(仅待审核/已驳回且未下发可修改;已驳回单编辑后状态重置为待审核)
|
||||
*/
|
||||
|
||||
+6
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.mhd.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
|
||||
|
||||
import java.util.List;
|
||||
@@ -21,6 +22,11 @@ public interface IExecutionStockOutOrderService extends IService<ExecutionStockO
|
||||
*/
|
||||
public List<ExecutionStockOutOrderPO> queryList(ExecutionStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 新增出库单
|
||||
*/
|
||||
|
||||
+6
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mhd.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
//import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
|
||||
@@ -23,6 +24,11 @@ public interface ExecutionStockOutOrderMapper extends BaseMapper<ExecutionStockO
|
||||
*/
|
||||
public List<ExecutionStockOutOrderPO> queryList(ExecutionStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 出库单提货单打印-表头
|
||||
*/
|
||||
|
||||
+10
@@ -6,6 +6,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -34,6 +35,15 @@ public class ExecutionStockOutOrderImpl extends ServiceImpl<ExecutionStockOutOrd
|
||||
return reservationStockOutOrderMapper.queryList(stockOutOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
@Override
|
||||
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO)
|
||||
{
|
||||
return reservationStockOutOrderMapper.queryListTotal(stockOutOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增出库单
|
||||
*/
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.mhd.oms.domain.executionStockOutOrder.repository.po;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 出库执行单列表合计PO
|
||||
*
|
||||
* <p>当前筛选条件下不分页的全量合计,随分页列表一起返回
|
||||
* (totalPlanQuantity/totalOutboundQuantity),计划数量与出库数量分开计算</p>
|
||||
*
|
||||
* @author rcx
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "出库执行单列表合计对象", description = "出库执行单列表合计响应对象")
|
||||
public class ExecutionStockOutOrderTotalPO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("计划数量总数(一级明细计划数量之和)")
|
||||
private BigDecimal totalPlanQuantity;
|
||||
|
||||
@ApiModelProperty("出库数量总数(一级明细出库数量之和)")
|
||||
private BigDecimal totalOutboundQuantity;
|
||||
}
|
||||
+10
@@ -26,6 +26,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDTO;
|
||||
import com.mhd.oms.domain.gwLog.entity.GwLog;
|
||||
@@ -225,6 +226,15 @@ public class ExecutionStockOutOrderApplicationService {
|
||||
return stockOutOrderPOS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量。
|
||||
* 须在 queryList/queryList1 之后用同一个DO调用:数据权限条件由列表查询原地写入DO,
|
||||
* 合计查询才能继承生效后的筛选;且 PageHelper startPage 只拦截下一条SQL,本查询不会被分页。
|
||||
*/
|
||||
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO) {
|
||||
return stockOutOrderDomainService.queryListTotal(stockOutOrderDO);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 新增出库单
|
||||
|
||||
+15
-3
@@ -30,6 +30,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
|
||||
import com.mhd.oms.domain.materialBaseInfo.service.MaterialBaseInfoResolveService;
|
||||
import com.mhd.oms.domain.reservationDeliveryDetailsLink.entity.ReservationDeliveryDetailsLink;
|
||||
@@ -114,6 +115,13 @@ public class ExecutionStockOutOrderDomainService {
|
||||
return stockOutOrderService.queryList(stockOutOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO) {
|
||||
return stockOutOrderService.queryListTotal(stockOutOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从单号列表中提取最大序号
|
||||
* 单号格式: prefix + 数字序号(如 RK260807001 中 prefix=RK260807, 序号=001)
|
||||
@@ -440,8 +448,9 @@ public class ExecutionStockOutOrderDomainService {
|
||||
BigDecimal freeze = freezeAfterQuantity.subtract(freezeBeforeQuantity);
|
||||
BigDecimal allocationQuantity = reservationMaterialInventory.getAllocationQuantity();
|
||||
BigDecimal freezeQuantity = reservationMaterialInventory.getFreezeQuantity();
|
||||
allocationQuantity.add(allocation);
|
||||
freezeQuantity.subtract(freeze);
|
||||
// 修复:BigDecimal 不可变,add/subtract 返回新对象必须回赋(原实现未回赋,取消下发的镜像库存还原实际不生效)
|
||||
allocationQuantity = (allocationQuantity == null ? BigDecimal.ZERO : allocationQuantity).add(allocation);
|
||||
freezeQuantity = (freezeQuantity == null ? BigDecimal.ZERO : freezeQuantity).subtract(freeze);
|
||||
reservationMaterialInventory.setAllocationQuantity(allocationQuantity);
|
||||
reservationMaterialInventory.setFreezeQuantity(freezeQuantity);
|
||||
reservationMaterialInventory.setUpdateTime(new Date());
|
||||
@@ -505,7 +514,7 @@ public class ExecutionStockOutOrderDomainService {
|
||||
checkDetail.setMaterialName(detail.getMaterialName());
|
||||
checkList.add(checkDetail);
|
||||
}
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList, byId.getBusinessOutOrderNumber());
|
||||
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
|
||||
validateErrors.add("出库单[" + byId.getOutOrderNumber() + "]库存校验失败:"
|
||||
+ (checkResult == null ? "WMS服务不可用,已阻止下发" : checkResult.get("msg")));
|
||||
@@ -527,6 +536,9 @@ public class ExecutionStockOutOrderDomainService {
|
||||
try {
|
||||
StockOutOrder stockOutOrder = new StockOutOrder();
|
||||
BeanUtils.copyProperties(byId, stockOutOrder,"outOrderId");
|
||||
// 下单冻结模式:WMS单头带上业务单号(字段名不同copyProperties不会复制,必须显式set),
|
||||
// 供WMS分配承接/下发校验做【单据级】冻结记录过滤(双单冻结同行防误消费)
|
||||
stockOutOrder.setBusinessOrderNo(byId.getBusinessOutOrderNumber());
|
||||
Long warehouseId = stockOutOrder.getWarehouseId();
|
||||
Long topOrgId = stockInOrderMapper.getTopOrgId(warehouseId);
|
||||
Long orgId = stockInOrderMapper.getOrgId(warehouseId);
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
|
||||
|
||||
import com.alibaba.excel.converters.Converter;
|
||||
import com.alibaba.excel.enums.CellDataTypeEnum;
|
||||
import com.alibaba.excel.metadata.GlobalConfiguration;
|
||||
import com.alibaba.excel.metadata.data.ReadCellData;
|
||||
import com.alibaba.excel.metadata.property.ExcelContentProperty;
|
||||
|
||||
/**
|
||||
* 自定义转换器,不进行任何类型转换,直接返回原始值
|
||||
* 覆盖EasyExcel默认的StringNumberConverter,避免数字格式化错误
|
||||
*
|
||||
* @author rcx
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
public class NoConvertConverter implements Converter<String> {
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
// 关键:返回NUMBER类型,覆盖StringNumberConverter
|
||||
return CellDataTypeEnum.NUMBER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 不进行任何格式化,直接返回原始值的字符串表示
|
||||
if (cellData == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 根据单元格类型获取原始值
|
||||
try {
|
||||
CellDataTypeEnum type = cellData.getType();
|
||||
if (type == CellDataTypeEnum.STRING) {
|
||||
return cellData.getStringValue() != null ? cellData.getStringValue() : "";
|
||||
} else if (type == CellDataTypeEnum.NUMBER) {
|
||||
// 直接转换数字为字符串,不进行格式化
|
||||
return cellData.getNumberValue() != null ? cellData.getNumberValue().toString() : "";
|
||||
} else if (type == CellDataTypeEnum.BOOLEAN) {
|
||||
return cellData.getBooleanValue() != null ? cellData.getBooleanValue().toString() : "";
|
||||
} else if (type == CellDataTypeEnum.EMPTY) {
|
||||
return "";
|
||||
} else {
|
||||
// 其他类型尝试获取字符串值
|
||||
return cellData.getStringValue() != null ? cellData.getStringValue() : "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果获取失败,返回空字符串
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.ExcelReader;
|
||||
import com.alibaba.excel.read.metadata.ReadSheet;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.alibaba.fastjson2.TypeReference;
|
||||
import com.mhd.common.core.domain.po.SysDictDataVo;
|
||||
import com.mhd.common.core.domain.po.UserPo;
|
||||
import com.mhd.common.core.exception.ServiceException;
|
||||
import com.mhd.common.core.utils.StringUtils;
|
||||
import com.mhd.common.core.web.domain.AjaxResult;
|
||||
import com.mhd.common.security.utils.SecurityUtils;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.mapper.OutEntrustOrderMapper;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
|
||||
import com.mhd.system.api.SystemServiceFeign;
|
||||
import com.mhd.system.api.UserServiceFeign;
|
||||
import com.mhd.system.api.model.LoginUser;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 出仓委托单Excel解析服务
|
||||
* 模板与WMS出库管理导入一致:前11行为表头,第12行起为明细
|
||||
*
|
||||
* @author rcx
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OutEntrustExcelParseService {
|
||||
|
||||
@Resource
|
||||
private SystemServiceFeign systemServiceFeign;
|
||||
|
||||
@Resource
|
||||
private UserServiceFeign userServiceFeign;
|
||||
|
||||
@Autowired
|
||||
private OutEntrustOrderMapper outEntrustOrderMapper;
|
||||
|
||||
/**
|
||||
* 解析结果:委托单对象 + 校验错误信息(空串表示通过)
|
||||
*/
|
||||
@Getter
|
||||
public static class ParseResult {
|
||||
private final OutEntrustOrderDO order;
|
||||
private final StringBuilder errorMsg;
|
||||
|
||||
public ParseResult(OutEntrustOrderDO order, StringBuilder errorMsg) {
|
||||
this.order = order;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出仓委托单Excel(不落库,落库由应用服务走新增链路)
|
||||
*
|
||||
* @param file 上传的Excel文件
|
||||
* @param warehouseName 仓库名称(导入弹窗选择)
|
||||
* @param warehouseCode 仓库编码
|
||||
* @param warehouseId 仓库ID
|
||||
* @return 解析结果:委托单对象 + 校验错误信息;错误信息非空时不应落库
|
||||
*/
|
||||
public ParseResult parseOutEntrustOrderExcel(MultipartFile file, String warehouseName, String warehouseCode, Long warehouseId) throws IOException {
|
||||
|
||||
OutEntrustHeaderListener headerListener = new OutEntrustHeaderListener();
|
||||
|
||||
ReadSheet headerSheet = EasyExcel.readSheet(0)
|
||||
.headRowNumber(0)
|
||||
.build();
|
||||
|
||||
ExcelReader headerReader = EasyExcel.read(file.getInputStream())
|
||||
.autoCloseStream(false)
|
||||
.registerReadListener(headerListener)
|
||||
.registerConverter(new NoConvertConverter())
|
||||
.headRowNumber(0)
|
||||
.build();
|
||||
|
||||
// 通过监听器控制只读取前11行
|
||||
headerReader.read(headerSheet);
|
||||
|
||||
InputStream itemInputStream = file.getInputStream();
|
||||
OutEntrustItemListener itemListener = new OutEntrustItemListener();
|
||||
|
||||
// 商品明细需要跳过前11行,从第12行开始读取
|
||||
ExcelReader itemReader = EasyExcel.read(itemInputStream)
|
||||
.autoCloseStream(false)
|
||||
.registerReadListener(itemListener)
|
||||
.registerConverter(new NoConvertConverter())
|
||||
.headRowNumber(12)
|
||||
.build();
|
||||
|
||||
ReadSheet itemSheet = EasyExcel.readSheet(0).build();
|
||||
itemReader.read(itemSheet);
|
||||
|
||||
itemReader.finish();
|
||||
List<OutEntrustOrderDetailDO> itemList = itemListener.getItemList();
|
||||
OutEntrustOrderDO outEntrustOrder = headerListener.getOutEntrustOrder();
|
||||
|
||||
// 提前获取货主ID,用于查询物料基础信息时过滤
|
||||
String customerName = outEntrustOrder.getShipperName();
|
||||
UserPo shipperUser = resolveCustomerShipperUserPoForImport(customerName);
|
||||
Long shipperId = shipperUser != null ? shipperUser.getUserId() : null;
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
Long topOrganizationId = loginUser != null && loginUser.getUserPo() != null
|
||||
? loginUser.getUserPo().getTopOrganizationId() : null;
|
||||
initList(itemList, shipperId, topOrganizationId);
|
||||
outEntrustOrder.setMaterialDetailList(itemList);
|
||||
outEntrustOrder.setWarehouseName(warehouseName);
|
||||
outEntrustOrder.setWarehouseCode(warehouseCode);
|
||||
outEntrustOrder.setWarehouseId(warehouseId);
|
||||
|
||||
//业务员默认当前登录人
|
||||
if (loginUser != null) {
|
||||
outEntrustOrder.setSalesmanId(String.valueOf(loginUser.getUserid()));
|
||||
outEntrustOrder.setSalesmanName(loginUser.getUserPo() != null ? loginUser.getUserPo().getUserName() : loginUser.getUsername());
|
||||
}
|
||||
//货主信息回填(名称/编号以用户中心为准)
|
||||
if (shipperUser != null) {
|
||||
outEntrustOrder.setShipperId(shipperUser.getUserId());
|
||||
outEntrustOrder.setShipperCode(shipperUser.getUserMemberCode());
|
||||
outEntrustOrder.setShipperName(shipperUser.getUserName());
|
||||
}
|
||||
initDict(outEntrustOrder);
|
||||
//校验必填字段
|
||||
StringBuilder errorMsg = check(outEntrustOrder);
|
||||
// 日期单元格有值但解析失败:与物料一致性错误同等对待,阻断导入并明确提示,杜绝静默丢日期
|
||||
for (String dateError : headerListener.getDateParseErrors()) {
|
||||
errorMsg.append(dateError).append("; ");
|
||||
}
|
||||
return new ParseResult(outEntrustOrder, errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验导入明细是否已在当前货主下维护物料基础信息
|
||||
*/
|
||||
private StringBuilder validateShipperMaterialDetails(OutEntrustOrderDO outEntrustOrder) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<OutEntrustOrderDetailDO> details = outEntrustOrder.getMaterialDetailList();
|
||||
if (details == null || details.isEmpty()) {
|
||||
return sb;
|
||||
}
|
||||
Long shipperId = outEntrustOrder.getShipperId();
|
||||
for (OutEntrustOrderDetailDO row : details) {
|
||||
if (row.getMaterialBaseInfoId() != null) {
|
||||
continue;
|
||||
}
|
||||
boolean hasIdentifier = StringUtils.isNotBlank(row.getMaterialNo())
|
||||
|| StringUtils.isNotBlank(row.getSkucode())
|
||||
|| StringUtils.isNotBlank(row.getCommodityName());
|
||||
if (!hasIdentifier) {
|
||||
continue;
|
||||
}
|
||||
String desc = buildImportMaterialRowDesc(row);
|
||||
if (ObjectUtil.isEmpty(shipperId)) {
|
||||
sb.append("请先确认客户(货主)信息;无法校验以下物料是否已在货主下维护:").append(desc).append("; ");
|
||||
} else {
|
||||
sb.append("当前货主下未维护以下物料:").append(desc).append("; ");
|
||||
}
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示文案只强调 Excel 导入列「商品名称」;商品名称为空时用导入料号/SKU 辅助说明。
|
||||
*/
|
||||
private String buildImportMaterialRowDesc(OutEntrustOrderDetailDO row) {
|
||||
String commodityName = StringUtils.trimToEmpty(row.getCommodityName());
|
||||
if (StringUtils.isNotBlank(commodityName)) {
|
||||
return "商品名称「" + commodityName + "」";
|
||||
}
|
||||
String materialNo = StringUtils.trimToEmpty(row.getMaterialNo());
|
||||
String sku = StringUtils.trimToEmpty(row.getSkucode());
|
||||
StringBuilder b = new StringBuilder("导入商品名称为空");
|
||||
if (StringUtils.isNotBlank(materialNo)) {
|
||||
b.append(",料号「").append(materialNo).append("」");
|
||||
}
|
||||
if (StringUtils.isNotBlank(sku)) {
|
||||
b.append(",SKU「").append(sku).append("」");
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
private StringBuilder check(OutEntrustOrderDO outEntrustOrder) {
|
||||
StringBuilder errorMsg = new StringBuilder();
|
||||
errorMsg.append(validateShipperMaterialDetails(outEntrustOrder));
|
||||
if (outEntrustOrder.getMaterialDetailList() == null || outEntrustOrder.getMaterialDetailList().isEmpty()) {
|
||||
errorMsg.append("物料明细列表不能为空或匹配不上物料信息; ");
|
||||
}
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getToReceiver())) errorMsg.append("To不能为空; ");
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getCarrier())) errorMsg.append("承运商不能为空; ");
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getTransportModeCode())) errorMsg.append("运输方式不能为空; ");
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getSupervisionModeCode())) errorMsg.append("监督方式不能为空; ");
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getDepartureArrivalCountry())) errorMsg.append("出发/到达国家不能为空; ");
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getContactPerson())) errorMsg.append("联系人不能为空; ");
|
||||
if (ObjectUtil.isEmpty(outEntrustOrder.getShipperId())) errorMsg.append("客户名称不能为空或匹配不上客户信息; ");
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getDeclareCustoms())) errorMsg.append("申报地关区不能为空; ");
|
||||
if (StringUtils.isEmpty(outEntrustOrder.getEntryExitCustoms())) errorMsg.append("进出境关别不能为空; ");
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 初始化物料明细列表,查询物料基础信息并设置物料基础信息ID(与WMS出库管理导入逻辑一致:料号 > SKU码 > 商品名称)
|
||||
* @param itemList 出仓委托单明细列表
|
||||
* @param shipperId 货主ID(用于过滤物料基础信息,确保查询到的是该货主绑定的物料)
|
||||
* @param topOrganizationId 一级组织ID(用于过滤物料基础信息)
|
||||
*/
|
||||
private void initList(List<OutEntrustOrderDetailDO> itemList, Long shipperId, Long topOrganizationId) {
|
||||
for (OutEntrustOrderDetailDO detail : itemList) {
|
||||
// 未识别货主时不查物料,避免匹配到其他货主的物料基础信息
|
||||
if (shipperId == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 根据明细表中的字段查询物料基础信息(优先:料号 > 商品SKU码 > 商品名称)
|
||||
MaterialBaseInfoPO materialBaseInfoPO = null;
|
||||
|
||||
// 优先使用料号查询(materialNo对应materialCode)
|
||||
String materialNo = detail.getMaterialNo();
|
||||
if (StringUtils.isNotBlank(materialNo)) {
|
||||
List<MaterialBaseInfoPO> materialList = outEntrustOrderMapper.getMaterialByMaterialCode(materialNo, shipperId, topOrganizationId);
|
||||
if (materialList != null && !materialList.isEmpty()) {
|
||||
materialBaseInfoPO = materialList.get(0);
|
||||
// ==== 料号与规格型号/SKU码一致性校验:任何一项不符直接报对应错误 ====
|
||||
// 双方(trim后)必须完全一致:Excel留空而档案有值、或Excel填了值而档案为空,均视为不一致报错
|
||||
String excelSpec = StringUtils.trimToEmpty(detail.getSpecificationModel());
|
||||
String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel());
|
||||
if (!excelSpec.equals(dbSpec)) {
|
||||
throw new ServiceException(String.format("导入失败:料号「%s」规格型号与物料档案不一致(Excel:%s / 档案:%s)",
|
||||
materialNo, excelSpec.isEmpty() ? "空" : excelSpec, dbSpec.isEmpty() ? "空" : dbSpec));
|
||||
}
|
||||
String excelSku = StringUtils.trimToEmpty(detail.getSkucode());
|
||||
String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode());
|
||||
if (!excelSku.equals(dbSku)) {
|
||||
throw new ServiceException(String.format("导入失败:料号「%s」SKU码与物料档案不一致(Excel:%s / 档案:%s)",
|
||||
materialNo, excelSku.isEmpty() ? "空" : excelSku, dbSku.isEmpty() ? "空" : dbSku));
|
||||
}
|
||||
} else {
|
||||
// 料号已填写但在该货主下查不到 → 直接报错(与WMS出库导入一致),不再被SKU码/商品名称静默兜底
|
||||
throw new ServiceException(String.format("导入失败:料号「%s」在当前客户(货主)下不存在,请核对料号或在物料管理中维护", materialNo));
|
||||
}
|
||||
}
|
||||
|
||||
// 如果料号查询不到,使用商品SKU码查询(skucode对应barCode)
|
||||
if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getSkucode())) {
|
||||
List<MaterialBaseInfoPO> materialList = outEntrustOrderMapper.getMaterialByBarCode(detail.getSkucode(), shipperId, topOrganizationId);
|
||||
if (materialList != null && !materialList.isEmpty()) {
|
||||
materialBaseInfoPO = materialList.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果料号和SKU码都查询不到,使用商品名称查询(commodityName对应materialName)
|
||||
if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getCommodityName())) {
|
||||
List<MaterialBaseInfoPO> materialList = outEntrustOrderMapper.getMaterialByMaterialName(detail.getCommodityName(), shipperId, topOrganizationId);
|
||||
if (materialList != null && !materialList.isEmpty()) {
|
||||
materialBaseInfoPO = materialList.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置物料基础信息ID,并从物料基础信息中回填料号、物料编码、物料名称等信息
|
||||
if (materialBaseInfoPO != null) {
|
||||
detail.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId());
|
||||
// 单价统一从物料档案回填
|
||||
detail.setUnitPrice(materialBaseInfoPO.getUnitPrice());
|
||||
// 如果Excel中没有料号,从物料基础信息中回填
|
||||
if (StringUtils.isBlank(detail.getMaterialNo())) {
|
||||
detail.setMaterialNo(materialBaseInfoPO.getMaterialCode());
|
||||
}
|
||||
// 回填物料编码
|
||||
if (StringUtils.isBlank(detail.getMaterialCode())) {
|
||||
detail.setMaterialCode(materialBaseInfoPO.getMaterialCode());
|
||||
}
|
||||
// 回填商品编号
|
||||
if (StringUtils.isBlank(detail.getCommodityNo())) {
|
||||
detail.setCommodityNo(materialBaseInfoPO.getMaterialCode());
|
||||
}
|
||||
// 回填物料名称(如果Excel中的商品名称为空,使用物料基础信息中的名称)
|
||||
if (StringUtils.isBlank(detail.getCommodityName())) {
|
||||
detail.setCommodityName(materialBaseInfoPO.getMaterialName());
|
||||
}
|
||||
// 回填物料名称到materialName字段
|
||||
detail.setMaterialName(materialBaseInfoPO.getMaterialName());
|
||||
// 回填条形码
|
||||
detail.setBarCode(materialBaseInfoPO.getBarCode());
|
||||
// 如果Excel中没有SKU码,从物料基础信息中回填
|
||||
if (StringUtils.isBlank(detail.getSkucode())) {
|
||||
detail.setSkucode(materialBaseInfoPO.getBarCode());
|
||||
}
|
||||
}
|
||||
// 单位:优先使用Excel中的"单位"字段,如果为空则使用"申报单位",如果还为空则从物料主数据获取(与WMS出库导入处理一致)
|
||||
String unit = StringUtils.isNotBlank(detail.getUnit()) ? detail.getUnit() : null;
|
||||
if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(detail.getDeclareUnit())) {
|
||||
unit = detail.getDeclareUnit();
|
||||
}
|
||||
if (StringUtils.isBlank(unit) && materialBaseInfoPO != null && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) {
|
||||
unit = materialBaseInfoPO.getUnitName();
|
||||
}
|
||||
detail.setUnit(unit);
|
||||
}
|
||||
}
|
||||
|
||||
private void initDict(OutEntrustOrderDO outEntrustOrder) {
|
||||
if (ObjectUtil.isNotNull(outEntrustOrder.getContainerNoName())) {
|
||||
AjaxResult containerNo = systemServiceFeign.selectListByDictType("cabinetNo");
|
||||
if (ObjectUtil.isNotNull(containerNo) && "200".equals(String.valueOf(containerNo.get("code")))) {
|
||||
List<SysDictDataVo> containerNoList = JSON.parseArray(JSONObject.toJSONString(containerNo.get("data")), SysDictDataVo.class);
|
||||
for (SysDictDataVo sysDictDataVo : containerNoList) {
|
||||
if (outEntrustOrder.getContainerNoName().equals(sysDictDataVo.getDictLabel())) {
|
||||
outEntrustOrder.setContainerNo(sysDictDataVo.getDictValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ObjectUtil.isNotNull(outEntrustOrder.getTransportModeName())) {
|
||||
AjaxResult transportMode = systemServiceFeign.selectListByDictType("transportMode");
|
||||
if (ObjectUtil.isNotNull(transportMode) && "200".equals(String.valueOf(transportMode.get("code")))) {
|
||||
List<SysDictDataVo> transportModeList = JSON.parseArray(JSONObject.toJSONString(transportMode.get("data")), SysDictDataVo.class);
|
||||
for (SysDictDataVo sysDictDataVo : transportModeList) {
|
||||
if (outEntrustOrder.getTransportModeName().equals(sysDictDataVo.getDictLabel())) {
|
||||
outEntrustOrder.setTransportModeCode(sysDictDataVo.getDictValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ObjectUtil.isNotNull(outEntrustOrder.getSupervisionModeName())) {
|
||||
AjaxResult supervisionMode = systemServiceFeign.selectListByDictType("controlType");
|
||||
if (ObjectUtil.isNotNull(supervisionMode) && "200".equals(String.valueOf(supervisionMode.get("code")))) {
|
||||
List<SysDictDataVo> supervisionModeList = JSON.parseArray(JSONObject.toJSONString(supervisionMode.get("data")), SysDictDataVo.class);
|
||||
for (SysDictDataVo sysDictDataVo : supervisionModeList) {
|
||||
if (outEntrustOrder.getSupervisionModeName().equals(sysDictDataVo.getDictLabel())) {
|
||||
outEntrustOrder.setSupervisionModeCode(sysDictDataVo.getDictValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入出库单:解析出的货主用户是否属于当前登录用户同一组织(organizationId)。
|
||||
* 仅当登录用户带有 organizationId 时启用过滤;否则保持与旧行为一致。
|
||||
*/
|
||||
private boolean shipperMatchesImporterOrganization(UserPo userPo, Long loginOrganizationId) {
|
||||
if (loginOrganizationId == null) {
|
||||
return true;
|
||||
}
|
||||
if (userPo == null) {
|
||||
return false;
|
||||
}
|
||||
Long oid = userPo.getOrganizationId();
|
||||
return oid != null && loginOrganizationId.equals(oid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按客户名称或数字ID解析货主用户,逻辑与WMS出库管理导入一致:getInfoByName → 货主列表兜底,均做 organizationId 校验。
|
||||
*/
|
||||
private UserPo resolveCustomerShipperUserPoForImport(String customerName) {
|
||||
if (StringUtils.isEmpty(customerName)) {
|
||||
return null;
|
||||
}
|
||||
Long loginOrganizationId = null;
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null && loginUser.getUserPo() != null) {
|
||||
loginOrganizationId = loginUser.getUserPo().getOrganizationId();
|
||||
}
|
||||
String shipperName = customerName.trim();
|
||||
if (StringUtils.isEmpty(shipperName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 纯数字:按用户ID查询
|
||||
if (shipperName.matches("\\d+")) {
|
||||
try {
|
||||
Long possibleId = Long.valueOf(shipperName);
|
||||
AjaxResult ajaxResult = userServiceFeign.getInfo(possibleId);
|
||||
if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
|
||||
UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
|
||||
if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) {
|
||||
return userPo;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 按名称查询(用户中心)
|
||||
try {
|
||||
AjaxResult ajaxResult = userServiceFeign.getInfoByName(shipperName);
|
||||
if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
|
||||
UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
|
||||
if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) {
|
||||
return userPo;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 继续货主列表兜底
|
||||
}
|
||||
|
||||
// 货主列表兜底(与WMS出库导入一致)
|
||||
try {
|
||||
AjaxResult shipperListResult = userServiceFeign.userShipperListAll();
|
||||
if ("200".equals(String.valueOf(shipperListResult.get("code"))) && shipperListResult.get("data") != null) {
|
||||
Object data = shipperListResult.get("data");
|
||||
List<Map<String, Object>> shipperList = null;
|
||||
if (data instanceof List) {
|
||||
List<Object> rawList = (List<Object>) data;
|
||||
shipperList = new ArrayList<>();
|
||||
for (Object item : rawList) {
|
||||
if (item instanceof Map) {
|
||||
Map<String, Object> mapItem = (Map<String, Object>) item;
|
||||
shipperList.add(mapItem);
|
||||
} else {
|
||||
Map<String, Object> mapItem = JSON.parseObject(JSONObject.toJSONString(item), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
shipperList.add(mapItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String jsonStr = JSONObject.toJSONString(data);
|
||||
shipperList = JSON.parseObject(jsonStr, new TypeReference<List<Map<String, Object>>>() {
|
||||
});
|
||||
}
|
||||
|
||||
if (shipperList != null && !shipperList.isEmpty()) {
|
||||
for (Map<String, Object> shipper : shipperList) {
|
||||
Object userId = shipper.get("userId");
|
||||
Object userName = shipper.get("userName");
|
||||
if (userName == null) {
|
||||
userName = shipper.get("name");
|
||||
}
|
||||
if (userName == null) {
|
||||
userName = shipper.get("shipperName");
|
||||
}
|
||||
boolean matched = false;
|
||||
if (StringUtils.isNotEmpty(shipperName) && userName != null) {
|
||||
String normalizedUserName = String.valueOf(userName).trim();
|
||||
if (shipperName.equals(normalizedUserName) || normalizedUserName.contains(shipperName)) {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
if (matched && userId != null) {
|
||||
Long candidateId = Long.valueOf(String.valueOf(userId));
|
||||
AjaxResult infoRes = userServiceFeign.getInfo(candidateId);
|
||||
if ("200".equals(String.valueOf(infoRes.get("code"))) && infoRes.get("data") != null) {
|
||||
UserPo up = JSON.parseObject(JSONObject.toJSONString(infoRes.get("data")), UserPo.class);
|
||||
if (shipperMatchesImporterOrganization(up, loginOrganizationId)) {
|
||||
return up;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
|
||||
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
|
||||
import lombok.Getter;
|
||||
import org.apache.poi.ss.usermodel.DateUtil; // Excel序列号转日期,POI已随EasyExcel引入
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 出仓委托单导入-表头信息监听器(处理前11行,与WMS出库管理导入模板一致)
|
||||
*
|
||||
* @author rcx
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@Getter
|
||||
public class OutEntrustHeaderListener extends AnalysisEventListener<Map<Integer, Object>> {
|
||||
private final OutEntrustOrderDO outEntrustOrder = new OutEntrustOrderDO();
|
||||
private int rowNum = 0;
|
||||
/** 日期单元格有值但格式无法识别的收集器(类上已有@Getter,自动生成getDateParseErrors()) */
|
||||
private final List<String> dateParseErrors = new ArrayList<>();
|
||||
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
@Override
|
||||
public void invoke(Map<Integer, Object> data, AnalysisContext context) {
|
||||
// 将Map<Integer, Object>转换为Map<Integer, String>
|
||||
Map<Integer, String> stringData = convertToStringMap(data);
|
||||
// 将Map转换为List再转为数组
|
||||
List<String> dataList = mapToList(stringData);
|
||||
Object[] rowData = listToArray(dataList);
|
||||
String stringValue1 = getStringValue(rowData[0]);
|
||||
if (stringValue1 != null && !stringValue1.isEmpty()) {
|
||||
if ("备注:".equals(stringValue1)) {
|
||||
outEntrustOrder.setRemark(getStringValue(rowData[1]));
|
||||
}
|
||||
}
|
||||
// 根据行号提取对应信息
|
||||
switch (rowNum) {
|
||||
case 4: // 第5行:to、仓库联系人、车型及车牌、委托编码
|
||||
outEntrustOrder.setToReceiver(getStringValue(rowData[2]));
|
||||
outEntrustOrder.setContactPerson(getStringValue(rowData[5]));
|
||||
outEntrustOrder.setVehicleModelPlate(getStringValue(rowData[12]));
|
||||
outEntrustOrder.setEntrustNo(getStringValue(rowData[16]));
|
||||
break;
|
||||
case 5: // 第6行:承运方、tel、柜号、司机签名
|
||||
outEntrustOrder.setCarrier(getStringValue(rowData[2]));
|
||||
outEntrustOrder.setTel(getStringValue(rowData[5]));
|
||||
outEntrustOrder.setContainerNoName(getStringValue(rowData[12]));
|
||||
outEntrustOrder.setDriverSign(getStringValue(rowData[16]));
|
||||
break;
|
||||
case 6: // 第7行:出仓日期、fax、运输方式、申报地关区
|
||||
Date outboundDate = parseDateSmart(getStringValue(rowData[2]), "出仓日期");
|
||||
if (outboundDate != null) {
|
||||
outEntrustOrder.setOutboundDate(outboundDate);
|
||||
}
|
||||
outEntrustOrder.setFax(getStringValue(rowData[5]));
|
||||
outEntrustOrder.setTransportModeName(getStringValue(rowData[12]));
|
||||
outEntrustOrder.setDeclareCustoms(getStringValue(rowData[16]));
|
||||
break;
|
||||
case 7: // 第8行:生产商、客户名称、报关姓名及联系方式、完成时间
|
||||
outEntrustOrder.setManufacturer(getStringValue(rowData[2]));
|
||||
outEntrustOrder.setShipperName(getStringValue(rowData[5]));
|
||||
outEntrustOrder.setCustomsDeclarerInfo(getStringValue(rowData[12]));
|
||||
Date completeTime = parseDateSmart(getStringValue(rowData[16]), "完成时间");
|
||||
if (completeTime != null) {
|
||||
outEntrustOrder.setCompleteTime(completeTime);
|
||||
}
|
||||
break;
|
||||
case 8: // 第9行:监管方式、送货地址、海关是否查验货物、进出境关别
|
||||
outEntrustOrder.setSupervisionModeName(getStringValue(rowData[2]));
|
||||
outEntrustOrder.setDeliveryAddr(getStringValue(rowData[5]));
|
||||
String stringValue = getStringValue(rowData[12]);
|
||||
outEntrustOrder.setCustomsInspectionFlag(stringValue);
|
||||
outEntrustOrder.setEntryExitCustoms(getStringValue(rowData[16]));
|
||||
break;
|
||||
case 9: // 第10行:下单日期、起运国
|
||||
Date orderDate = parseDateSmart(getStringValue(rowData[2]), "下单日期");
|
||||
if (orderDate != null) {
|
||||
outEntrustOrder.setOrderDate(orderDate);
|
||||
}
|
||||
outEntrustOrder.setDepartureArrivalCountry(getStringValue(rowData[5]));
|
||||
break;
|
||||
}
|
||||
rowNum++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
// 解析完成后的处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 将List转换为数组
|
||||
*/
|
||||
private Object[] listToArray(List<String> list) {
|
||||
// 创建一个足够大的数组来容纳最多30列数据
|
||||
Object[] array = new Object[30];
|
||||
for (int i = 0; i < 30 && i < list.size(); i++) {
|
||||
array[i] = list.get(i);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Object的Map转换为String的Map
|
||||
*/
|
||||
private Map<Integer, String> convertToStringMap(Map<Integer, Object> dataMap) {
|
||||
if (dataMap == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
Map<Integer, String> stringMap = new HashMap<>();
|
||||
for (Map.Entry<Integer, Object> entry : dataMap.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
String stringValue = value != null ? value.toString() : "";
|
||||
stringMap.put(entry.getKey(), stringValue);
|
||||
}
|
||||
|
||||
return stringMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Map转换为List
|
||||
*/
|
||||
private List<String> mapToList(Map<Integer, String> map) {
|
||||
if (map == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 找到最大索引以确定列表大小
|
||||
int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1;
|
||||
maxSize = Math.max(maxSize, 30); // 至少30个位置
|
||||
|
||||
List<String> list = new ArrayList<>(Collections.nCopies(maxSize, ""));
|
||||
|
||||
for (Map.Entry<Integer, String> entry : map.entrySet()) {
|
||||
if (entry.getKey() >= 0 && entry.getKey() < maxSize) {
|
||||
String value = entry.getValue();
|
||||
list.set(entry.getKey(), value != null ? value : "");
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为字符串,处理null值
|
||||
*/
|
||||
private String getStringValue(Object value) {
|
||||
return value == null ? "" : value.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能解析日期:兼容 Excel 日期单元格序列号 + 常见文本格式。
|
||||
* 解析失败记入 dateParseErrors,由解析服务汇总返回给用户,不再静默丢失。
|
||||
* @param raw 单元格原始字符串
|
||||
* @param fieldLabel 字段中文名(用于错误提示)
|
||||
*/
|
||||
private Date parseDateSmart(String raw, String fieldLabel) {
|
||||
String s = raw == null ? "" : raw.trim();
|
||||
if (s.isEmpty()) {
|
||||
return null; // 单元格为空:保持原行为,不算错误
|
||||
}
|
||||
// 1) Excel 日期序列号:NoConvertConverter 会把真日期单元格转成纯数字字符串(如 46234 = 2026-08-20)
|
||||
// 区间 20000~60000 对应 1954~2064 年,避免把普通数字误判成日期
|
||||
if (s.matches("^\\d{1,5}(\\.\\d+)?$")) {
|
||||
try {
|
||||
double serial = Double.parseDouble(s);
|
||||
if (serial >= 20000 && serial <= 60000) {
|
||||
return DateUtil.getJavaDate(serial);
|
||||
}
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
}
|
||||
// 2) 常见文本格式逐个尝试
|
||||
String[] patterns = {
|
||||
"yyyy-MM-dd", "yyyy/M/d", "yyyy.M.d", "yyyy年M月d日",
|
||||
"yyyy-MM-dd HH:mm:ss", "yyyy/M/d H:mm:ss", "yyyy-MM-dd H:mm"
|
||||
};
|
||||
for (String p : patterns) {
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat(p);
|
||||
sdf.setLenient(false);
|
||||
return sdf.parse(s);
|
||||
} catch (ParseException ignore) {
|
||||
}
|
||||
}
|
||||
dateParseErrors.add(fieldLabel + "「" + s + "」格式无法识别,请改为日期格式或 2026-08-20 / 2026/8/20 / 2026年8月20日 等格式");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
|
||||
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 出仓委托单导入-商品明细监听器(从第12行开始,与WMS出库管理导入模板一致)
|
||||
*
|
||||
* @author rcx
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@Getter
|
||||
public class OutEntrustItemListener extends AnalysisEventListener<Map<Integer, Object>> {
|
||||
private final List<OutEntrustOrderDetailDO> itemList = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void invoke(Map<Integer, Object> data, AnalysisContext context) {
|
||||
// 将Map<Integer, Object>转换为Map<Integer, String>
|
||||
Map<Integer, String> stringData = convertToStringMap(data);
|
||||
// 将Map转换为List再转为数组
|
||||
List<String> dataList = mapToList(stringData);
|
||||
Object[] rowData = listToArray(dataList);
|
||||
|
||||
// 提取序号(第一列),过滤空行和标题行
|
||||
String serialNoStr = getStringValue(rowData[0]);
|
||||
String commodityName = getStringValue(rowData[1]);
|
||||
String materialNo = getStringValue(rowData[2]);
|
||||
|
||||
if (commodityName.isEmpty() && materialNo.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (serialNoStr.isEmpty() || "序号".equals(serialNoStr) || !isSequenceNumber(serialNoStr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
OutEntrustOrderDetailDO item = new OutEntrustOrderDetailDO();
|
||||
|
||||
// 填充商品明细数据
|
||||
//商品名称 料号 规格型号/ITEM 商品SKU码 Invoice No. 申报数量 申报单位 升 箱数 件数 尺寸 出库数量 "总净重
|
||||
//(KG)" 总毛重 (KG) "总体积
|
||||
//(CBM)" 总面积(SQM) Batch Ref NO's Sheet Ref NO's 原产国 箱号/卡板号 总价 币制 备注
|
||||
item.setCommodityName(getStringValue(rowData[1]));
|
||||
item.setMaterialNo(getStringValue(rowData[2]));
|
||||
item.setSpecificationModel(getStringValue(rowData[3]));
|
||||
item.setSkucode(getStringValue(rowData[4]));
|
||||
item.setInvoiceNo(getStringValue(rowData[5]));
|
||||
item.setDeclareQuantity(parseBigDecimal(rowData[6]));
|
||||
item.setDeclareUnit(getStringValue(rowData[7]));
|
||||
item.setLiter(normalizeNumeric(getStringValue(rowData[8])));
|
||||
item.setCaseCount(parseBigDecimal(rowData[9]));
|
||||
item.setPieceCount(parseBigDecimal(rowData[10]));
|
||||
item.setSize(getStringValue(rowData[11]));
|
||||
item.setOutboundQuantity(parseBigDecimal(rowData[12]));
|
||||
item.setTotalNetWeight(parseBigDecimal(rowData[13]));
|
||||
item.setTotalGrossWeight(parseBigDecimal(rowData[14]));
|
||||
item.setTotalVolume(parseBigDecimal(rowData[15]));
|
||||
item.setTotalArea(parseBigDecimal(rowData[16]));
|
||||
item.setBatchRefNo(getStringValue(rowData[17]));
|
||||
item.setSheetRefNo(getStringValue(rowData[18]));
|
||||
item.setCountryOfOrigin(getStringValue(rowData[19]));
|
||||
item.setBoxPalletNo(getStringValue(rowData[20]));
|
||||
item.setTotalAmount(parseBigDecimal(rowData[21]));
|
||||
item.setCurrency(getStringValue(rowData[22]));
|
||||
item.setRemark(getStringValue(rowData[23]));
|
||||
|
||||
itemList.add(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
// 解析完成后的处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 将List转换为数组
|
||||
*/
|
||||
private Object[] listToArray(List<String> list) {
|
||||
// 创建一个足够大的数组来容纳最多30列数据
|
||||
Object[] array = new Object[30];
|
||||
for (int i = 0; i < 30 && i < list.size(); i++) {
|
||||
array[i] = list.get(i);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Object的Map转换为String的Map
|
||||
*/
|
||||
private Map<Integer, String> convertToStringMap(Map<Integer, Object> dataMap) {
|
||||
if (dataMap == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
Map<Integer, String> stringMap = new HashMap<>();
|
||||
for (Map.Entry<Integer, Object> entry : dataMap.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
String stringValue = convertToString(value);
|
||||
stringMap.put(entry.getKey(), stringValue);
|
||||
}
|
||||
|
||||
return stringMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Object转换为String,处理BigDecimal等数字类型,去除小数点后的零
|
||||
*/
|
||||
private String convertToString(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 如果是BigDecimal类型,需要特殊处理,去除小数点后的零
|
||||
if (value instanceof BigDecimal) {
|
||||
BigDecimal bd = (BigDecimal) value;
|
||||
// 如果小数部分为0,转换为整数形式
|
||||
if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) {
|
||||
return String.valueOf(bd.longValue());
|
||||
}
|
||||
// 否则去除尾部零
|
||||
return bd.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
// 如果是Double或Float类型,也需要处理
|
||||
if (value instanceof Double) {
|
||||
Double d = (Double) value;
|
||||
if (d == d.longValue()) {
|
||||
return String.valueOf(d.longValue());
|
||||
}
|
||||
return d.toString();
|
||||
}
|
||||
|
||||
if (value instanceof Float) {
|
||||
Float f = (Float) value;
|
||||
if (f == f.longValue()) {
|
||||
return String.valueOf(f.longValue());
|
||||
}
|
||||
return f.toString();
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
String str = value.toString().trim();
|
||||
if (str.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Map转换为List
|
||||
*/
|
||||
private List<String> mapToList(Map<Integer, String> map) {
|
||||
if (map == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 找到最大索引以确定列表大小
|
||||
int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1;
|
||||
maxSize = Math.max(maxSize, 30); // 至少30个位置
|
||||
|
||||
List<String> list = new ArrayList<>(Collections.nCopies(maxSize, ""));
|
||||
|
||||
for (Map.Entry<Integer, String> entry : map.entrySet()) {
|
||||
if (entry.getKey() >= 0 && entry.getKey() < maxSize) {
|
||||
String value = entry.getValue();
|
||||
list.set(entry.getKey(), value != null ? value : "");
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private String getStringValue(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 如果是BigDecimal类型,需要特殊处理,去除小数点后的零
|
||||
if (value instanceof BigDecimal) {
|
||||
BigDecimal bd = (BigDecimal) value;
|
||||
// 如果小数部分为0,转换为整数形式
|
||||
if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) {
|
||||
return String.valueOf(bd.longValue());
|
||||
}
|
||||
// 否则去除尾部零
|
||||
return bd.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
// 如果是Double或Float类型,也需要处理
|
||||
if (value instanceof Double) {
|
||||
Double d = (Double) value;
|
||||
if (d == d.longValue()) {
|
||||
return String.valueOf(d.longValue());
|
||||
}
|
||||
return d.toString();
|
||||
}
|
||||
|
||||
if (value instanceof Float) {
|
||||
Float f = (Float) value;
|
||||
if (f == f.longValue()) {
|
||||
return String.valueOf(f.longValue());
|
||||
}
|
||||
return f.toString();
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
String str = value.toString().trim();
|
||||
if (str.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
// 尝试解析为数字,如果是整数形式的小数(如 "123.0"),转换为整数
|
||||
try {
|
||||
BigDecimal bd = new BigDecimal(str);
|
||||
// 如果小数部分为0,转换为整数形式
|
||||
if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) {
|
||||
return String.valueOf(bd.longValue());
|
||||
}
|
||||
// 否则去除尾部零
|
||||
return bd.stripTrailingZeros().toPlainString();
|
||||
} catch (NumberFormatException e) {
|
||||
// 不是数字字符串,直接返回
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析BigDecimal,失败返回0
|
||||
*/
|
||||
private BigDecimal parseBigDecimal(Object value) {
|
||||
try {
|
||||
String strValue = getStringValue(value);
|
||||
if (strValue.isEmpty()) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return new BigDecimal(strValue);
|
||||
} catch (Exception e) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 升列归一化:去千分位逗号、去空白;空值统一存 null(避免前端合计时对空串求和);
|
||||
* 纯数字归一为无多余零的字符串;非数字内容原样保留。
|
||||
*/
|
||||
private String normalizeNumeric(String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
String s = raw.trim().replace(",", "");
|
||||
if (s.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(s).stripTrailingZeros().toPlainString();
|
||||
} catch (NumberFormatException e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSequenceNumber(String str) {
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
str = str.trim();
|
||||
|
||||
return str.matches("^[1-9]\\d*$") || str.matches("^[1-9]\\d*\\.\\d+$") || str.matches("^0\\.\\d+$");
|
||||
}
|
||||
}
|
||||
+22
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrder;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
|
||||
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
@@ -25,4 +26,25 @@ public interface OutEntrustOrderMapper extends BaseMapper<OutEntrustOrder> {
|
||||
* 根据编号集合查询出仓委托单列表
|
||||
*/
|
||||
List<OutEntrustOrderPO> getInfoByIds(@Param("idList") List<Long> idList);
|
||||
|
||||
/**
|
||||
* 导入用:按料号查询货主下的物料基础信息(跨库WMS物料表)
|
||||
*/
|
||||
List<MaterialBaseInfoPO> getMaterialByMaterialCode(@Param("materialCode") String materialCode,
|
||||
@Param("shipperId") Long shipperId,
|
||||
@Param("topOrganizationId") Long topOrganizationId);
|
||||
|
||||
/**
|
||||
* 导入用:按SKU码(条形码)查询货主下的物料基础信息(跨库WMS物料表)
|
||||
*/
|
||||
List<MaterialBaseInfoPO> getMaterialByBarCode(@Param("barCode") String barCode,
|
||||
@Param("shipperId") Long shipperId,
|
||||
@Param("topOrganizationId") Long topOrganizationId);
|
||||
|
||||
/**
|
||||
* 导入用:按物料名称查询货主下的物料基础信息(跨库WMS物料表)
|
||||
*/
|
||||
List<MaterialBaseInfoPO> getMaterialByMaterialName(@Param("materialName") String materialName,
|
||||
@Param("shipperId") Long shipperId,
|
||||
@Param("topOrganizationId") Long topOrganizationId);
|
||||
}
|
||||
|
||||
+633
@@ -10,6 +10,7 @@ import com.mhd.common.core.domain.po.UserPo;
|
||||
import com.mhd.common.core.enums.DictCode;
|
||||
import com.mhd.common.core.exception.ServiceException;
|
||||
import com.mhd.common.core.utils.StringUtils;
|
||||
import com.mhd.common.core.utils.poi.ExcelUtil;
|
||||
import com.mhd.common.core.web.domain.AjaxResult;
|
||||
import com.mhd.common.security.utils.SecurityUtils;
|
||||
import com.mhd.oms.domain.erpCountry.entity.ErpCountry;
|
||||
@@ -18,6 +19,7 @@ import com.mhd.oms.domain.gwLog.repository.mapper.GwLogMapper;
|
||||
import com.mhd.oms.domain.reservationInMaterialDetail.entity.ReservationInMaterialDetail;
|
||||
import com.mhd.oms.domain.reservationInMaterialDetail.repository.mapper.ReservationInMaterialDetailMapper;
|
||||
import com.mhd.oms.domain.reservationInMaterialDetail.repository.po.ReservationInMaterialDetailPO;
|
||||
import com.mhd.oms.domain.reservationInMaterialDetail.repository.todo.ReservationInMaterialDetailDO;
|
||||
import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService;
|
||||
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.entity.ReservationStockInOrder;
|
||||
@@ -26,6 +28,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfo
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDTO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderImportRowDTO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwInDetail;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwStockInOrder;
|
||||
import com.mhd.oms.interfaces.dto.erpCountry.ErpCountryDTO;
|
||||
@@ -47,6 +50,7 @@ import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
@@ -1590,6 +1594,635 @@ public class ReservationStockInOrderApplicationService {
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 导入入仓委托单(模板与WMS入库管理导入一致:第1-12行表头,第13行列名,第14行起明细)
|
||||
* 以【委托编号NO】作为分组键,同一委托编号的多条明细归为一张入库单
|
||||
*/
|
||||
public Boolean importData(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("导入文件不能为空");
|
||||
}
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
throw new ServiceException("登录已失效,请重新登录");
|
||||
}
|
||||
Long topOrganizationId = loginUser.getUserPo() != null ? loginUser.getUserPo().getTopOrganizationId() : null;
|
||||
|
||||
try (InputStream is = file.getInputStream()) {
|
||||
// 明细表的列名在第13行(索引12),数据从第14行开始
|
||||
int detailTitleRowIndex = 12;
|
||||
|
||||
// 1. 读取表头信息(第1-12行),按标签扫描提取字段
|
||||
ReservationStockInOrderImportRowDTO headerInfo = new ReservationStockInOrderImportRowDTO();
|
||||
try (InputStream isForHeader = file.getInputStream()) {
|
||||
Workbook wb = WorkbookFactory.create(isForHeader);
|
||||
Sheet sheet = wb.getSheetAt(0);
|
||||
fillHeaderInfoByLabels(sheet, headerInfo);
|
||||
} catch (Exception e) {
|
||||
log.error("读取Excel表头信息失败", e);
|
||||
// 不抛出异常,继续尝试从明细行读取
|
||||
}
|
||||
log.info("表头信息读取完成 - 委托编号NO: {}, 客户名称: {}, To: {}, 联系人: {}, 承运商: {}, 入仓日期: {}, 完成时间: {}, 下单日期: {}",
|
||||
headerInfo.getConsignmentNo(), headerInfo.getShipperName(), headerInfo.getTo(),
|
||||
headerInfo.getContactPerson(), headerInfo.getCarrier(), headerInfo.getWarehouseDate(),
|
||||
headerInfo.getCompletionTime(), headerInfo.getOrderDate());
|
||||
|
||||
// 2. 读取明细数据(ExcelUtil从第13行列名开始)
|
||||
ExcelUtil<ReservationStockInOrderImportRowDTO> util = new ExcelUtil<>(ReservationStockInOrderImportRowDTO.class);
|
||||
List<ReservationStockInOrderImportRowDTO> rows = util.importExcel(is, detailTitleRowIndex);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
throw new ServiceException("导入数据为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行");
|
||||
}
|
||||
log.info("成功读取 {} 行明细数据", rows.size());
|
||||
|
||||
// 3. 将表头信息应用到每一行(明细行值为空时使用表头值)
|
||||
applyHeaderToRows(headerInfo, rows);
|
||||
|
||||
// 4. 过滤掉商品名称为空的空行
|
||||
List<ReservationStockInOrderImportRowDTO> validRows = rows.stream()
|
||||
.filter(r -> r != null && StringUtils.isNotBlank(r.getMaterialName()))
|
||||
.collect(Collectors.toList());
|
||||
if (validRows.isEmpty()) {
|
||||
throw new ServiceException("导入数据为空(可能全为空行)。共读取 " + rows.size()
|
||||
+ " 行数据,但商品名称都为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行");
|
||||
}
|
||||
|
||||
// 5. 仓库必填校验(与出库单导入保持一致,仓库由导入弹窗选择传入)
|
||||
if (warehouseId == null) {
|
||||
throw new ServiceException("导入失败:仓库ID不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(warehouseCode)) {
|
||||
throw new ServiceException("导入失败:仓库编码不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(warehouseName)) {
|
||||
throw new ServiceException("导入失败:仓库名称不能为空");
|
||||
}
|
||||
|
||||
// 6. 以委托编号NO分组;若为空则归到同一组
|
||||
Map<String, List<ReservationStockInOrderImportRowDTO>> groupMap = validRows.stream()
|
||||
.collect(Collectors.groupingBy(r -> StringUtils.isBlank(r.getConsignmentNo()) ? "__EMPTY_CONSIGNMENT__" : r.getConsignmentNo()));
|
||||
|
||||
List<ReservationStockInOrderDO> orderList = new ArrayList<>();
|
||||
for (Map.Entry<String, List<ReservationStockInOrderImportRowDTO>> entry : groupMap.entrySet()) {
|
||||
ReservationStockInOrderDO order = buildStockInOrderFromImport(
|
||||
entry.getValue(), headerInfo, loginUser, topOrganizationId,
|
||||
warehouseId, warehouseCode, warehouseName);
|
||||
if (order != null) {
|
||||
orderList.add(order);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有订单都被跳过(orderList为空),抛出异常提示用户
|
||||
if (orderList.isEmpty()) {
|
||||
throw new ServiceException("导入失败:所有入库单的明细行都因数据不完整(数量为空或物料基础信息缺失)被跳过,请检查Excel数据");
|
||||
}
|
||||
|
||||
return stockInOrderDomainService.batchInsert(orderList);
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("导入入库单失败", e);
|
||||
throw new ServiceException("导入入库单失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个委托编号分组的入库单组装(解析+校验,不落库)
|
||||
* 明细行数量为空/容器类型行跳过;料号必填且必须在当前货主下存在,规格型号/SKU码与物料档案不一致直接报错
|
||||
*
|
||||
* @return 组装完成的入库单;该组明细全部被跳过时返回null
|
||||
*/
|
||||
private ReservationStockInOrderDO buildStockInOrderFromImport(List<ReservationStockInOrderImportRowDTO> groupRows,
|
||||
ReservationStockInOrderImportRowDTO headerInfo,
|
||||
LoginUser loginUser, Long topOrganizationId,
|
||||
Long warehouseId, String warehouseCode, String warehouseName) {
|
||||
ReservationStockInOrderImportRowDTO h = groupRows.get(0);
|
||||
|
||||
// 客户名称优先使用表头,表头为空时使用明细行
|
||||
String customerName = StringUtils.isNotBlank(headerInfo.getShipperName()) ? headerInfo.getShipperName() : h.getShipperName();
|
||||
if (StringUtils.isBlank(customerName)) {
|
||||
throw new ServiceException("导入失败:客户名称不能为空(委托编号NO=" + h.getConsignmentNo()
|
||||
+ ")。请确保Excel表头(第1-12行)中包含'客户名称'字段");
|
||||
}
|
||||
|
||||
ReservationStockInOrderDO order = new ReservationStockInOrderDO();
|
||||
// 组织/审计字段
|
||||
if (loginUser.getUserPo() != null) {
|
||||
order.setOrganizationId(loginUser.getUserPo().getOrganizationId());
|
||||
order.setOrganizationName(loginUser.getUserPo().getOrganizationName());
|
||||
order.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
|
||||
}
|
||||
order.setCreateBy(loginUser.getUserid());
|
||||
order.setCreateByName(resolveUserRealName(loginUser));
|
||||
order.setCreateTime(new Date());
|
||||
|
||||
// 仓库(导入弹窗选择传入)
|
||||
order.setWarehouseId(warehouseId);
|
||||
order.setWarehouseCode(warehouseCode);
|
||||
order.setWarehouseName(warehouseName);
|
||||
|
||||
// 货主信息:名称/编号解析货主ID
|
||||
String shipperCode = StringUtils.isNotBlank(headerInfo.getShipperCode()) ? headerInfo.getShipperCode() : h.getShipperCode();
|
||||
Long shipperId = getShipperIdByNameOrCode(customerName, shipperCode);
|
||||
order.setShipperId(shipperId);
|
||||
if (StringUtils.isNotBlank(customerName)) {
|
||||
order.setShipperName(customerName);
|
||||
}
|
||||
if (StringUtils.isNotBlank(shipperCode)) {
|
||||
order.setShipperCode(shipperCode);
|
||||
}
|
||||
order.setConsignmentNo(h.getConsignmentNo());
|
||||
order.setRemark(h.getRemark());
|
||||
// 导入入库单时,如果入库单类型为空,默认设置为普通入库
|
||||
order.setOrderTypeCode("pu_tong_ru_ku");
|
||||
order.setOrderTypeName("普通入库单");
|
||||
// 订单来源:数据导入(与入库业务单多页签导入保持一致)
|
||||
order.setReservationOrderSource("数据导入");
|
||||
|
||||
// 报关表头字段(优先使用表头字段,表头为空则使用明细行字段)
|
||||
order.setTo(firstNotBlank(headerInfo.getTo(), h.getTo()));
|
||||
order.setContactPerson(firstNotBlank(headerInfo.getContactPerson(), h.getContactPerson()));
|
||||
order.setVehicleInfo(firstNotBlank(headerInfo.getVehicleInfo(), h.getVehicleInfo()));
|
||||
order.setTel(firstNotBlank(headerInfo.getTel(), h.getTel()));
|
||||
order.setDriverSignature(firstNotBlank(headerInfo.getDriverSignature(), h.getDriverSignature()));
|
||||
order.setFax(firstNotBlank(headerInfo.getFax(), h.getFax()));
|
||||
order.setDeclarationArea(firstNotBlank(headerInfo.getDeclarationArea(), h.getDeclarationArea()));
|
||||
order.setManufacturer(firstNotBlank(headerInfo.getManufacturer(), h.getManufacturer()));
|
||||
order.setCustomsBrokerInfo(firstNotBlank(headerInfo.getCustomsBrokerInfo(), h.getCustomsBrokerInfo()));
|
||||
order.setDeliveryAddress(firstNotBlank(headerInfo.getDeliveryAddress(), h.getDeliveryAddress()));
|
||||
order.setEntryExitCustoms(firstNotBlank(headerInfo.getEntryExitCustoms(), h.getEntryExitCustoms()));
|
||||
order.setOriginDestinationCountry(firstNotBlank(headerInfo.getOriginDestinationCountry(), h.getOriginDestinationCountry()));
|
||||
order.setCarrier(firstNotBlank(headerInfo.getCarrier(), h.getCarrier()));
|
||||
order.setContainerNo(firstNotBlank(headerInfo.getContainerNo(), h.getContainerNo()));
|
||||
order.setTransportMethod(firstNotBlank(headerInfo.getTransportMethod(), h.getTransportMethod()));
|
||||
order.setSupervisionMethod(firstNotBlank(headerInfo.getSupervisionMethod(), h.getSupervisionMethod()));
|
||||
|
||||
// 报关字段默认值(优先使用表头字段)
|
||||
Integer customsInspection = headerInfo.getCustomsInspection() != null ? headerInfo.getCustomsInspection() : h.getCustomsInspection();
|
||||
order.setCustomsInspection(customsInspection == null ? 0 : customsInspection); // 默认:否
|
||||
Integer needCustomsDeclaration = headerInfo.getNeedCustomsDeclaration() != null ? headerInfo.getNeedCustomsDeclaration() : h.getNeedCustomsDeclaration();
|
||||
order.setNeedCustomsDeclaration(needCustomsDeclaration == null ? 1 : needCustomsDeclaration); // 默认:是
|
||||
Integer needTransport = headerInfo.getNeedTransport() != null ? headerInfo.getNeedTransport() : h.getNeedTransport();
|
||||
order.setNeedTransport(needTransport == null ? 0 : needTransport); // 默认:否
|
||||
|
||||
// 日期字段(优先使用表头字段)
|
||||
String warehouseDateStr = firstNotBlank(headerInfo.getWarehouseDate(), h.getWarehouseDate());
|
||||
if (StringUtils.isNotBlank(warehouseDateStr)) {
|
||||
Date warehouseDate = parseDateString(warehouseDateStr);
|
||||
if (warehouseDate != null) {
|
||||
order.setWarehouseDate(warehouseDate);
|
||||
}
|
||||
}
|
||||
String completionTimeStr = firstNotBlank(headerInfo.getCompletionTime(), h.getCompletionTime());
|
||||
if (StringUtils.isNotBlank(completionTimeStr)) {
|
||||
Date completionTime = parseDateString(completionTimeStr);
|
||||
if (completionTime != null) {
|
||||
order.setCompletionTime(completionTime);
|
||||
}
|
||||
}
|
||||
String orderDateStr = firstNotBlank(headerInfo.getOrderDate(), h.getOrderDate());
|
||||
if (StringUtils.isNotBlank(orderDateStr)) {
|
||||
Date orderDate = parseDateString(orderDateStr);
|
||||
if (orderDate != null) {
|
||||
order.setOrderDate(orderDate);
|
||||
}
|
||||
}
|
||||
|
||||
// 明细
|
||||
List<ReservationInMaterialDetailDO> details = new ArrayList<>();
|
||||
for (ReservationStockInOrderImportRowDTO r : groupRows) {
|
||||
// 先检查数量(仓库数量优先,入库数量兜底),数量为空直接跳过该明细行
|
||||
BigDecimal quantity = r.getWarehouseQuantity();
|
||||
if (quantity == null) {
|
||||
quantity = r.getInboundQuantity();
|
||||
}
|
||||
if (quantity == null) {
|
||||
log.warn("跳过明细行:仓库数量或入库数量不能为空 - 委托编号NO: [{}], 商品名称: [{}], 料号: [{}]",
|
||||
h.getConsignmentNo(), r.getMaterialName(), r.getMaterialCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否为容器类型等非物料信息,如果是则跳过
|
||||
String materialName = r.getMaterialName();
|
||||
if (StringUtils.isNotBlank(materialName)) {
|
||||
String lowerName = materialName.trim();
|
||||
if (lowerName.contains("尺柜") || lowerName.contains("尺集装箱")
|
||||
|| lowerName.contains("Container") || lowerName.contains("container")
|
||||
|| (lowerName.contains("柜") && (lowerName.contains("20") || lowerName.contains("40") || lowerName.contains("45")))) {
|
||||
log.info("跳过明细行:识别为容器类型信息,非实际物料 - 委托编号NO: [{}], 商品名称: [{}]",
|
||||
h.getConsignmentNo(), materialName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 导入文件中必须填写料号,根据料号获取商品信息,必须是货主下有的才可以匹配
|
||||
if (StringUtils.isBlank(r.getMaterialCode())) {
|
||||
throw new ServiceException("导入失败:请在导入文件中填写正确的料号");
|
||||
}
|
||||
|
||||
// 根据料号跨库查询物料基础信息(getByCode按组织过滤),查到后需在Java侧核对货主归属
|
||||
MaterialBaseInfoPO materialBaseInfoPO = safeGetMaterialByCode(r.getMaterialCode(), loginUser);
|
||||
if (materialBaseInfoPO != null && shipperId != null
|
||||
&& (materialBaseInfoPO.getShipperId() == null || !materialBaseInfoPO.getShipperId().equals(shipperId))) {
|
||||
// 查到但不属于该货主,等同于不存在,不允许导入
|
||||
materialBaseInfoPO = null;
|
||||
}
|
||||
if (materialBaseInfoPO == null) {
|
||||
// 如果提供了料号但查询不到(或不属于该货主),直接报错,不允许导入
|
||||
throw new ServiceException(String.format("导入失败:该货主下不存在该料号(货主名称=%s,料号=%s)。请确保该料号已在物料管理中创建,且属于该货主",
|
||||
order.getShipperName(), r.getMaterialCode()));
|
||||
}
|
||||
|
||||
// 校验料号与规格型号/SKU码一致性(与WMS入库管理导入一致):任何一项不符直接报对应错误
|
||||
String excelSpec = StringUtils.trimToEmpty(r.getSpecificationModel());
|
||||
String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel());
|
||||
if (!excelSpec.equals(dbSpec)) {
|
||||
throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)规格型号与物料档案不一致(Excel:%s / 档案:%s)",
|
||||
r.getMaterialCode(), r.getMaterialName(),
|
||||
excelSpec.isEmpty() ? "空" : excelSpec, dbSpec.isEmpty() ? "空" : dbSpec));
|
||||
}
|
||||
String excelSku = StringUtils.trimToEmpty(r.getSkuCode());
|
||||
String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode());
|
||||
if (!excelSku.equals(dbSku)) {
|
||||
throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)SKU码与物料档案不一致(Excel:%s / 档案:%s)",
|
||||
r.getMaterialCode(), r.getMaterialName(),
|
||||
excelSku.isEmpty() ? "空" : excelSku, dbSku.isEmpty() ? "空" : dbSku));
|
||||
}
|
||||
|
||||
ReservationInMaterialDetailDO d = new ReservationInMaterialDetailDO();
|
||||
d.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId());
|
||||
// 从物料档案获取名称(与WMS入库导入一致)
|
||||
d.setMaterialName(materialBaseInfoPO.getMaterialName());
|
||||
d.setMaterialCode(r.getMaterialCode());
|
||||
d.setQuantity(quantity);
|
||||
d.setSpecificationModel(r.getSpecificationModel());
|
||||
d.setSkuCode(r.getSkuCode());
|
||||
d.setInvoiceNo(r.getInvoiceNo());
|
||||
d.setDeclaredQuantity(r.getDeclaredQuantity());
|
||||
d.setDeclarationUnit(r.getDeclarationUnit());
|
||||
d.setLiter(r.getLiter());
|
||||
d.setBoxCount(r.getBoxCount());
|
||||
d.setPieceCount(r.getPieceCount());
|
||||
d.setDimensions(r.getDimensions());
|
||||
// 入库数量:优先使用入库数量,如果为空则使用仓库数量
|
||||
BigDecimal inboundQuantity = r.getInboundQuantity();
|
||||
if (inboundQuantity == null) {
|
||||
inboundQuantity = r.getWarehouseQuantity();
|
||||
}
|
||||
d.setInboundQuantity(inboundQuantity);
|
||||
// 单位:优先使用Excel中的"单位"字段,如果为空则使用"申报单位",如果还为空则从物料主数据获取
|
||||
String unit = StringUtils.isNotBlank(r.getUnit()) ? r.getUnit() : null;
|
||||
if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(r.getDeclarationUnit())) {
|
||||
unit = r.getDeclarationUnit();
|
||||
}
|
||||
if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) {
|
||||
unit = materialBaseInfoPO.getUnitName();
|
||||
}
|
||||
d.setUnit(unit);
|
||||
// 总净重/总毛重/总体积/总面积:优先使用主字段,如果为空则使用备用字段
|
||||
d.setTotalNetWeight(r.getTotalNetWeight() != null ? r.getTotalNetWeight() : r.getTotalNetWeight2());
|
||||
d.setTotalGrossWeight(r.getTotalGrossWeight() != null ? r.getTotalGrossWeight() : r.getTotalGrossWeight2());
|
||||
d.setTotalVolume(r.getTotalVolume() != null ? r.getTotalVolume() : r.getTotalVolume2());
|
||||
d.setTotalArea(r.getTotalArea() != null ? r.getTotalArea() : r.getTotalArea2());
|
||||
d.setBatchRefNo(r.getBatchRefNo());
|
||||
d.setSheetRefNo(r.getSheetRefNo());
|
||||
// 原产国:优先使用Excel中的数据,如果为空则从物料主数据获取
|
||||
String originCountry = StringUtils.isNotBlank(r.getOriginCountry()) ? r.getOriginCountry() : null;
|
||||
if (StringUtils.isBlank(originCountry) && StringUtils.isNotBlank(materialBaseInfoPO.getOriginCountry())) {
|
||||
originCountry = materialBaseInfoPO.getOriginCountry();
|
||||
}
|
||||
d.setOriginCountry(originCountry);
|
||||
d.setBoxPalletNo(r.getBoxPalletNo());
|
||||
d.setTotalPrice(r.getTotalPrice());
|
||||
// 币制:优先使用Excel中的数据,如果为空则从物料主数据获取
|
||||
String currency = StringUtils.isNotBlank(r.getCurrency()) ? r.getCurrency() : null;
|
||||
if (StringUtils.isBlank(currency) && StringUtils.isNotBlank(materialBaseInfoPO.getCurrency())) {
|
||||
currency = materialBaseInfoPO.getCurrency();
|
||||
}
|
||||
d.setCurrency(currency);
|
||||
d.setRemark(r.getDetailRemark());
|
||||
|
||||
details.add(d);
|
||||
}
|
||||
|
||||
// 如果该订单的所有明细行都被跳过(details为空),跳过该订单
|
||||
if (details.isEmpty()) {
|
||||
log.warn("跳过入库单:委托编号NO [{}] 的所有明细行都因数据不完整被跳过,该入库单将不会被导入", h.getConsignmentNo());
|
||||
return null;
|
||||
}
|
||||
|
||||
order.setMaterialDetailList(details);
|
||||
|
||||
// 导入入库单时,计划数量取所有明细行的入库数量之和
|
||||
BigDecimal totalInboundQuantity = details.stream()
|
||||
.map(detail -> detail.getInboundQuantity() != null ? detail.getInboundQuantity() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
order.setQuantity(totalInboundQuantity);
|
||||
|
||||
// 导入时补全货主/供应商/数据字典信息(与手动新增链路一致)
|
||||
try {
|
||||
shipperParam(order);
|
||||
} catch (Exception e) {
|
||||
log.error("导入入库单时设置货主信息失败,委托编号NO:{},货主名称:{},错误:{}",
|
||||
h.getConsignmentNo(), order.getShipperName(), e.getMessage());
|
||||
throw new ServiceException("导入失败:设置货主信息失败(委托编号NO=" + h.getConsignmentNo()
|
||||
+ ",货主名称=" + order.getShipperName() + ")。" + e.getMessage());
|
||||
}
|
||||
supplierParam(order);
|
||||
setDataDict(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入用:按料号跨库查询物料基础信息(查询结果需在Java侧核对货主归属)
|
||||
*/
|
||||
private MaterialBaseInfoPO safeGetMaterialByCode(String materialCode, LoginUser loginUser) {
|
||||
try {
|
||||
Long orgId = loginUser.getUserPo() != null ? loginUser.getUserPo().getOrganizationId() : null;
|
||||
return reservationStockInOrderMapper.getByCode(materialCode, orgId);
|
||||
} catch (Exception e) {
|
||||
log.warn("按料号查询物料基础信息失败,料号: [{}], 错误: {}", materialCode, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将表头信息应用到每一行明细(明细行值为空时使用表头值;客户名称/货主编号表头有值时强制应用)
|
||||
*/
|
||||
private void applyHeaderToRows(ReservationStockInOrderImportRowDTO headerInfo, List<ReservationStockInOrderImportRowDTO> rows) {
|
||||
for (ReservationStockInOrderImportRowDTO row : rows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
// 应用表头中的委托编号NO
|
||||
if (StringUtils.isBlank(row.getConsignmentNo()) && StringUtils.isNotBlank(headerInfo.getConsignmentNo())) {
|
||||
row.setConsignmentNo(headerInfo.getConsignmentNo());
|
||||
}
|
||||
// 表头有客户名称/货主编号时,强制应用到所有明细行
|
||||
if (StringUtils.isNotBlank(headerInfo.getShipperName())) {
|
||||
row.setShipperName(headerInfo.getShipperName());
|
||||
}
|
||||
if (StringUtils.isNotBlank(headerInfo.getShipperCode())) {
|
||||
row.setShipperCode(headerInfo.getShipperCode());
|
||||
}
|
||||
// 应用其他表头字段(如果明细行为空,则使用表头值)
|
||||
row.setTo(firstNotBlank(row.getTo(), headerInfo.getTo()));
|
||||
row.setContactPerson(firstNotBlank(row.getContactPerson(), headerInfo.getContactPerson()));
|
||||
row.setVehicleInfo(firstNotBlank(row.getVehicleInfo(), headerInfo.getVehicleInfo()));
|
||||
row.setCarrier(firstNotBlank(row.getCarrier(), headerInfo.getCarrier()));
|
||||
row.setTel(firstNotBlank(row.getTel(), headerInfo.getTel()));
|
||||
row.setDriverSignature(firstNotBlank(row.getDriverSignature(), headerInfo.getDriverSignature()));
|
||||
row.setFax(firstNotBlank(row.getFax(), headerInfo.getFax()));
|
||||
row.setDeclarationArea(firstNotBlank(row.getDeclarationArea(), headerInfo.getDeclarationArea()));
|
||||
row.setManufacturer(firstNotBlank(row.getManufacturer(), headerInfo.getManufacturer()));
|
||||
row.setCustomsBrokerInfo(firstNotBlank(row.getCustomsBrokerInfo(), headerInfo.getCustomsBrokerInfo()));
|
||||
row.setDeliveryAddress(firstNotBlank(row.getDeliveryAddress(), headerInfo.getDeliveryAddress()));
|
||||
row.setEntryExitCustoms(firstNotBlank(row.getEntryExitCustoms(), headerInfo.getEntryExitCustoms()));
|
||||
row.setOriginDestinationCountry(firstNotBlank(row.getOriginDestinationCountry(), headerInfo.getOriginDestinationCountry()));
|
||||
row.setContainerNo(firstNotBlank(row.getContainerNo(), headerInfo.getContainerNo()));
|
||||
row.setTransportMethod(firstNotBlank(row.getTransportMethod(), headerInfo.getTransportMethod()));
|
||||
row.setSupervisionMethod(firstNotBlank(row.getSupervisionMethod(), headerInfo.getSupervisionMethod()));
|
||||
if (row.getCustomsInspection() == null) {
|
||||
row.setCustomsInspection(headerInfo.getCustomsInspection());
|
||||
}
|
||||
if (row.getNeedCustomsDeclaration() == null) {
|
||||
row.setNeedCustomsDeclaration(headerInfo.getNeedCustomsDeclaration());
|
||||
}
|
||||
if (row.getNeedTransport() == null) {
|
||||
row.setNeedTransport(headerInfo.getNeedTransport());
|
||||
}
|
||||
row.setWarehouseDate(firstNotBlank(row.getWarehouseDate(), headerInfo.getWarehouseDate()));
|
||||
row.setCompletionTime(firstNotBlank(row.getCompletionTime(), headerInfo.getCompletionTime()));
|
||||
row.setOrderDate(firstNotBlank(row.getOrderDate(), headerInfo.getOrderDate()));
|
||||
row.setRemark(firstNotBlank(row.getRemark(), headerInfo.getRemark()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描表头区域(第1-12行)按标签提取字段值:
|
||||
* 值优先取标签同行右侧最近的合法单元格,其次取下一行同列,最后取标签文本冒号后的内容;
|
||||
* 看起来仍是标签文本(含冒号或命中标签关键词)的值会被跳过
|
||||
*/
|
||||
private void fillHeaderInfoByLabels(Sheet sheet, ReservationStockInOrderImportRowDTO headerInfo) {
|
||||
for (int rowIdx = 0; rowIdx < 12; rowIdx++) {
|
||||
Row row = sheet.getRow(rowIdx);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
for (int colIdx = 0; colIdx < row.getPhysicalNumberOfCells(); colIdx++) {
|
||||
Cell labelCell = row.getCell(colIdx);
|
||||
if (labelCell == null) {
|
||||
continue;
|
||||
}
|
||||
String labelValue = getCellValueAsString(labelCell);
|
||||
if (StringUtils.isBlank(labelValue)) {
|
||||
continue;
|
||||
}
|
||||
// 标签文本冒号后的内容(兜底值)
|
||||
String extractedFromSameCell = null;
|
||||
if (labelValue.contains(":")) {
|
||||
String[] parts = labelValue.split(":", 2);
|
||||
if (parts.length > 1) {
|
||||
extractedFromSameCell = parts[1].trim();
|
||||
}
|
||||
}
|
||||
String value = findValueRightwards(row, colIdx);
|
||||
if (StringUtils.isBlank(value)) {
|
||||
value = findValueBelow(sheet, rowIdx, colIdx);
|
||||
}
|
||||
if (StringUtils.isBlank(value)) {
|
||||
value = extractedFromSameCell;
|
||||
}
|
||||
if (StringUtils.isBlank(value)) {
|
||||
continue;
|
||||
}
|
||||
dispatchHeaderField(labelValue, value, headerInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在同行右侧(最多10列)查找第一个合法值
|
||||
*/
|
||||
private String findValueRightwards(Row row, int colIdx) {
|
||||
for (int searchColIdx = colIdx + 1; searchColIdx < colIdx + 10 && searchColIdx < row.getPhysicalNumberOfCells(); searchColIdx++) {
|
||||
Cell valueCell = row.getCell(searchColIdx);
|
||||
if (valueCell == null) {
|
||||
continue;
|
||||
}
|
||||
String cellValue = getCellValueAsString(valueCell);
|
||||
if (StringUtils.isNotBlank(cellValue)) {
|
||||
return cellValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在下一行同列查找合法值(模板变体:值写在标签下一行)
|
||||
*/
|
||||
private String findValueBelow(Sheet sheet, int rowIdx, int colIdx) {
|
||||
Row nextRow = sheet.getRow(rowIdx + 1);
|
||||
if (nextRow == null) {
|
||||
return null;
|
||||
}
|
||||
Cell nextRowCell = nextRow.getCell(colIdx);
|
||||
if (nextRowCell == null) {
|
||||
return null;
|
||||
}
|
||||
String nextRowValue = getCellValueAsString(nextRowCell);
|
||||
return StringUtils.isNotBlank(nextRowValue) ? nextRowValue : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按标签关键词分发到表头字段
|
||||
*/
|
||||
private void dispatchHeaderField(String labelValue, String value, ReservationStockInOrderImportRowDTO headerInfo) {
|
||||
// 备注:值内容任意,不做标签过滤
|
||||
if (labelValue.contains("备注")) {
|
||||
if (StringUtils.isBlank(headerInfo.getRemark())) {
|
||||
headerInfo.setRemark(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 委托编号NO(支持"委托编号NO"、"委托编号"等格式,排除标签本身)
|
||||
if (labelValue.contains("委托编号")) {
|
||||
if (StringUtils.isBlank(headerInfo.getConsignmentNo())
|
||||
&& !value.contains("委托编号") && !value.contains("NO")
|
||||
&& !"委托编号NO".equals(value) && !"委托编号".equals(value)) {
|
||||
headerInfo.setConsignmentNo(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 客户名称
|
||||
if (labelValue.contains("客户名称")) {
|
||||
if (StringUtils.isBlank(headerInfo.getShipperName())
|
||||
&& !value.contains("客户") && !"客户名称".equals(value)) {
|
||||
headerInfo.setShipperName(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// To(需要精确匹配,避免误匹配)
|
||||
if (isToLabel(labelValue)) {
|
||||
if (StringUtils.isBlank(headerInfo.getTo()) && !isToLabel(value) && !looksLikeLabel(value)) {
|
||||
headerInfo.setTo(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 值是标签文本(含冒号或命中标签关键词)时跳过
|
||||
if (value.contains(":") || value.contains(":") || looksLikeLabel(value)) {
|
||||
return;
|
||||
}
|
||||
if (labelValue.contains("联系人") && StringUtils.isBlank(headerInfo.getContactPerson())) {
|
||||
headerInfo.setContactPerson(value);
|
||||
} else if ((labelValue.contains("车型") || labelValue.contains("车牌")) && StringUtils.isBlank(headerInfo.getVehicleInfo())) {
|
||||
headerInfo.setVehicleInfo(value);
|
||||
} else if ((labelValue.contains("承运方") || labelValue.contains("承运商")) && StringUtils.isBlank(headerInfo.getCarrier())) {
|
||||
headerInfo.setCarrier(value);
|
||||
} else if (labelValue.contains("Tel") && StringUtils.isBlank(headerInfo.getTel())) {
|
||||
headerInfo.setTel(value);
|
||||
} else if (labelValue.contains("司机签名") && StringUtils.isBlank(headerInfo.getDriverSignature())) {
|
||||
headerInfo.setDriverSignature(value);
|
||||
} else if (labelValue.contains("入仓日期") && StringUtils.isBlank(headerInfo.getWarehouseDate())) {
|
||||
headerInfo.setWarehouseDate(value);
|
||||
} else if (labelValue.contains("Fax") && StringUtils.isBlank(headerInfo.getFax())) {
|
||||
headerInfo.setFax(value);
|
||||
} else if (labelValue.contains("申报地关区") && StringUtils.isBlank(headerInfo.getDeclarationArea())) {
|
||||
headerInfo.setDeclarationArea(value);
|
||||
} else if (labelValue.contains("生产商") && StringUtils.isBlank(headerInfo.getManufacturer())) {
|
||||
headerInfo.setManufacturer(value);
|
||||
} else if (labelValue.contains("报关员") && StringUtils.isBlank(headerInfo.getCustomsBrokerInfo())) {
|
||||
headerInfo.setCustomsBrokerInfo(value);
|
||||
} else if (labelValue.contains("送货地址") && StringUtils.isBlank(headerInfo.getDeliveryAddress())) {
|
||||
headerInfo.setDeliveryAddress(value);
|
||||
} else if (labelValue.contains("进出境关别") && StringUtils.isBlank(headerInfo.getEntryExitCustoms())) {
|
||||
headerInfo.setEntryExitCustoms(value);
|
||||
} else if ((labelValue.contains("起运") || labelValue.contains("运抵国")) && StringUtils.isBlank(headerInfo.getOriginDestinationCountry())) {
|
||||
headerInfo.setOriginDestinationCountry(value);
|
||||
} else if (labelValue.contains("柜号") && StringUtils.isBlank(headerInfo.getContainerNo())) {
|
||||
headerInfo.setContainerNo(value);
|
||||
} else if (labelValue.contains("运输方式") && StringUtils.isBlank(headerInfo.getTransportMethod())) {
|
||||
headerInfo.setTransportMethod(value);
|
||||
} else if (labelValue.contains("监管方式") && StringUtils.isBlank(headerInfo.getSupervisionMethod())) {
|
||||
headerInfo.setSupervisionMethod(value);
|
||||
} else if (labelValue.contains("海关是否查验货物") && headerInfo.getCustomsInspection() == null) {
|
||||
headerInfo.setCustomsInspection(parseYesNo(value));
|
||||
} else if (labelValue.contains("是否需要报关") && headerInfo.getNeedCustomsDeclaration() == null) {
|
||||
headerInfo.setNeedCustomsDeclaration(parseYesNo(value));
|
||||
} else if (labelValue.contains("是否需要运输") && headerInfo.getNeedTransport() == null) {
|
||||
headerInfo.setNeedTransport(parseYesNo(value));
|
||||
} else if (labelValue.contains("完成时间") && StringUtils.isBlank(headerInfo.getCompletionTime())) {
|
||||
headerInfo.setCompletionTime(value);
|
||||
} else if (labelValue.contains("下单日期") && StringUtils.isBlank(headerInfo.getOrderDate())) {
|
||||
headerInfo.setOrderDate(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为To标签(精确匹配,避免把Value/Total等英文词误认为To)
|
||||
*/
|
||||
private boolean isToLabel(String text) {
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
String t = text.trim();
|
||||
return "To".equals(t) || "To:".equals(t) || "To:".equals(t) || t.startsWith("To:") || t.startsWith("To:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 值是否看起来仍是标签文本(命中常见标签关键词)
|
||||
*/
|
||||
private boolean looksLikeLabel(String value) {
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
String v = value.trim();
|
||||
return v.contains("委托编号") || v.contains("司机签名") || v.contains("完成时间") || v.contains("下单日期")
|
||||
|| v.contains("入仓日期") || v.contains("联系人") || v.contains("Tel") || v.contains("Fax")
|
||||
|| v.contains("客户") || v.contains("地址") || v.contains("日期") || v.contains("方式")
|
||||
|| v.contains("关区") || v.contains("关别") || v.contains("报关员") || v.contains("生产商")
|
||||
|| v.contains("承运方") || v.contains("承运商") || v.contains("起运") || v.contains("运抵国")
|
||||
|| v.contains("地区") || v.contains("车型") || v.contains("车牌") || v.contains("柜号")
|
||||
|| v.contains("海关是否查验货物") || v.contains("是否需要报关") || v.contains("是否需要运输");
|
||||
}
|
||||
|
||||
/**
|
||||
* 是/否(或1/0)转Integer;无法识别返回null
|
||||
*/
|
||||
private Integer parseYesNo(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String v = value.trim();
|
||||
if (v.contains("是") || "1".equals(v)) {
|
||||
return 1;
|
||||
}
|
||||
if (v.contains("否") || "0".equals(v)) {
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取第一个非空字符串
|
||||
*/
|
||||
private String firstNotBlank(String a, String b) {
|
||||
return StringUtils.isNotBlank(a) ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录人姓名(优先UserPo.userName,其次realname,最后username兜底)
|
||||
*/
|
||||
private String resolveUserRealName(LoginUser loginUser) {
|
||||
if (loginUser.getUserPo() != null && StringUtils.isNotBlank(loginUser.getUserPo().getUserName())) {
|
||||
return loginUser.getUserPo().getUserName();
|
||||
}
|
||||
if (StringUtils.isNotBlank(loginUser.getRealname())) {
|
||||
return loginUser.getRealname();
|
||||
}
|
||||
return loginUser.getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串为Date对象
|
||||
* 支持多种日期格式:yyyy-MM-dd, yyyy/MM/dd, yyyy年MM月dd日, Date.toString()格式等
|
||||
|
||||
+6
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
|
||||
|
||||
|
||||
@@ -22,6 +23,11 @@ public interface IReservationStockOutOrderService extends IService<ReservationSt
|
||||
*/
|
||||
public List<ReservationStockOutOrderPO> queryList(ReservationStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 新增出库单
|
||||
*/
|
||||
|
||||
+6
@@ -6,6 +6,7 @@ import com.mhd.common.core.domain.po.UserPo;
|
||||
import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
//import com.mhd.wms.domain.stockOutOrder.entity.ReservationStockOutOrder;
|
||||
@@ -27,6 +28,11 @@ public interface ReservationStockOutOrderMapper extends BaseMapper<ReservationSt
|
||||
*/
|
||||
public List<ReservationStockOutOrderPO> queryList(ReservationStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
/**
|
||||
* 出库单提货单打印-表头
|
||||
*/
|
||||
|
||||
+10
@@ -11,6 +11,7 @@ import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrd
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.mapper.ReservationStockOutOrderMapper;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
|
||||
import com.mhd.system.api.model.LoginUser;
|
||||
//import com.mhd.wms.domain.deliveTask.repository.facade.IDeliveTaskTypeService;
|
||||
@@ -48,6 +49,15 @@ public class ReservationStockOutOrderImpl extends ServiceImpl<ReservationStockOu
|
||||
return reservationStockOutOrderMapper.queryList(stockOutOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
@Override
|
||||
public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO)
|
||||
{
|
||||
return reservationStockOutOrderMapper.queryListTotal(stockOutOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增出库单
|
||||
*/
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.mhd.oms.domain.reservationStockOutOrder.repository.po;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 出库业务单列表合计PO
|
||||
*
|
||||
* <p>当前筛选条件下不分页的全量合计,随分页列表一起返回
|
||||
* (totalPlanQuantity/totalOutboundQuantity),计划数量与出库数量分开计算</p>
|
||||
*
|
||||
* @author rcx
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "出库业务单列表合计对象", description = "出库业务单列表合计响应对象")
|
||||
public class ReservationStockOutOrderTotalPO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("计划数量总数(一级明细计划数量之和)")
|
||||
private BigDecimal totalPlanQuantity;
|
||||
|
||||
@ApiModelProperty("出库数量总数(一级明细出库数量之和)")
|
||||
private BigDecimal totalOutboundQuantity;
|
||||
}
|
||||
+10
@@ -45,6 +45,7 @@ import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrd
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.mapper.ReservationStockOutOrderMapper;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDTO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.vo.GwOutDetail;
|
||||
@@ -246,6 +247,15 @@ public class ReservationStockOutOrderApplicationService {
|
||||
return stockOutOrderPOS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量。
|
||||
* 须在 queryList/queryList1 之后用同一个DO调用:数据权限条件由列表查询原地写入DO,
|
||||
* 合计查询才能继承生效后的筛选;且 PageHelper startPage 只拦截下一条SQL,本查询不会被分页。
|
||||
*/
|
||||
public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO) {
|
||||
return stockOutOrderDomainService.queryListTotal(stockOutOrderDO);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 新增出库单
|
||||
|
||||
+235
-18
@@ -57,6 +57,7 @@ import com.mhd.oms.domain.reservationInventoryAdjustmentRecord.repository.mapper
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.mapper.ReservationStockOutOrderMapper;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
|
||||
import com.mhd.oms.domain.reserveStockOutOrder.repository.mapper.ReserveStockOutOrderMapper;
|
||||
import com.mhd.system.api.SystemServiceFeign;
|
||||
@@ -142,6 +143,13 @@ public class ReservationStockOutOrderDomainService {
|
||||
public List<ReservationStockOutOrderPO> queryList(ReservationStockOutOrderDO stockOutOrderDO) {
|
||||
return stockOutOrderService.queryList(stockOutOrderDO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
|
||||
*/
|
||||
public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO) {
|
||||
return stockOutOrderService.queryListTotal(stockOutOrderDO);
|
||||
}
|
||||
/**
|
||||
* 从单号列表中提取最大序号
|
||||
* 单号格式: prefix + 数字序号(如 RK260807001 中 prefix=RK260807, 序号=001)
|
||||
@@ -236,22 +244,32 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (stockOutOrderDO.getReservationOrderSource() == null || stockOutOrderDO.getReservationOrderSource().isEmpty()) {
|
||||
stockOutOrderDO.setReservationOrderSource("手动创建");
|
||||
}
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
List<ReservationDeliveryDetailsLink> reservationDeliveryDetailsLinkList = stockOutOrderDO.getReservationDeliveryDetailsLinkList();
|
||||
if (reservationDeliveryDetailsLinkList != null && !reservationDeliveryDetailsLinkList.isEmpty()){
|
||||
reservationDeliveryDetailsLinkList.forEach(reservationDeliveryDetailsLink -> {
|
||||
reservationDeliveryDetailsLink.setOrderNumber(stockOutOrderDO.getOutOrderNumber());
|
||||
reservationDeliveryDetailsLink.setOrderId(stockOutOrderDO.getOutOrderId());
|
||||
});
|
||||
if (!CollectionUtils.isEmpty(reservationDeliveryDetailsLinkList)) {
|
||||
reservationDeliveryDetailsLinkService.saveBatch(reservationDeliveryDetailsLinkList);
|
||||
// 下单冻结库存(防超卖):下单冻结模式仓库的明细先冻结WMS库存(可用-、冻结+,库存数量不变),
|
||||
// 冻结失败阻断下单;本地落库失败补偿解冻(对齐执行并下发的补偿模式)
|
||||
boolean frozen = tryFreezeOnCreate(stockOutOrderDO);
|
||||
try {
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
List<ReservationDeliveryDetailsLink> reservationDeliveryDetailsLinkList = stockOutOrderDO.getReservationDeliveryDetailsLinkList();
|
||||
if (reservationDeliveryDetailsLinkList != null && !reservationDeliveryDetailsLinkList.isEmpty()){
|
||||
reservationDeliveryDetailsLinkList.forEach(reservationDeliveryDetailsLink -> {
|
||||
reservationDeliveryDetailsLink.setOrderNumber(stockOutOrderDO.getOutOrderNumber());
|
||||
reservationDeliveryDetailsLink.setOrderId(stockOutOrderDO.getOutOrderId());
|
||||
});
|
||||
if (!CollectionUtils.isEmpty(reservationDeliveryDetailsLinkList)) {
|
||||
reservationDeliveryDetailsLinkService.saveBatch(reservationDeliveryDetailsLinkList);
|
||||
}
|
||||
}
|
||||
List<ReservationOutMaterialDetailDO> materialDetailList = stockOutOrderDO.getMaterialDetailList();
|
||||
if (materialDetailList != null && !materialDetailList.isEmpty()) {
|
||||
return outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
if (frozen) {
|
||||
compensateUnfreeze(stockOutOrderDO.getOutOrderNumber());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
List<ReservationOutMaterialDetailDO> materialDetailList = stockOutOrderDO.getMaterialDetailList();
|
||||
if (materialDetailList != null && !materialDetailList.isEmpty()) {
|
||||
return outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,12 +302,186 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (stockOutOrderDO.getCheckTaskDistribution() == null) {
|
||||
stockOutOrderDO.setCheckTaskDistribution(2);
|
||||
}
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
// 下单冻结库存(防超卖):与 insert 同口径,冻结失败阻断、落库失败补偿解冻
|
||||
boolean frozen = tryFreezeOnCreate(stockOutOrderDO);
|
||||
try {
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
} catch (Exception e) {
|
||||
if (frozen) {
|
||||
compensateUnfreeze(stockOutOrderDO.getOutOrderNumber());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================ 下单冻结库存(防超卖) ================================
|
||||
|
||||
/**
|
||||
* 下单冻结模式仓库判断:stock_lock_mode=1-下单冻结(2-分配库存冻结为默认现有模式)。
|
||||
* 复用仓库信息兜底组件(Feign优先+本地跨库兜底);兜底时 stockLockMode=null → 按模式2处理(保守降级不冻结)
|
||||
*/
|
||||
private boolean isOrderFreezeWarehouse(Long warehouseId) {
|
||||
if (warehouseId == null) {
|
||||
return false;
|
||||
}
|
||||
WarehouseFeignPO warehouseFeignPO = getWarehouseInfoWithFallback(warehouseId);
|
||||
return warehouseFeignPO != null && warehouseFeignPO.getStockLockMode() != null && warehouseFeignPO.getStockLockMode() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库单保存时下单冻结:筛选下单冻结模式仓库的明细,先冻结WMS库存再落库(失败抛异常阻断保存)。
|
||||
* ★下单冻结模式仓库明细强制必选库存批次:未关联 materialInventoryId 无法确定冻结哪条库存行,直接阻断。
|
||||
*
|
||||
* @return true-已冻结(落库失败需补偿解冻);false-无冻结模式明细未冻结
|
||||
*/
|
||||
private boolean tryFreezeOnCreate(ReservationStockOutOrderDO stockOutOrderDO) {
|
||||
List<ReservationOutMaterialDetailDO> details = stockOutOrderDO.getMaterialDetailList();
|
||||
if (CollectionUtils.isEmpty(details)) {
|
||||
return false;
|
||||
}
|
||||
// 样品出库交接不扣减库存,同样不参与下单冻结
|
||||
if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) {
|
||||
return false;
|
||||
}
|
||||
List<OutMaterialDetail> freezeDetails = filterFreezeDetails(details, stockOutOrderDO.getWarehouseId(), stockOutOrderDO.getOutOrderNumber());
|
||||
if (freezeDetails.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(stockOutOrderDO.getOutOrderNumber());
|
||||
param.setDetails(freezeDetails);
|
||||
fillOperator(param);
|
||||
AjaxResult result = wmsServiceFeign.omsFreezeInventory(param);
|
||||
if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
|
||||
throw new ServiceException("下单冻结库存失败,无法保存出库单:"
|
||||
+ (result == null ? "WMS服务不可用" : result.get("msg")));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库单编辑-原子重锁:已下发的冻结单禁止编辑(判据=活动执行单存在,issueStatus 不可靠——下发不改该字段);
|
||||
* 未下发则WMS同一事务内释放原冻结+按新明细重锁,成功后才改本地
|
||||
*/
|
||||
private void handleFreezeOnUpdate(ReservationStockOutOrderDO stockOutOrderDO, ReservationStockOutOrderPO existingPO) {
|
||||
List<ReservationOutMaterialDetailDO> details = stockOutOrderDO.getMaterialDetailList();
|
||||
if (CollectionUtils.isEmpty(details)) {
|
||||
return;
|
||||
}
|
||||
// 样品出库交接不扣减库存,同样不参与下单冻结
|
||||
if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) {
|
||||
return;
|
||||
}
|
||||
List<OutMaterialDetail> freezeDetails = filterFreezeDetails(details, stockOutOrderDO.getWarehouseId(), existingPO.getOutOrderNumber());
|
||||
if (freezeDetails.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String orderNumber = existingPO.getOutOrderNumber();
|
||||
// 已下发WMS(存在活动执行单)禁止编辑:执行单明细与冻结记录必须保持一致,须先取消下发
|
||||
List<ExecutionStockOutOrder> activeExecutions = executionStockOutOrderService.list(new QueryWrapper<ExecutionStockOutOrder>().lambda()
|
||||
.eq(ExecutionStockOutOrder::getBusinessOutOrderNumber, orderNumber)
|
||||
.eq(ExecutionStockOutOrder::getDelFlag, 1));
|
||||
if (!CollectionUtils.isEmpty(activeExecutions)) {
|
||||
throw new ServiceException("出库单[" + orderNumber + "]已下发WMS,不可编辑,请先取消下发");
|
||||
}
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(orderNumber);
|
||||
param.setDetails(freezeDetails);
|
||||
fillOperator(param);
|
||||
AjaxResult result = wmsServiceFeign.omsRelockInventory(param);
|
||||
if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
|
||||
throw new ServiceException("编辑重锁库存失败,无法修改出库单:"
|
||||
+ (result == null ? "WMS服务不可用" : result.get("msg")));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除/关闭单据前释放冻结(WMS按业务单号幂等处理:无冻结直接成功;
|
||||
* 冻结已被执行单承接时WMS拒绝并提示先取消下发;已交接时拒绝并提示走反审流程)
|
||||
*/
|
||||
private void unfreezeIfFrozen(ReservationStockOutOrderPO stockOutOrderPO) {
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(stockOutOrderPO.getOutOrderNumber());
|
||||
fillOperator(param);
|
||||
AjaxResult result = wmsServiceFeign.omsUnfreezeInventory(param);
|
||||
if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
|
||||
throw new ServiceException("释放冻结库存失败:"
|
||||
+ (result == null ? "WMS服务不可用" : result.get("msg")));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地落库失败的补偿解冻(对齐执行并下发的补偿模式):补偿失败仅告警,
|
||||
* 凭业务单号在WMS冻结记录(out_order_freeze_record,relate_no 前缀=业务单号)中人工处理
|
||||
*/
|
||||
private void compensateUnfreeze(String outOrderNumber) {
|
||||
try {
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(outOrderNumber);
|
||||
fillOperator(param);
|
||||
AjaxResult result = wmsServiceFeign.omsUnfreezeInventory(param);
|
||||
if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
|
||||
log.error("下单冻结补偿解冻失败,请人工核查WMS冻结记录:出库业务单[{}],返回:{}",
|
||||
outOrderNumber, result == null ? "WMS服务不可用" : result.get("msg"));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("下单冻结补偿解冻异常,请人工核查WMS冻结记录:出库业务单[{}]", outOrderNumber, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选下单冻结模式仓库的明细并转换为冻结参数。
|
||||
* ★下单冻结模式仓库明细强制必选库存批次(R2):materialInventoryId 为空直接阻断保存;
|
||||
* 数量<=0的明细交由WMS侧兜底校验
|
||||
*/
|
||||
private List<OutMaterialDetail> filterFreezeDetails(List<ReservationOutMaterialDetailDO> details, Long defaultWarehouseId, String businessOrderNumber) {
|
||||
List<OutMaterialDetail> freezeDetails = new ArrayList<>();
|
||||
for (ReservationOutMaterialDetailDO detail : details) {
|
||||
Long warehouseId = detail.getWarehouseId() != null ? detail.getWarehouseId() : defaultWarehouseId;
|
||||
if (!isOrderFreezeWarehouse(warehouseId)) {
|
||||
continue;
|
||||
}
|
||||
// 下单冻结必须知道冻结哪条库存行:模式1仓库明细未关联库存直接阻断
|
||||
if (detail.getMaterialInventoryId() == null || detail.getMaterialInventoryId() <= 0) {
|
||||
throw new ServiceException("出库单[" + (businessOrderNumber != null ? businessOrderNumber : "当前单据") + "]物料["
|
||||
+ (detail.getMaterialCode() != null ? detail.getMaterialCode() : detail.getMaterialName())
|
||||
+ "]未选择库存批次,下单冻结模式仓库必须先选择库存批次");
|
||||
}
|
||||
OutMaterialDetail out = new OutMaterialDetail();
|
||||
out.setMaterialInventoryId(detail.getMaterialInventoryId());
|
||||
out.setMaterialBaseInfoId(detail.getMaterialBaseInfoId());
|
||||
out.setMaterialCode(detail.getMaterialCode());
|
||||
out.setMaterialName(detail.getMaterialName());
|
||||
out.setWarehouseId(warehouseId);
|
||||
out.setQuantity(detail.getQuantity());
|
||||
out.setBatchNumber(detail.getBatchNumber());
|
||||
out.setBatchRefNo(detail.getBatchRefNo());
|
||||
out.setSheetRefNo(detail.getSheetRefNo());
|
||||
out.setBoxPalletNo(detail.getBoxPalletNo());
|
||||
out.setUniqueId(detail.getUniqueId());
|
||||
freezeDetails.add(out);
|
||||
}
|
||||
return freezeDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充操作人(Feign调用WMS时兜底传递,WMS侧无登录上下文时使用)
|
||||
*/
|
||||
private void fillOperator(OmsFreezeInventoryParam param) {
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
param.setOperatorId(loginUser.getUserid());
|
||||
param.setOperatorName(loginUser.getUsername());
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
// 无登录上下文场景(Feign内部调用),操作人留空由WMS兜底
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改出库单
|
||||
*/
|
||||
@@ -300,6 +492,9 @@ public class ReservationStockOutOrderDomainService {
|
||||
}
|
||||
//设置物料信息
|
||||
setMaterialDetailInfo(stockOutOrderDO);
|
||||
// 下单冻结模式编辑重锁(原子):先WMS侧释放原冻结+按新明细重锁(同一事务),成功后才改本地;
|
||||
// 已下发WMS的冻结单不可编辑,须先取消下发(判据=活动执行单,issueStatus 不可靠)
|
||||
handleFreezeOnUpdate(stockOutOrderDO, stockInOrderPO);
|
||||
//统计种类
|
||||
stockOutOrderDO.setMaterialQuantity(stockOutOrderDO.getMaterialDetailList().size());
|
||||
//统计总数量
|
||||
@@ -379,6 +574,8 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (stockInOrderPO.getStatus() != 1){
|
||||
throw new ServiceException("只有已创建状态下能允许关闭出库单");
|
||||
}
|
||||
// 下单冻结模式:关闭单据不再出库,先释放冻结库存(幂等;冻结已被执行单承接时WMS会拒绝并提示先取消下发)
|
||||
unfreezeIfFrozen(stockInOrderPO);
|
||||
stockOutOrderDO.setStatus(11);
|
||||
return stockOutOrderService.update(stockOutOrderDO);
|
||||
}
|
||||
@@ -789,6 +986,18 @@ public class ReservationStockOutOrderDomainService {
|
||||
List<ExecutionStockOutOrder> list = executionStockOutOrderService.list(new QueryWrapper<ExecutionStockOutOrder>().lambda()
|
||||
.eq(ExecutionStockOutOrder::getBusinessOutOrderNumber, inOrderNumber)
|
||||
.eq(ExecutionStockOutOrder::getDelFlag, 1));
|
||||
// 取消下发必须同步WMS(修复存量不同步问题:此前只软删本地执行单,WMS残留单据与分配占用):
|
||||
// WMS侧还原库存/承接冻结并软删单据;下单冻结模式下仅解除承接(冻结归还业务单),
|
||||
// 分配库存冻结模式下还原分配占用;失败抛异常整体回滚本地软删(WMS清理幂等可重试)
|
||||
for (ExecutionStockOutOrder executionStockOutOrder : list) {
|
||||
StockOutOrder cancelParam = new StockOutOrder();
|
||||
cancelParam.setOutOrderNumber(executionStockOutOrder.getOutOrderNumber());
|
||||
AjaxResult cancelResult = wmsServiceFeign.omsCancelExecutionOrder(cancelParam);
|
||||
if (cancelResult == null || !"200".equals(String.valueOf(cancelResult.get("code")))) {
|
||||
throw new ServiceException("取消下发失败:WMS执行单[" + executionStockOutOrder.getOutOrderNumber()
|
||||
+ "]清理失败:" + (cancelResult == null ? "WMS服务不可用" : cancelResult.get("msg")));
|
||||
}
|
||||
}
|
||||
list.forEach(executionStockOutOrder -> {
|
||||
String outOrderNumber = executionStockOutOrder.getOutOrderNumber();
|
||||
List<ExecutionOutMaterialDetail> materialDetails = executionOutMaterialDetailService.list(new QueryWrapper<ExecutionOutMaterialDetail>().lambda()
|
||||
@@ -992,7 +1201,8 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (!groupBaseValid) {
|
||||
return;
|
||||
}
|
||||
//WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存)
|
||||
//WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存;
|
||||
// 传业务单号:下单冻结模式下行级可用量=库存可用+本单冻结记录量)
|
||||
List<OutMaterialDetail> checkList = new ArrayList<>();
|
||||
for (ReservationOutMaterialDetail detail : details) {
|
||||
OutMaterialDetail checkDetail = new OutMaterialDetail();
|
||||
@@ -1002,7 +1212,7 @@ public class ReservationStockOutOrderDomainService {
|
||||
checkDetail.setMaterialName(detail.getMaterialName());
|
||||
checkList.add(checkDetail);
|
||||
}
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList, inOrderNumber);
|
||||
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
|
||||
String groupWarehouseName = reservationStockInOrderMapper.getWarehouseName(warehouseId);
|
||||
issueValidateErrors.add("仓库[" + (groupWarehouseName != null ? groupWarehouseName : warehouseId) + "]库存校验失败:"
|
||||
@@ -1174,6 +1384,13 @@ public class ReservationStockOutOrderDomainService {
|
||||
* 批量删除出库单
|
||||
*/
|
||||
public Boolean delete(Long[] outOrderIds) {
|
||||
// 下单冻结模式:删除单据前先释放冻结库存(幂等;冻结已被执行单承接时WMS会拒绝并提示先取消下发)
|
||||
for (Long outOrderId : outOrderIds) {
|
||||
ReservationStockOutOrderPO stockOutOrderPO = stockOutOrderService.getInfo(outOrderId);
|
||||
if (stockOutOrderPO != null) {
|
||||
unfreezeIfFrozen(stockOutOrderPO);
|
||||
}
|
||||
}
|
||||
return stockOutOrderService.delete(outOrderIds);
|
||||
}
|
||||
|
||||
|
||||
+28
-6
@@ -1,15 +1,17 @@
|
||||
package com.mhd.oms.interfaces.facade.ReservationStockOutOrder;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.mhd.common.core.constant.HttpStatus;
|
||||
import com.mhd.common.core.web.controller.BaseController;
|
||||
import com.mhd.common.core.web.domain.AjaxResult;
|
||||
import com.mhd.common.core.web.page.TableDataInfo;
|
||||
import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDTO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDTO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.service.ReservationStockOutOrderApplicationService;
|
||||
@@ -46,17 +48,19 @@ public class ReservationStockOutOrderApi extends BaseController {
|
||||
private IReservationStockOutOrderService stockOutOrderService;
|
||||
|
||||
/**
|
||||
* 分页查询出库单列表
|
||||
* 分页查询出库单列表(附:当前查询条件下全量不分页的计划数量总数/出库数量总数)
|
||||
*/
|
||||
@ApiOperation("查询出库单列表")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(ReservationStockOutOrderDTO stockOutOrderDTO)
|
||||
public HashMap<String, Object> list(ReservationStockOutOrderDTO stockOutOrderDTO)
|
||||
{
|
||||
//转换实体
|
||||
ReservationStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO);
|
||||
startPage();
|
||||
List<ReservationStockOutOrderPO> list = stockOutOrderApplicationService.queryList(stockOutOrderDO);
|
||||
return getDataTable(list);
|
||||
// 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截
|
||||
ReservationStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO);
|
||||
return getDataTableWithTotal(list, total);
|
||||
}
|
||||
|
||||
|
||||
@@ -250,13 +254,31 @@ public class ReservationStockOutOrderApi extends BaseController {
|
||||
|
||||
@ApiOperation("查询出库单列表")
|
||||
@GetMapping("/list1")
|
||||
public TableDataInfo list1(ReservationStockOutOrderDTO stockOutOrderDTO)
|
||||
public HashMap<String, Object> list1(ReservationStockOutOrderDTO stockOutOrderDTO)
|
||||
{
|
||||
//转换实体
|
||||
ReservationStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO);
|
||||
startPage();
|
||||
List<ReservationStockOutOrderPO> list = stockOutOrderApplicationService.queryList1(stockOutOrderDO);
|
||||
return getDataTable(list);
|
||||
// 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截
|
||||
ReservationStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO);
|
||||
return getDataTableWithTotal(list, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页结果 + 合计:data/total/code/msg 之外追加计划数量总数/出库数量总数(key与TableDataInfo序列化结果一致,仅多两个字段)
|
||||
*/
|
||||
private HashMap<String, Object> getDataTableWithTotal(List<?> list, ReservationStockOutOrderTotalPO total) {
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
result.put("data", list);
|
||||
result.put("total", new PageInfo<>(list).getTotal());
|
||||
result.put("code", HttpStatus.SUCCESS);
|
||||
result.put("msg", "查询成功");
|
||||
if (total != null) {
|
||||
result.put("totalPlanQuantity", total.getTotalPlanQuantity());
|
||||
result.put("totalOutboundQuantity", total.getTotalOutboundQuantity());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ApiOperation("生成出库预约单号")
|
||||
|
||||
+28
-6
@@ -1,13 +1,15 @@
|
||||
package com.mhd.oms.interfaces.facade.executionStockOutOrder;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.mhd.common.core.constant.HttpStatus;
|
||||
import com.mhd.common.core.web.controller.BaseController;
|
||||
import com.mhd.common.core.web.domain.AjaxResult;
|
||||
import com.mhd.common.core.web.page.TableDataInfo;
|
||||
import com.mhd.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDTO;
|
||||
import com.mhd.oms.domain.executionStockOutOrder.service.ExecutionStockOutOrderApplicationService;
|
||||
@@ -45,17 +47,19 @@ public class ExecutionStockOutOrderApi extends BaseController {
|
||||
private IExecutionStockOutOrderService stockOutOrderService;
|
||||
|
||||
/**
|
||||
* 分页查询出库单列表
|
||||
* 分页查询出库单列表(附:当前查询条件下全量不分页的计划数量总数/出库数量总数)
|
||||
*/
|
||||
@ApiOperation("查询出库单列表")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(ExecutionStockOutOrderDTO stockOutOrderDTO)
|
||||
public HashMap<String, Object> list(ExecutionStockOutOrderDTO stockOutOrderDTO)
|
||||
{
|
||||
//转换实体
|
||||
ExecutionStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO);
|
||||
startPage();
|
||||
List<ExecutionStockOutOrderPO> list = stockOutOrderApplicationService.queryList(stockOutOrderDO);
|
||||
return getDataTable(list);
|
||||
// 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截
|
||||
ExecutionStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO);
|
||||
return getDataTableWithTotal(list, total);
|
||||
}
|
||||
|
||||
|
||||
@@ -234,13 +238,31 @@ public class ExecutionStockOutOrderApi extends BaseController {
|
||||
|
||||
@ApiOperation("查询出库单列表")
|
||||
@GetMapping("/list1")
|
||||
public TableDataInfo list1(ExecutionStockOutOrderDTO stockOutOrderDTO)
|
||||
public HashMap<String, Object> list1(ExecutionStockOutOrderDTO stockOutOrderDTO)
|
||||
{
|
||||
//转换实体
|
||||
ExecutionStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO);
|
||||
startPage();
|
||||
List<ExecutionStockOutOrderPO> list = stockOutOrderApplicationService.queryList1(stockOutOrderDO);
|
||||
return getDataTable(list);
|
||||
// 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截
|
||||
ExecutionStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO);
|
||||
return getDataTableWithTotal(list, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页结果 + 合计:data/total/code/msg 之外追加计划数量总数/出库数量总数(key与TableDataInfo序列化结果一致,仅多两个字段)
|
||||
*/
|
||||
private HashMap<String, Object> getDataTableWithTotal(List<?> list, ExecutionStockOutOrderTotalPO total) {
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
result.put("data", list);
|
||||
result.put("total", new PageInfo<>(list).getTotal());
|
||||
result.put("code", HttpStatus.SUCCESS);
|
||||
result.put("msg", "查询成功");
|
||||
if (total != null) {
|
||||
result.put("totalPlanQuantity", total.getTotalPlanQuantity());
|
||||
result.put("totalOutboundQuantity", total.getTotalOutboundQuantity());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -12,8 +12,10 @@ import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@@ -80,6 +82,42 @@ public class OutEntrustOrderApi extends BaseController {
|
||||
return AjaxResult.success(outEntrustOrderApplicationService.getInfo(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传并解析出仓委托单Excel(模板与WMS出库管理导入一致:前11行表头+第12行起明细)
|
||||
* 解析校验通过后落库为出仓委托单(生成CKWT委托单号,状态待审核/未下发)
|
||||
*/
|
||||
@ApiOperation("导入出仓委托单")
|
||||
@PostMapping("/import")
|
||||
public AjaxResult importOutEntrustOrder(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("warehouseId") Long warehouseId,
|
||||
@RequestParam("warehouseCode") String warehouseCode,
|
||||
@RequestParam("warehouseName") String warehouseName) {
|
||||
|
||||
// 验证文件
|
||||
if (file.isEmpty()) {
|
||||
return error("文件为空");
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
String fileName = file.getOriginalFilename();
|
||||
if (fileName == null || !fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||
return error("文件类型错误");
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析Excel并返回结果
|
||||
StringBuilder result = outEntrustOrderApplicationService.importOutEntrustOrder(file, warehouseId, warehouseCode, warehouseName);
|
||||
if (result.length() > 0) {
|
||||
return AjaxResult.error(result.toString());
|
||||
} else {
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return error("解析失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量审核:待审核/已驳回 → 已审核(通过)或已驳回
|
||||
* 请求体:{"idList":[1,2],"orderStatus":3,"auditRemark":"备注"},orderStatus:2-审核通过 3-驳回(auditRemark选填)
|
||||
|
||||
+21
-11
@@ -99,18 +99,28 @@ public class ReservationStockInOrderApi extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入入库单
|
||||
* 导入入仓委托单(模板与WMS入库管理导入一致:第1-12行表头,第13行列名,第14行起明细)
|
||||
* 按【委托编号NO】分组,同一委托编号的多条明细归为一张入库单
|
||||
*/
|
||||
// @ApiOperation("导入入库单")
|
||||
// @PostMapping(value = "/importData")
|
||||
// public AjaxResult importData(
|
||||
// @RequestParam("file") MultipartFile file,
|
||||
// @RequestParam("warehouseId") Long warehouseId,
|
||||
// @RequestParam("warehouseCode") String warehouseCode,
|
||||
// @RequestParam("warehouseName") String warehouseName)
|
||||
// {
|
||||
// return AjaxResult.success(stockInOrderApplicationService.importData(file, warehouseId, warehouseCode, warehouseName));
|
||||
// }
|
||||
@ApiOperation("导入入仓委托单")
|
||||
@PostMapping(value = "/importData")
|
||||
public AjaxResult importData(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("warehouseId") Long warehouseId,
|
||||
@RequestParam("warehouseCode") String warehouseCode,
|
||||
@RequestParam("warehouseName") String warehouseName)
|
||||
{
|
||||
// 验证文件
|
||||
if (file.isEmpty()) {
|
||||
return error("文件为空");
|
||||
}
|
||||
// 验证文件类型
|
||||
String fileName = file.getOriginalFilename();
|
||||
if (fileName == null || !fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||
return error("文件类型错误");
|
||||
}
|
||||
return AjaxResult.success(stockInOrderApplicationService.importData(file, warehouseId, warehouseCode, warehouseName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核入库单
|
||||
|
||||
+29
-1
@@ -117,6 +117,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="businessChargeType" column="BUSINESS_CHARGE_TYPE" />
|
||||
<result property="businessPriceWay" column="BUSINESS_PRICE_WAY" />
|
||||
<result property="discount" column="DISCOUNT" />
|
||||
<result property="totalOutboundQuantity" column="TOTAL_OUTBOUND_QUANTITY" />
|
||||
</resultMap>
|
||||
|
||||
|
||||
@@ -408,7 +409,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="queryList" parameterType="com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO" resultMap="StockOutOrderResult">
|
||||
select
|
||||
<include refid="selectStockOutOrderPo"/>
|
||||
<include refid="selectStockOutOrderPo"/>,
|
||||
<!-- 出库数量:汇总明细表实际出库数量,只统计一级明细(level=1),与出库业务单列表(178b7697)同口径;
|
||||
原逻辑为Java循环汇总(N+1)后注释,PO字段totalOutboundQuantity无映射导致列表该字段恒为null -->
|
||||
(select coalesce(sum(emd.outbound_quantity), 0)
|
||||
from execution_out_material_detail emd
|
||||
where emd.out_order_number = a.out_order_number
|
||||
and emd.del_flag = 1
|
||||
and (emd.level is null or emd.level = 1)) as total_outbound_quantity
|
||||
from execution_stock_out_order a
|
||||
<where>
|
||||
<include refid="selectStockOutOrderPo1"/>
|
||||
@@ -416,6 +424,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by a.create_time desc nulls last
|
||||
</select>
|
||||
|
||||
<!-- 列表合计:按查询条件(与queryList同条件片段)全量不分页汇总一级明细的计划数量与出库数量,计划数量与出库数量分开计算 -->
|
||||
<select id="queryListTotal" parameterType="com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO" resultType="com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO">
|
||||
select
|
||||
coalesce(sum(md.plan_quantity), 0) as totalPlanQuantity,
|
||||
coalesce(sum(md.outbound_quantity), 0) as totalOutboundQuantity
|
||||
from execution_stock_out_order a
|
||||
left join (
|
||||
select out_order_number,
|
||||
sum(quantity) as plan_quantity,
|
||||
sum(outbound_quantity) as outbound_quantity
|
||||
from execution_out_material_detail
|
||||
where del_flag = 1
|
||||
and (level is null or level = 1)
|
||||
group by out_order_number
|
||||
) md on md.out_order_number = a.out_order_number
|
||||
<where>
|
||||
<include refid="selectStockOutOrderPo1"/>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
|
||||
<!-- 出库单提货单打印-表头查询 -->
|
||||
<select id="queryOutStockNoticePrint" resultMap="StockOutOrderResult">
|
||||
|
||||
@@ -178,4 +178,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- 导入用:跨库查询WMS物料基础信息(与reservationStockInOrder模块的直查模式一致),按货主+一级组织过滤 -->
|
||||
<sql id="selectImportMaterialBaseInfo">
|
||||
select * from "NGWL_TEST_WMS".MATERIAL_BASE_INFO
|
||||
where DEL_FLAG = 1
|
||||
and SHIPPER_ID = #{shipperId}
|
||||
<if test="topOrganizationId != null">
|
||||
and TOP_ORGANIZATION_ID = #{topOrganizationId}
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="getMaterialByMaterialCode" resultType="com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO">
|
||||
<include refid="selectImportMaterialBaseInfo"/>
|
||||
and MATERIAL_CODE = #{materialCode}
|
||||
</select>
|
||||
|
||||
<select id="getMaterialByBarCode" resultType="com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO">
|
||||
<include refid="selectImportMaterialBaseInfo"/>
|
||||
and BAR_CODE = #{barCode}
|
||||
</select>
|
||||
|
||||
<select id="getMaterialByMaterialName" resultType="com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO">
|
||||
<include refid="selectImportMaterialBaseInfo"/>
|
||||
and MATERIAL_NAME = #{materialName}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
+20
@@ -437,6 +437,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by a.create_time desc nulls last
|
||||
</select>
|
||||
|
||||
<!-- 列表合计:按查询条件(与queryList同条件片段)全量不分页汇总一级明细的计划数量与出库数量,计划数量与出库数量分开计算 -->
|
||||
<select id="queryListTotal" parameterType="com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO" resultType="com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO">
|
||||
select
|
||||
coalesce(sum(md.plan_quantity), 0) as totalPlanQuantity,
|
||||
coalesce(sum(md.outbound_quantity), 0) as totalOutboundQuantity
|
||||
from reservation_stock_out_order a
|
||||
left join (
|
||||
select out_order_number,
|
||||
sum(quantity) as plan_quantity,
|
||||
sum(outbound_quantity) as outbound_quantity
|
||||
from reservation_out_material_detail
|
||||
where del_flag = 1
|
||||
and (level is null or level = 1)
|
||||
group by out_order_number
|
||||
) md on md.out_order_number = a.out_order_number
|
||||
<where>
|
||||
<include refid="selectStockOutOrderPo1"/>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
|
||||
<!-- 出库单提货单打印-表头查询 -->
|
||||
<select id="queryOutStockNoticePrint" resultMap="StockOutOrderResult">
|
||||
|
||||
+36
-6
@@ -66,6 +66,7 @@ import com.mhd.wms.domain.stockOutOrder.repository.mapper.StockOutOrderMapper;
|
||||
import com.mhd.wms.domain.stockOutOrder.repository.po.StockOutOrderPO;
|
||||
import com.mhd.wms.domain.stockOutOrder.repository.todo.StockOutOrderDO;
|
||||
import com.mhd.wms.domain.stockOutOrder.service.StockOutOrderDomainService;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService;
|
||||
import com.mhd.wms.interfaces.dto.stockOutOrder.StockOutOrderDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
@@ -99,6 +100,8 @@ public class StockOutOrderApplicationService {
|
||||
@Autowired
|
||||
private StockOutOrderDomainService stockOutOrderDomainService;
|
||||
@Autowired
|
||||
private OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService;
|
||||
@Autowired
|
||||
private OutMaterialDetailDomainService outMaterialDetailDomainService;
|
||||
@Autowired
|
||||
private MaterialInventoryDomainService materialInventoryDomainService;
|
||||
@@ -1062,6 +1065,13 @@ public class StockOutOrderApplicationService {
|
||||
}
|
||||
// 当前物料的可用库存
|
||||
BigDecimal availableStock = materialInventory.getAllocationQuantity();
|
||||
// 下单冻结模式:加回本单该行的冻结记录量(下单时已冻结导致可用=0,冻结行会被跳过
|
||||
// 导致分配不到量;承接时数量不动,此处仅校验/选行口径)
|
||||
BigDecimal orderFrozen = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity(
|
||||
stockOutOrderDb.getBusinessOrderNo(), materialInventory.getMaterialInventoryId());
|
||||
if (orderFrozen.compareTo(BigDecimal.ZERO) > 0) {
|
||||
availableStock = availableStock.add(orderFrozen);
|
||||
}
|
||||
// 如果当前物料没有库存,跳过
|
||||
if (availableStock == null || availableStock.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
@@ -1149,14 +1159,23 @@ public class StockOutOrderApplicationService {
|
||||
|
||||
// 当前物料的可用库存
|
||||
BigDecimal availableStock = materialInventory.getAllocationQuantity();
|
||||
// 下单冻结模式:加回本单该行的冻结记录量(下单时已冻结导致可用=0,纯可用口径会把冻结明细
|
||||
// 跳过/报错,永远到不了 batchAssignUpdate 的承接分支;承接时数量不动,此处仅校验口径)
|
||||
BigDecimal orderFrozen = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity(
|
||||
stockOutOrderDb.getBusinessOrderNo(), materialInventoryId);
|
||||
if (orderFrozen.compareTo(BigDecimal.ZERO) > 0) {
|
||||
availableStock = availableStock.add(orderFrozen);
|
||||
}
|
||||
// 如果当前物料没有库存,跳过
|
||||
if (availableStock == null || availableStock.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (availableStock.compareTo(remainingQuantity) < 0) {
|
||||
throw new ServiceException(
|
||||
String.format("出库单[%s]物料[%s]可用库存不足,计划数量:%s,当前可用库存:%s",
|
||||
outOrderNumber, materialDesc, remainingQuantity, availableStock)
|
||||
String.format("出库单[%s]物料[%s]可用库存不足,计划数量:%s,当前可用库存:%s(含下单冻结%s)",
|
||||
outOrderNumber, materialDesc, remainingQuantity,
|
||||
availableStock.stripTrailingZeros().toPlainString(),
|
||||
orderFrozen.stripTrailingZeros().toPlainString())
|
||||
);
|
||||
}
|
||||
// detail.setMaterialInventoryId(materialInventory.getMaterialInventoryId());
|
||||
@@ -1187,9 +1206,12 @@ public class StockOutOrderApplicationService {
|
||||
/**
|
||||
* OMS下发前库存校验:按库存ID聚合各明细计划数量,逐一比对WMS可用库存,
|
||||
* 全部比对完成后汇总所有不足项一次性抛错。用于OMS在下发推送前拦截
|
||||
* (此前单据先落库、分配才失败,会在WMS残留"已审核"状态的垃圾单)
|
||||
* (此前单据先落库、分配才失败,会在WMS残留"已审核"状态的垃圾单)。
|
||||
* 下单冻结模式:businessOrderNumber 非空时行级可用量 = 库存可用 + 本单该行冻结记录量
|
||||
* (下单时已冻结导致可用=0,按纯可用校验必失败;冻结量加回后校验=防冻结漂移),
|
||||
* 冻结量<需求即报"下单冻结记录不足";重复下发保护原样保留。
|
||||
*/
|
||||
public void checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails) {
|
||||
public void checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails, String businessOrderNumber) {
|
||||
if (CollectionUtils.isEmpty(outMaterialDetails)) {
|
||||
return;
|
||||
}
|
||||
@@ -1232,9 +1254,17 @@ public class StockOutOrderApplicationService {
|
||||
}
|
||||
BigDecimal available = materialInventory.getAllocationQuantity() == null
|
||||
? BigDecimal.ZERO : materialInventory.getAllocationQuantity();
|
||||
// 下单冻结模式:可用量加回本单该行的冻结记录量(下单冻结导致可用=0,纯可用校验必失败);
|
||||
// 正常流冻结量≥需求(下单冻结/重锁保证),冻结量不足即报错=防冻结被异常释放的漂移
|
||||
BigDecimal frozen = BigDecimal.ZERO;
|
||||
if (businessOrderNumber != null && !businessOrderNumber.isEmpty()) {
|
||||
frozen = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity(businessOrderNumber, materialInventoryId);
|
||||
available = available.add(frozen);
|
||||
}
|
||||
if (available.compareTo(need) < 0) {
|
||||
errors.add(String.format("物料[%s]可用库存不足:需要%s,当前可用%s",
|
||||
materialDesc, need.stripTrailingZeros().toPlainString(), available.stripTrailingZeros().toPlainString()));
|
||||
errors.add(String.format("物料[%s]可用库存不足:需要%s,当前可用%s(含下单冻结%s)",
|
||||
materialDesc, need.stripTrailingZeros().toPlainString(),
|
||||
available.stripTrailingZeros().toPlainString(), frozen.stripTrailingZeros().toPlainString()));
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
|
||||
+5
@@ -80,6 +80,8 @@ public class HandoverTaskOrderDomainService {
|
||||
@Autowired
|
||||
private IHandoverTaskOrderService handoverTaskOrderService;
|
||||
@Autowired
|
||||
private com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService;
|
||||
@Autowired
|
||||
private MaterialInventoryMapper materialInventoryMapper;
|
||||
@Autowired
|
||||
private OutMaterialDetailMapper outMaterialDetailMapper;
|
||||
@@ -591,6 +593,9 @@ public class HandoverTaskOrderDomainService {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 下单冻结模式:交接扣减完成(库存-、冻结-),执行单承接的冻结记录置已交接终态
|
||||
// (无冻结记录时幂等无操作;数量扣减本身两种模式同逻辑,零改动)
|
||||
outOrderFreezeRecordDomainService.markHandoverCompleted(orderNumber);
|
||||
return clearoutParamDOs;
|
||||
}
|
||||
|
||||
|
||||
+37
@@ -140,4 +140,41 @@ public interface MaterialInventoryMapper extends BaseMapper<MaterialInventory>
|
||||
public Map<String,Object> queryShipperParameters(@Param("shipperId") Long shipperId,@Param("parametersName") String parametersName);
|
||||
|
||||
public Map<String, Object> queryMaterialClassify(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* OMS下单冻结-条件扣减:可用数量-quantity、冻结数量+quantity(库存数量不变),
|
||||
* WHERE 带可用数量充足条件,返回影响行数(0=可用不足),由调用方判0抛异常整体回滚防止并发超卖
|
||||
* (不可用现有 GREATEST 截断版批量SQL——静默截为0会掩盖超卖)
|
||||
*
|
||||
* @param materialInventoryId 库存行ID
|
||||
* @param quantity 冻结数量
|
||||
* @return 影响行数:1-成功 0-可用数量不足
|
||||
*/
|
||||
public int omsFreezeDeductAvailable(@Param("materialInventoryId") Long materialInventoryId, @Param("quantity") BigDecimal quantity);
|
||||
|
||||
/**
|
||||
* OMS下单解冻-条件释放:可用数量+quantity、冻结数量-quantity(库存数量不变),
|
||||
* WHERE 带冻结数量充足条件,返回影响行数(0=冻结不足),由调用方判0抛异常防止重复解冻/负数
|
||||
*
|
||||
* @param materialInventoryId 库存行ID
|
||||
* @param quantity 释放数量
|
||||
* @return 影响行数:1-成功 0-冻结数量不足
|
||||
*/
|
||||
public int omsReleaseFreezeToAvailable(@Param("materialInventoryId") Long materialInventoryId, @Param("quantity") BigDecimal quantity);
|
||||
|
||||
/**
|
||||
* OMS下单冻结单出库反审-还原为冻结:实物库存+quantity、冻结数量+quantity(可用不变),
|
||||
* 并还原重量/体积/面积;将已出库扣减状态还原为"下单已冻结待出库"(与交接扣减 ckjj 库存-冻结- 互逆)
|
||||
*
|
||||
* @param materialInventoryId 库存行ID
|
||||
* @param quantity 还原数量
|
||||
* @param netWeight 还原净重
|
||||
* @param grossWeight 还原毛重
|
||||
* @param volume 还原体积
|
||||
* @param area 还原面积
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int omsRestoreInventoryToFrozen(@Param("materialInventoryId") Long materialInventoryId, @Param("quantity") BigDecimal quantity,
|
||||
@Param("netWeight") BigDecimal netWeight, @Param("grossWeight") BigDecimal grossWeight,
|
||||
@Param("volume") BigDecimal volume, @Param("area") BigDecimal area);
|
||||
}
|
||||
+39
-1
@@ -647,7 +647,7 @@ public class MaterialInventoryImpl extends ServiceImpl<MaterialInventoryMapper,
|
||||
}
|
||||
inventoryAdjustmentRecord.setMaterialBaseInfoId(materialInventory.getMaterialBaseInfoId());
|
||||
inventoryAdjustmentRecord.setAdjustType(1L);
|
||||
if ("库存冻结".equals(adjustAction)) {
|
||||
if ("库存冻结".equals(adjustAction) || "下单冻结".equals(adjustAction)) {
|
||||
inventoryAdjustmentRecord.setAdjustType(3L);
|
||||
}
|
||||
inventoryAdjustmentRecord.setOrganizationId(materialInventory.getOrganizationId());
|
||||
@@ -803,6 +803,44 @@ public class MaterialInventoryImpl extends ServiceImpl<MaterialInventoryMapper,
|
||||
inventoryAdjustmentRecord.setAdjustedGrossWeight(materialInventory.getGrossWeight());
|
||||
inventoryAdjustmentRecord.setAdjustedVolume(materialInventory.getVolume());
|
||||
inventoryAdjustmentRecord.setAdjustedArea(materialInventory.getArea());
|
||||
} else if ("下单冻结".equals(adjustAction)) {
|
||||
// OMS下单冻结(防超卖):总库存不变;可用减少、冻结增加(数量从可用转至冻结,与"库存冻结"同构)
|
||||
BigDecimal invBefore = nz(materialInventory.getInventoryQuantity());
|
||||
BigDecimal allocBefore = nz(materialInventory.getAllocationQuantity());
|
||||
BigDecimal freezeBefore = nz(materialInventory.getFreezeQuantity());
|
||||
inventoryAdjustmentRecord.setInventoryBeforeQuantity(invBefore);
|
||||
inventoryAdjustmentRecord.setInventoryAfterQuantity(invBefore);
|
||||
inventoryAdjustmentRecord.setAllocationBeforeQuantity(allocBefore);
|
||||
inventoryAdjustmentRecord.setAllocationAfterQuantity(allocBefore.subtract(qty));
|
||||
inventoryAdjustmentRecord.setFreezeBeforeQuantity(freezeBefore);
|
||||
inventoryAdjustmentRecord.setFreezeAfterQuantity(freezeBefore.add(qty));
|
||||
inventoryAdjustmentRecord.setNetWeight(nz(materialInventory.getNetWeight()));
|
||||
inventoryAdjustmentRecord.setGrossWeight(nz(materialInventory.getGrossWeight()));
|
||||
inventoryAdjustmentRecord.setVolume(nz(materialInventory.getVolume()));
|
||||
inventoryAdjustmentRecord.setArea(nz(materialInventory.getArea()));
|
||||
inventoryAdjustmentRecord.setAdjustedNetWeight(nz(materialInventory.getNetWeight()));
|
||||
inventoryAdjustmentRecord.setAdjustedGrossWeight(nz(materialInventory.getGrossWeight()));
|
||||
inventoryAdjustmentRecord.setAdjustedVolume(nz(materialInventory.getVolume()));
|
||||
inventoryAdjustmentRecord.setAdjustedArea(nz(materialInventory.getArea()));
|
||||
} else if ("下单解冻".equals(adjustAction)) {
|
||||
// OMS取消/编辑重锁释放:总库存不变;可用增加、冻结减少(与"取消分配"同构)
|
||||
BigDecimal invBefore = nz(materialInventory.getInventoryQuantity());
|
||||
BigDecimal allocBefore = nz(materialInventory.getAllocationQuantity());
|
||||
BigDecimal freezeBefore = nz(materialInventory.getFreezeQuantity());
|
||||
inventoryAdjustmentRecord.setInventoryBeforeQuantity(invBefore);
|
||||
inventoryAdjustmentRecord.setInventoryAfterQuantity(invBefore);
|
||||
inventoryAdjustmentRecord.setAllocationBeforeQuantity(allocBefore);
|
||||
inventoryAdjustmentRecord.setAllocationAfterQuantity(allocBefore.add(qty));
|
||||
inventoryAdjustmentRecord.setFreezeBeforeQuantity(freezeBefore);
|
||||
inventoryAdjustmentRecord.setFreezeAfterQuantity(freezeBefore.subtract(qty));
|
||||
inventoryAdjustmentRecord.setNetWeight(nz(materialInventory.getNetWeight()));
|
||||
inventoryAdjustmentRecord.setGrossWeight(nz(materialInventory.getGrossWeight()));
|
||||
inventoryAdjustmentRecord.setVolume(nz(materialInventory.getVolume()));
|
||||
inventoryAdjustmentRecord.setArea(nz(materialInventory.getArea()));
|
||||
inventoryAdjustmentRecord.setAdjustedNetWeight(nz(materialInventory.getNetWeight()));
|
||||
inventoryAdjustmentRecord.setAdjustedGrossWeight(nz(materialInventory.getGrossWeight()));
|
||||
inventoryAdjustmentRecord.setAdjustedVolume(nz(materialInventory.getVolume()));
|
||||
inventoryAdjustmentRecord.setAdjustedArea(nz(materialInventory.getArea()));
|
||||
}
|
||||
com.mhd.system.api.domain.InventoryAdjustmentRecord inventoryAdjustmentRecordOMS = new com.mhd.system.api.domain.InventoryAdjustmentRecord();
|
||||
BeanUtils.copyProperties(inventoryAdjustmentRecord, inventoryAdjustmentRecordOMS);
|
||||
|
||||
+61
-1
@@ -1,6 +1,7 @@
|
||||
package com.mhd.wms.domain.outMaterialDetail.repository.persistence;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
@@ -40,6 +41,7 @@ import com.mhd.wms.domain.outMaterialDetail.repository.po.OutMaterialDetailWithO
|
||||
import com.mhd.wms.domain.outMaterialDetail.repository.po.OutStockListMaterialAgg;
|
||||
import com.mhd.wms.domain.outMaterialDetail.repository.todo.OutMaterialDetailBaseDO;
|
||||
import com.mhd.wms.domain.outMaterialDetail.repository.todo.OutMaterialDetailDO;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService;
|
||||
import com.mhd.wms.domain.outMaterialDetailSerialNumber.entity.OutMaterialDetailSerialNumber;
|
||||
import com.mhd.wms.domain.outMaterialDetailSerialNumber.repository.facade.IOutMaterialDetailSerialNumberService;
|
||||
import com.mhd.wms.domain.overStock.repository.todo.QueryOrderOverStockDO;
|
||||
@@ -71,6 +73,8 @@ public class OutMaterialDetailImpl extends ServiceImpl<OutMaterialDetailMapper,
|
||||
@Autowired
|
||||
private OutMaterialDetailMapper outMaterialDetailMapper;
|
||||
@Autowired
|
||||
private OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService;
|
||||
@Autowired
|
||||
private IOutMaterialDetailSerialNumberService outMaterialDetailSerialNumberService;
|
||||
@Autowired
|
||||
private IdGenerator idGenerator;
|
||||
@@ -427,7 +431,9 @@ public class OutMaterialDetailImpl extends ServiceImpl<OutMaterialDetailMapper,
|
||||
.eq(StockOutOrder::getOutOrderNumber, outOrderNumber)
|
||||
.eq(StockOutOrder::getDelFlag, 1));
|
||||
if (!CollectionUtils.isEmpty(stockOutOrderList) && !"yang_pin_chu_ku".equals(stockOutOrderList.get(0).getBusinessType())) {
|
||||
xgkc(outMaterialDetailList,outOrderNumber,loginUser);
|
||||
// 下单冻结模式分流:本单冻结明细(其库存行存在本单活跃冻结记录)数量不动(下单时已冻结,
|
||||
// 再调 xgkc 会冻结翻倍/可用错扣),仅校验冻结量充足并做承接标记;其余明细走现有分配扣减
|
||||
assignByFreezeMode(outMaterialDetailList, outOrderNumber, loginUser, stockOutOrderList.get(0).getBusinessOrderNo());
|
||||
}
|
||||
//保存物料明细信息
|
||||
outMaterialDetailList.forEach(outMaterialDetail -> {
|
||||
@@ -1016,15 +1022,61 @@ public class OutMaterialDetailImpl extends ServiceImpl<OutMaterialDetailMapper,
|
||||
return stockOutOrderPO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 分配扣减按库存冻结模式分流:
|
||||
* 明细的库存行存在【本单】活跃冻结记录(OMS下单冻结模式:下单时已冻结)→ 库存数量不动
|
||||
* (再调 xgkc 会二次冻结导致冻结翻倍/可用错扣),仅校验本单冻结量充足并做执行单承接标记
|
||||
* (FIFO 按量承接,余量保留业务单冻结,支持部分分配),明细绑定由后续统一逻辑完成;
|
||||
* 其他明细(分配库存冻结模式=现有行为)→ 调 xgkc 分配扣减(可用-、冻结+)。
|
||||
* ★单据级隔离:冻结记录按 businessOrderNo + materialInventoryId 双条件过滤,
|
||||
* 双单冻结同一库存行时互不误消费;businessOrderNo 为空(历史单)→ 查不到记录安全走原逻辑。
|
||||
* @author gen
|
||||
* @date 2026/9/12
|
||||
*/
|
||||
private void assignByFreezeMode(List<OutMaterialDetail> outMaterialDetailList, String outOrderNumber,
|
||||
LoginUser loginUser, String businessOrderNo) {
|
||||
List<OutMaterialDetail> normalAssignList = new ArrayList<>();
|
||||
for (OutMaterialDetail detail : outMaterialDetailList) {
|
||||
if (detail.getNowAllocationQuantity() == null
|
||||
|| detail.getNowAllocationQuantity().compareTo(BigDecimal.ZERO) <= 0
|
||||
|| detail.getMaterialInventoryId() == null) {
|
||||
// 无效行交给现有逻辑自然跳过,保持行为一致
|
||||
normalAssignList.add(detail);
|
||||
continue;
|
||||
}
|
||||
BigDecimal frozenSum = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity(businessOrderNo, detail.getMaterialInventoryId());
|
||||
if (frozenSum.compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (frozenSum.compareTo(detail.getNowAllocationQuantity()) < 0) {
|
||||
throw new ServiceException("物料[" + (detail.getMaterialCode() != null ? detail.getMaterialCode() : "")
|
||||
+ "]库存行[" + detail.getMaterialInventoryId() + "]本单下单冻结量[" + frozenSum
|
||||
+ "]小于本次分配数量[" + detail.getNowAllocationQuantity() + "],分配失败");
|
||||
}
|
||||
// 下单冻结模式明细:数量不动,冻结记录承接给执行单(本分配事务内,失败随事务回滚)
|
||||
outOrderFreezeRecordDomainService.undertakeFromFreezeRecords(
|
||||
businessOrderNo, detail.getMaterialInventoryId(), outOrderNumber, detail.getNowAllocationQuantity());
|
||||
} else {
|
||||
normalAssignList.add(detail);
|
||||
}
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(normalAssignList)) {
|
||||
xgkc(normalAssignList, outOrderNumber, loginUser);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void qxfp(List<OutMaterialDetailDO> outMaterialDetailList,String outOrderNumber,LoginUser loginUser) {
|
||||
StockOutOrder stockOutOrder = stockOutOrderMapper.selectOne(new QueryWrapper<StockOutOrder>().lambda()
|
||||
.eq(StockOutOrder::getDelFlag,1)
|
||||
.eq(StockOutOrder::getOutOrderNumber, outOrderNumber));
|
||||
// 下单冻结判据用业务单号(null 安全:查不到单/无单号时按无冻结记录处理,走原还原逻辑)
|
||||
String businessOrderNo = stockOutOrder == null ? null : stockOutOrder.getBusinessOrderNo();
|
||||
for (OutMaterialDetailDO outMaterialDetail : outMaterialDetailList) {
|
||||
Long materialInventoryId = outMaterialDetail.getMaterialInventoryId();
|
||||
BigDecimal allocationQuantity = outMaterialDetail.getActualQuantity();
|
||||
MaterialInventory materialInventoryPO = materialInventoryMapper.selectById(materialInventoryId);
|
||||
if (materialInventoryPO != null) {
|
||||
// 下单冻结模式明细(本单该库存行存在活跃冻结记录):分配时数量未动,取消分配不还原数量
|
||||
// (不可用 continue——会跳过循环尾部的子行 childrenqxfp 处理,须 if 包裹还原块)
|
||||
if (!outOrderFreezeRecordDomainService.hasOrderFreezeRecord(businessOrderNo, materialInventoryId)) {
|
||||
MaterialInventoryParamDO materialInventoryParamDO = new MaterialInventoryParamDO();
|
||||
materialInventoryParamDO.setMaterialInventoryId(materialInventoryId);
|
||||
materialInventoryParamDO.setQuantity(allocationQuantity);
|
||||
@@ -1046,6 +1098,7 @@ public class OutMaterialDetailImpl extends ServiceImpl<OutMaterialDetailMapper,
|
||||
materialInventoryOmsParamDO.setKctzType("取消分配");
|
||||
materialInventoryOmsParamDO.setLoginUser(loginUser);
|
||||
omsServiceFeign.omsEdit(materialInventoryOmsParamDO);
|
||||
}
|
||||
// //更新库存数量
|
||||
// //可用数量
|
||||
// BigDecimal oldAllocationQuantity = materialInventoryPO.getAllocationQuantity();
|
||||
@@ -1132,11 +1185,18 @@ public class OutMaterialDetailImpl extends ServiceImpl<OutMaterialDetailMapper,
|
||||
StockOutOrder stockOutOrder = stockOutOrderMapper.selectOne(new QueryWrapper<StockOutOrder>().lambda()
|
||||
.eq(StockOutOrder::getDelFlag,1)
|
||||
.eq(StockOutOrder::getOutOrderNumber, outOrderNumber));
|
||||
// 下单冻结判据用业务单号(null 安全:查不到单/无单号时按无冻结记录处理,走原还原逻辑)
|
||||
String businessOrderNo = stockOutOrder == null ? null : stockOutOrder.getBusinessOrderNo();
|
||||
for (OutMaterialDetailDO outMaterialDetail : outMaterialDetailList) {
|
||||
Long materialInventoryId = outMaterialDetail.getMaterialInventoryId();
|
||||
BigDecimal allocationQuantity = outMaterialDetail.getActualQuantity();
|
||||
MaterialInventory materialInventoryPO = materialInventoryMapper.selectById(materialInventoryId);
|
||||
if (materialInventoryPO != null) {
|
||||
// 下单冻结模式明细(本单该库存行存在活跃冻结记录):分配时数量未动,取消分配不还原数量
|
||||
// (叶子子行无嵌套子项,可安全 continue)
|
||||
if (outOrderFreezeRecordDomainService.hasOrderFreezeRecord(businessOrderNo, materialInventoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MaterialInventoryParamDO materialInventoryParamDO = new MaterialInventoryParamDO();
|
||||
materialInventoryParamDO.setMaterialInventoryId(materialInventoryId);
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.mhd.wms.domain.outOrderFreezeRecord.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.mhd.common.core.web.domain.BaseVOEntity;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* WMS出库单库存冻结记录对象 out_order_freeze_record
|
||||
*
|
||||
* @description OMS下单冻结库存模式(防超卖)的「业务单↔库存行↔冻结数量」占用关系:
|
||||
* OMS出库业务单下单即冻结WMS库存(可用-、冻结+,库存数量不变),
|
||||
* 编辑=原子释放旧冻结+按新明细重锁,取消=释放冻结,分配=承接(数量不动),
|
||||
* 交接=扣库存+释放冻结,反审=还原为冻结。
|
||||
* 本表是编辑重锁/取消解冻/分配承接/交接标记/反审还原的对账依据与单据级隔离判据。
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
@Data
|
||||
@TableName("out_order_freeze_record")
|
||||
public class OutOrderFreezeRecord extends BaseVOEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("冻结记录id")
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long freezeRecordId;
|
||||
|
||||
@ApiModelProperty("OMS出库业务单号")
|
||||
private String businessOrderNumber;
|
||||
|
||||
@ApiModelProperty("执行出库单号(分配承接时回填,可空)")
|
||||
private String executionOrderNumber;
|
||||
|
||||
@ApiModelProperty("明细唯一标识(OMS明细uniqueId)")
|
||||
private String materialDetailUniqueId;
|
||||
|
||||
@ApiModelProperty("库存行ID(material_inventory.material_inventory_id)")
|
||||
private Long materialInventoryId;
|
||||
|
||||
@ApiModelProperty("仓库ID")
|
||||
private Long warehouseId;
|
||||
|
||||
@ApiModelProperty("物料基础信息ID")
|
||||
private Long materialBaseInfoId;
|
||||
|
||||
@ApiModelProperty("冻结数量")
|
||||
private BigDecimal freezeQuantity;
|
||||
|
||||
@ApiModelProperty("冻结状态: 1-业务单冻结 2-执行单承接 3-已交接 4-已释放")
|
||||
private Integer freezeStatus;
|
||||
|
||||
@ApiModelProperty("关联单号(业务单号#明细uniqueId,与库存调整记录relate_no对应)")
|
||||
private String relateNo;
|
||||
|
||||
@ApiModelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 冻结状态: 业务单冻结(下单/编辑重锁后)
|
||||
*/
|
||||
public static final int STATUS_BUSINESS_FROZEN = 1;
|
||||
/**
|
||||
* 冻结状态: 执行单承接(WMS分配时数量不动,仅记录归属转移到执行单)
|
||||
*/
|
||||
public static final int STATUS_EXECUTION_UNDERTAKE = 2;
|
||||
/**
|
||||
* 冻结状态: 已交接(出库交接扣减库存后终态)
|
||||
*/
|
||||
public static final int STATUS_HANDOVER = 3;
|
||||
/**
|
||||
* 冻结状态: 已释放(取消/解冻/重锁时的旧记录)
|
||||
*/
|
||||
public static final int STATUS_RELEASED = 4;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.mhd.wms.domain.outOrderFreezeRecord.repository.facade;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord;
|
||||
|
||||
/**
|
||||
* WMS出库单库存冻结记录Service接口
|
||||
*
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
public interface IOutOrderFreezeRecordService extends IService<OutOrderFreezeRecord>
|
||||
{
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.mhd.wms.domain.outOrderFreezeRecord.repository.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord;
|
||||
|
||||
/**
|
||||
* WMS出库单库存冻结记录Mapper接口
|
||||
*
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
public interface OutOrderFreezeRecordMapper extends BaseMapper<OutOrderFreezeRecord>
|
||||
{
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.mhd.wms.domain.outOrderFreezeRecord.repository.persistence;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.repository.facade.IOutOrderFreezeRecordService;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.repository.mapper.OutOrderFreezeRecordMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* WMS出库单库存冻结记录Service业务层处理
|
||||
*
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
@Service
|
||||
public class OutOrderFreezeRecordImpl extends ServiceImpl<OutOrderFreezeRecordMapper, OutOrderFreezeRecord> implements IOutOrderFreezeRecordService {
|
||||
}
|
||||
+536
@@ -0,0 +1,536 @@
|
||||
package com.mhd.wms.domain.outOrderFreezeRecord.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.mhd.common.core.exception.ServiceException;
|
||||
import com.mhd.common.security.utils.SecurityUtils;
|
||||
import com.mhd.system.api.OmsServiceFeign;
|
||||
import com.mhd.system.api.domain.MaterialInventoryOmsParamDO;
|
||||
import com.mhd.system.api.domain.OmsFreezeInventoryParam;
|
||||
import com.mhd.system.api.domain.OutMaterialDetail;
|
||||
import com.mhd.system.api.model.LoginUser;
|
||||
import com.mhd.wms.domain.materialInventory.entity.MaterialInventory;
|
||||
import com.mhd.wms.domain.materialInventory.repository.facade.IMaterialInventoryService;
|
||||
import com.mhd.wms.domain.materialInventory.repository.mapper.MaterialInventoryMapper;
|
||||
import com.mhd.wms.domain.materialInventory.repository.todo.MaterialInventoryParamDO;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.repository.facade.IOutOrderFreezeRecordService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* WMS出库单库存冻结记录领域服务
|
||||
*
|
||||
* @description OMS下单冻结库存模式(防超卖):解决OMS下单与WMS分配库存时间差导致的出库库存不足问题。
|
||||
* 库存数量 = 可用数量 + 冻结数量,交接前全程不变(出库交接才扣减实物库存)。
|
||||
* 「下单」→ 冻结(可用-、冻结+,条件更新防并发超卖);
|
||||
* 「编辑」→ omsRelockInventory 同一事务先释放原锁定再按新明细重新锁定(原子操作);
|
||||
* 「取消」→ omsUnfreezeInventory 释放冻结(可用+、冻结-);
|
||||
* 「分配」→ 不动数量,冻结记录由业务单承接给执行单(undertakeFromFreezeRecords FIFO按量承接);
|
||||
* 「取消下发」→ releaseExecutionUndertake 承接归还业务单;
|
||||
* 「交接」→ markHandoverCompleted 置已交接终态(数量由现有 ckjj 扣减);
|
||||
* 「反审」→ 已出库 reactivateHandoverFreeze 重激活 + 实物/冻结还原。
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
@Service
|
||||
public class OutOrderFreezeRecordDomainService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OutOrderFreezeRecordDomainService.class);
|
||||
|
||||
@Autowired
|
||||
private IOutOrderFreezeRecordService outOrderFreezeRecordService;
|
||||
|
||||
@Autowired
|
||||
private MaterialInventoryMapper materialInventoryMapper;
|
||||
|
||||
@Autowired
|
||||
private IMaterialInventoryService materialInventoryService;
|
||||
|
||||
@Autowired
|
||||
private OmsServiceFeign omsServiceFeign;
|
||||
|
||||
// ================================ OMS 冻结三接口 ================================
|
||||
|
||||
/**
|
||||
* OMS出库业务单下单冻结库存(幂等:同业务单号已有活跃冻结记录时直接返回成功)
|
||||
* 明细必须携带 materialInventoryId 精确定位库存行(下单冻结模式仓库明细强制必选,OMS侧已前置校验)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean omsFreezeInventory(OmsFreezeInventoryParam param) {
|
||||
validateParam(param);
|
||||
String businessOrderNumber = param.getOutOrderNumber();
|
||||
List<OutOrderFreezeRecord> activeRecords = listActiveRecords(businessOrderNumber);
|
||||
if (!CollectionUtils.isEmpty(activeRecords)) {
|
||||
// 幂等:Feign超时重试/重复提交场景,已有活跃冻结视为成功
|
||||
log.info("下单冻结幂等:出库业务单[{}]已存在{}条活跃冻结记录,跳过重复冻结", businessOrderNumber, activeRecords.size());
|
||||
return true;
|
||||
}
|
||||
freezeByDetails(param, businessOrderNumber);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* OMS出库业务单取消/删除-释放冻结库存(可用+、冻结-,库存数量不变;幂等:无活跃冻结直接成功)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean omsUnfreezeInventory(String businessOrderNumber, Long operatorId, String operatorName) {
|
||||
if (!StringUtils.hasText(businessOrderNumber)) {
|
||||
throw new ServiceException("出库业务单号不能为空");
|
||||
}
|
||||
List<OutOrderFreezeRecord> activeRecords = listActiveRecords(businessOrderNumber);
|
||||
if (CollectionUtils.isEmpty(activeRecords)) {
|
||||
// 幂等:未冻结(模式切换前老单/重复取消)直接成功
|
||||
log.info("释放冻结幂等:出库业务单[{}]无活跃冻结记录", businessOrderNumber);
|
||||
return true;
|
||||
}
|
||||
// 已承接给执行单/已交接的冻结不可直接释放,必须先走取消下发/反审链路
|
||||
for (OutOrderFreezeRecord record : activeRecords) {
|
||||
if (record.getFreezeStatus() != null && record.getFreezeStatus() == OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]冻结已被执行单承接,请先取消下发");
|
||||
}
|
||||
if (record.getFreezeStatus() != null && record.getFreezeStatus() == OutOrderFreezeRecord.STATUS_HANDOVER) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]冻结已交接(已出库),请走反审流程");
|
||||
}
|
||||
}
|
||||
LoginUserHolder holder = resolveLoginUser(operatorId, operatorName);
|
||||
for (OutOrderFreezeRecord record : activeRecords) {
|
||||
releaseOneRecord(record, businessOrderNumber, holder);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* OMS出库业务单编辑-原子重锁:同一事务内先释放原冻结,再按新明细重新冻结。
|
||||
* 任一新明细可用不足→抛异常整体回滚(旧冻结同样还原,满足"先释放再重新锁定"的原子性要求)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean omsRelockInventory(OmsFreezeInventoryParam param) {
|
||||
validateParam(param);
|
||||
String businessOrderNumber = param.getOutOrderNumber();
|
||||
List<OutOrderFreezeRecord> activeRecords = listActiveRecords(businessOrderNumber);
|
||||
for (OutOrderFreezeRecord record : activeRecords) {
|
||||
if (record.getFreezeStatus() != null && record.getFreezeStatus() != OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]已下发WMS(冻结已被承接),不可编辑,请先取消下发");
|
||||
}
|
||||
}
|
||||
LoginUserHolder holder = resolveLoginUser(param.getOperatorId(), param.getOperatorName());
|
||||
// 1. 先按原冻结记录精确释放(数量以冻结记录为准,防止与明细数量漂移)
|
||||
for (OutOrderFreezeRecord record : activeRecords) {
|
||||
releaseOneRecord(record, businessOrderNumber, holder);
|
||||
}
|
||||
// 2. 再按新明细重新冻结(任一行失败抛异常,整个事务回滚=旧冻结也还原)
|
||||
freezeByDetails(param, businessOrderNumber);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================ 核心链路状态机方法 ================================
|
||||
|
||||
/**
|
||||
* WMS分配-冻结承接(FIFO按量):校验本单本行冻结量≥本次分配量后,从本单该行 status=1 记录
|
||||
* 按id升序逐条扣减承接量:记录余量承接完→status=2+执行单号;余量不足整条→只改freeze_quantity为承接量,
|
||||
* 剩余量另插一条status=1新记录(保证任一时刻记录数量之和=该单该行总冻结量,守恒)。
|
||||
* 数量字段不动(下单时已冻结)。在分配事务内调用,失败随分配整体回滚。
|
||||
*
|
||||
* @param businessOrderNumber 业务单号(单据级隔离判据,防止误消费别单冻结)
|
||||
* @param materialInventoryId 库存行ID
|
||||
* @param executionOrderNumber 执行出库单号(WMS出库单号)
|
||||
* @param undertakeQuantity 本次承接(分配)数量
|
||||
* @return true-承接成功(调用前需先校验冻结量充足)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean undertakeFromFreezeRecords(String businessOrderNumber, Long materialInventoryId,
|
||||
String executionOrderNumber, BigDecimal undertakeQuantity) {
|
||||
BigDecimal remaining = undertakeQuantity;
|
||||
List<OutOrderFreezeRecord> records = listOrderRowActiveRecords(businessOrderNumber, materialInventoryId);
|
||||
records.sort(Comparator.comparing(r -> r.getFreezeRecordId() == null ? Long.MAX_VALUE : r.getFreezeRecordId()));
|
||||
for (OutOrderFreezeRecord record : records) {
|
||||
if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
break;
|
||||
}
|
||||
BigDecimal recordQty = record.getFreezeQuantity() == null ? BigDecimal.ZERO : record.getFreezeQuantity();
|
||||
if (recordQty.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (recordQty.compareTo(remaining) <= 0) {
|
||||
// 整条记录承接完
|
||||
record.setFreezeStatus(OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE);
|
||||
record.setExecutionOrderNumber(executionOrderNumber);
|
||||
record.setUpdateTime(new Date());
|
||||
outOrderFreezeRecordService.updateById(record);
|
||||
remaining = remaining.subtract(recordQty);
|
||||
} else {
|
||||
// 部分承接:本条记承接量,余量拆一条新记录保留 status=1
|
||||
BigDecimal keepQty = recordQty.subtract(remaining);
|
||||
record.setFreezeQuantity(remaining);
|
||||
record.setFreezeStatus(OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE);
|
||||
record.setExecutionOrderNumber(executionOrderNumber);
|
||||
record.setUpdateTime(new Date());
|
||||
outOrderFreezeRecordService.updateById(record);
|
||||
OutOrderFreezeRecord keepRecord = new OutOrderFreezeRecord();
|
||||
keepRecord.setBusinessOrderNumber(record.getBusinessOrderNumber());
|
||||
keepRecord.setMaterialDetailUniqueId(record.getMaterialDetailUniqueId());
|
||||
keepRecord.setMaterialInventoryId(record.getMaterialInventoryId());
|
||||
keepRecord.setWarehouseId(record.getWarehouseId());
|
||||
keepRecord.setMaterialBaseInfoId(record.getMaterialBaseInfoId());
|
||||
keepRecord.setFreezeQuantity(keepQty);
|
||||
keepRecord.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN);
|
||||
keepRecord.setRelateNo(record.getRelateNo());
|
||||
keepRecord.setCreateBy(record.getCreateBy());
|
||||
keepRecord.setCreateByName(record.getCreateByName());
|
||||
keepRecord.setCreateTime(new Date());
|
||||
keepRecord.setDelFlag(1);
|
||||
outOrderFreezeRecordService.save(keepRecord);
|
||||
remaining = BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消下发/反审(未出库)-解除执行单承接:冻结归还业务单(数量不动,仅状态 2→1)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean releaseExecutionUndertake(String executionOrderNumber) {
|
||||
if (!StringUtils.hasText(executionOrderNumber)) {
|
||||
return true;
|
||||
}
|
||||
List<OutOrderFreezeRecord> records = outOrderFreezeRecordService.list(new LambdaQueryWrapper<OutOrderFreezeRecord>()
|
||||
.eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber)
|
||||
.eq(OutOrderFreezeRecord::getFreezeStatus, OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE)
|
||||
.eq(OutOrderFreezeRecord::getDelFlag, 1));
|
||||
for (OutOrderFreezeRecord record : records) {
|
||||
record.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN);
|
||||
record.setExecutionOrderNumber(null);
|
||||
record.setUpdateTime(new Date());
|
||||
outOrderFreezeRecordService.updateById(record);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库交接完成-冻结记录置已交接终态(数量已在交接时由现有 ckjj 扣减:库存-、冻结-)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean markHandoverCompleted(String executionOrderNumber) {
|
||||
if (!StringUtils.hasText(executionOrderNumber)) {
|
||||
return true;
|
||||
}
|
||||
List<OutOrderFreezeRecord> records = outOrderFreezeRecordService.list(new LambdaQueryWrapper<OutOrderFreezeRecord>()
|
||||
.eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber)
|
||||
.eq(OutOrderFreezeRecord::getFreezeStatus, OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE)
|
||||
.eq(OutOrderFreezeRecord::getDelFlag, 1));
|
||||
for (OutOrderFreezeRecord record : records) {
|
||||
record.setFreezeStatus(OutOrderFreezeRecord.STATUS_HANDOVER);
|
||||
record.setUpdateTime(new Date());
|
||||
outOrderFreezeRecordService.updateById(record);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反审(已出库)-重新激活:已交接冻结记录(status=3)归还业务单(status=1,数量不动),
|
||||
* 业务单回到未下发可编辑态,冻结继续由业务单持有(实物/冻结数量由调用方还原)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean reactivateHandoverFreeze(String executionOrderNumber) {
|
||||
if (!StringUtils.hasText(executionOrderNumber)) {
|
||||
return true;
|
||||
}
|
||||
List<OutOrderFreezeRecord> records = outOrderFreezeRecordService.list(new LambdaQueryWrapper<OutOrderFreezeRecord>()
|
||||
.eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber)
|
||||
.eq(OutOrderFreezeRecord::getFreezeStatus, OutOrderFreezeRecord.STATUS_HANDOVER)
|
||||
.eq(OutOrderFreezeRecord::getDelFlag, 1));
|
||||
for (OutOrderFreezeRecord record : records) {
|
||||
record.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN);
|
||||
record.setExecutionOrderNumber(null);
|
||||
record.setUpdateTime(new Date());
|
||||
outOrderFreezeRecordService.updateById(record);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================ 查询判据 ================================
|
||||
|
||||
/**
|
||||
* 执行单是否存在活跃冻结记录(status in 2-承接 3-已交接):判断该执行单是否下单冻结模式单据
|
||||
*/
|
||||
public boolean hasFreezeForExecutionOrder(String executionOrderNumber) {
|
||||
if (!StringUtils.hasText(executionOrderNumber)) {
|
||||
return false;
|
||||
}
|
||||
int count = outOrderFreezeRecordService.count(new LambdaQueryWrapper<OutOrderFreezeRecord>()
|
||||
.eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber)
|
||||
.in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList(
|
||||
OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE,
|
||||
OutOrderFreezeRecord.STATUS_HANDOVER))
|
||||
.eq(OutOrderFreezeRecord::getDelFlag, 1));
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本单该库存行的活跃冻结量合计(status in 1-业务单冻结 2-执行单承接)。
|
||||
* 单据级隔离判据:businessOrderNumber 必传(双单冻结同行时防止误消费别单冻结)
|
||||
*/
|
||||
public BigDecimal sumOrderFreezeQuantity(String businessOrderNumber, Long materialInventoryId) {
|
||||
if (!StringUtils.hasText(businessOrderNumber) || materialInventoryId == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
List<OutOrderFreezeRecord> records = listOrderRowActiveRecords(businessOrderNumber, materialInventoryId);
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
for (OutOrderFreezeRecord record : records) {
|
||||
if (record.getFreezeQuantity() != null) {
|
||||
sum = sum.add(record.getFreezeQuantity());
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本单该库存行是否存在活跃冻结记录(取消分配分支判据)
|
||||
*/
|
||||
public boolean hasOrderFreezeRecord(String businessOrderNumber, Long materialInventoryId) {
|
||||
return sumOrderFreezeQuantity(businessOrderNumber, materialInventoryId).compareTo(BigDecimal.ZERO) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓库是否存在活跃冻结记录(status in 1,2;仓库"库存锁定模式"切换守卫用:
|
||||
* 存在在途冻结单时切换模式会导致在途单解冻/承接逻辑错乱)
|
||||
*/
|
||||
public boolean hasActiveFreezeByWarehouse(Long warehouseId) {
|
||||
if (warehouseId == null) {
|
||||
return false;
|
||||
}
|
||||
int count = outOrderFreezeRecordService.count(new LambdaQueryWrapper<OutOrderFreezeRecord>()
|
||||
.eq(OutOrderFreezeRecord::getWarehouseId, warehouseId)
|
||||
.in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList(
|
||||
OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN,
|
||||
OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE))
|
||||
.eq(OutOrderFreezeRecord::getDelFlag, 1));
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
// ================================ 私有方法 ================================
|
||||
|
||||
private void validateParam(OmsFreezeInventoryParam param) {
|
||||
if (param == null || !StringUtils.hasText(param.getOutOrderNumber())) {
|
||||
throw new ServiceException("出库业务单号不能为空");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(param.getDetails())) {
|
||||
throw new ServiceException("冻结明细不能为空");
|
||||
}
|
||||
for (OutMaterialDetail detail : param.getDetails()) {
|
||||
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ServiceException("出库业务单[" + param.getOutOrderNumber() + "]明细["
|
||||
+ (detail.getMaterialCode() != null ? detail.getMaterialCode() : "未知物料") + "]冻结数量必须大于0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按明细冻结:明细必须携带 materialInventoryId(下单冻结模式强制必选),
|
||||
* 逐明细单行冻结——先写台账(调整前快照)再条件更新(0行=可用不足抛异常整体回滚),
|
||||
* 全部扣减成功后统一写冻结记录+推OMS镜像(推镜像放事务末尾,缩小漂移窗口)
|
||||
*/
|
||||
private void freezeByDetails(OmsFreezeInventoryParam param, String businessOrderNumber) {
|
||||
LoginUserHolder holder = resolveLoginUser(param.getOperatorId(), param.getOperatorName());
|
||||
List<PendingFreeze> pendingFreezes = new ArrayList<>();
|
||||
for (OutMaterialDetail detail : param.getDetails()) {
|
||||
if (detail.getMaterialInventoryId() == null || detail.getMaterialInventoryId() <= 0) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料["
|
||||
+ (detail.getMaterialCode() != null ? detail.getMaterialCode() : detail.getMaterialBaseInfoId())
|
||||
+ "]未关联库存行,无法冻结(下单冻结模式仓库明细必须选择库存批次)");
|
||||
}
|
||||
MaterialInventory row = materialInventoryMapper.selectById(detail.getMaterialInventoryId());
|
||||
if (row == null || (row.getDelFlag() != null && row.getDelFlag() != 1)) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料["
|
||||
+ (detail.getMaterialCode() != null ? detail.getMaterialCode() : "") + "]关联的库存行["
|
||||
+ detail.getMaterialInventoryId() + "]不存在,冻结失败");
|
||||
}
|
||||
BigDecimal freezeQuantity = detail.getQuantity();
|
||||
BigDecimal available = row.getAllocationQuantity() == null ? BigDecimal.ZERO : row.getAllocationQuantity();
|
||||
// 先行级预检给出友好错误(并发下仍由条件更新兜底判定)
|
||||
if (available.compareTo(freezeQuantity) < 0) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料["
|
||||
+ (detail.getMaterialCode() != null ? detail.getMaterialCode() : "") + "]库存行["
|
||||
+ row.getMaterialInventoryId() + "]可用数量不足,需要:" + freezeQuantity + ",当前可用:" + available + ",下单冻结失败");
|
||||
}
|
||||
String relateNo = buildRelateNo(businessOrderNumber, detail.getUniqueId());
|
||||
// 1.先写库存调整记录(调整前快照)2.条件更新扣可用加冻结(0行=并发下可用不足),失败抛异常整体回滚
|
||||
MaterialInventoryParamDO paramDO = buildParamDO(row, freezeQuantity, "7", relateNo);
|
||||
materialInventoryService.kctzjl(paramDO, "下单冻结", holder.loginUser);
|
||||
int affected = materialInventoryMapper.omsFreezeDeductAvailable(row.getMaterialInventoryId(), freezeQuantity);
|
||||
if (affected != 1) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料["
|
||||
+ (detail.getMaterialCode() != null ? detail.getMaterialCode() : "") + "]库存行["
|
||||
+ row.getMaterialInventoryId() + "]可用数量不足,冻结失败(需求:" + freezeQuantity + ")");
|
||||
}
|
||||
pendingFreezes.add(new PendingFreeze(row, freezeQuantity, detail.getUniqueId(), relateNo));
|
||||
}
|
||||
// 全部扣减成功后:写冻结记录 + 推送OMS镜像库存
|
||||
for (PendingFreeze pending : pendingFreezes) {
|
||||
insertFreezeRecord(pending.row, pending.quantity, businessOrderNumber, pending.uniqueId, pending.relateNo, holder);
|
||||
MaterialInventoryParamDO paramDO = buildParamDO(pending.row, pending.quantity, "7", pending.relateNo);
|
||||
pushOmsMirror(paramDO, "下单冻结");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放单条冻结记录:库存调整记录(下单解冻)+ 条件更新(可用+、冻结-)+ 记录置已释放 + 推OMS镜像
|
||||
*/
|
||||
private void releaseOneRecord(OutOrderFreezeRecord record, String businessOrderNumber, LoginUserHolder holder) {
|
||||
MaterialInventory row = materialInventoryMapper.selectById(record.getMaterialInventoryId());
|
||||
if (row == null || (row.getDelFlag() != null && row.getDelFlag() != 1)) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]冻结记录对应库存行["
|
||||
+ record.getMaterialInventoryId() + "]不存在,解冻失败");
|
||||
}
|
||||
MaterialInventoryParamDO paramDO = buildParamDO(row, record.getFreezeQuantity(), "3", record.getRelateNo());
|
||||
materialInventoryService.kctzjl(paramDO, "下单解冻", holder.loginUser);
|
||||
int affected = materialInventoryMapper.omsReleaseFreezeToAvailable(record.getMaterialInventoryId(), record.getFreezeQuantity());
|
||||
if (affected != 1) {
|
||||
throw new ServiceException("出库业务单[" + businessOrderNumber + "]库存行["
|
||||
+ record.getMaterialInventoryId() + "]冻结数量不足,解冻失败");
|
||||
}
|
||||
record.setFreezeStatus(OutOrderFreezeRecord.STATUS_RELEASED);
|
||||
record.setUpdateTime(new Date());
|
||||
outOrderFreezeRecordService.updateById(record);
|
||||
pushOmsMirror(buildParamDO(row, record.getFreezeQuantity(), "3", record.getRelateNo()), "下单解冻");
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装库存变更参数:匹配键全部从【库存行】取值(OMS镜像按
|
||||
* materialBaseInfoId+warehouseId+inOrderNumber+lotNumber+batchNumber+purchaseDate 精确匹配,
|
||||
* 从明细取会因缺 lotNo/purchaseDate 匹配到错误镜像行)
|
||||
*/
|
||||
private MaterialInventoryParamDO buildParamDO(MaterialInventory row, BigDecimal quantity, String type, String relateNo) {
|
||||
MaterialInventoryParamDO paramDO = new MaterialInventoryParamDO();
|
||||
BigDecimal zero = BigDecimal.ZERO;
|
||||
paramDO.setArea(zero);
|
||||
paramDO.setVolume(zero);
|
||||
paramDO.setNetWeight(zero);
|
||||
paramDO.setGrossWeight(zero);
|
||||
paramDO.setQuantity(quantity);
|
||||
paramDO.setType(type);
|
||||
paramDO.setMaterialInventoryId(row.getMaterialInventoryId());
|
||||
paramDO.setRelateNo(relateNo);
|
||||
paramDO.setMaterialBaseInfoId(row.getMaterialBaseInfoId());
|
||||
paramDO.setWarehouseId(row.getWarehouseId());
|
||||
paramDO.setLotNumber(row.getLotNumber());
|
||||
paramDO.setBatchNumber(row.getBatchNumber());
|
||||
paramDO.setInOrderNumber(row.getInOrderNumber());
|
||||
paramDO.setProductionDate(row.getProductionDate());
|
||||
paramDO.setExpiryDate(row.getExpiryDate());
|
||||
paramDO.setPurchaseDate(row.getPurchaseDate());
|
||||
return paramDO;
|
||||
}
|
||||
|
||||
private void insertFreezeRecord(MaterialInventory row, BigDecimal quantity, String businessOrderNumber,
|
||||
Long uniqueId, String relateNo, LoginUserHolder holder) {
|
||||
OutOrderFreezeRecord record = new OutOrderFreezeRecord();
|
||||
record.setBusinessOrderNumber(businessOrderNumber);
|
||||
record.setMaterialDetailUniqueId(uniqueId == null ? null : String.valueOf(uniqueId));
|
||||
record.setMaterialInventoryId(row.getMaterialInventoryId());
|
||||
record.setWarehouseId(row.getWarehouseId());
|
||||
record.setMaterialBaseInfoId(row.getMaterialBaseInfoId());
|
||||
record.setFreezeQuantity(quantity);
|
||||
record.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN);
|
||||
record.setRelateNo(relateNo);
|
||||
record.setCreateBy(holder.loginUser.getUserid());
|
||||
record.setCreateByName(holder.operatorName);
|
||||
record.setCreateTime(new Date());
|
||||
record.setDelFlag(1);
|
||||
outOrderFreezeRecordService.save(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送OMS镜像库存(type=7下单冻结:可用-冻结+ / type=3下单解冻:可用+冻结-),与分配/交接推送同模式
|
||||
*/
|
||||
private void pushOmsMirror(MaterialInventoryParamDO paramDO, String kctzType) {
|
||||
try {
|
||||
MaterialInventoryOmsParamDO omsParamDO = new MaterialInventoryOmsParamDO();
|
||||
BeanUtils.copyProperties(paramDO, omsParamDO);
|
||||
omsParamDO.setKctzType(kctzType);
|
||||
omsServiceFeign.omsEdit(omsParamDO);
|
||||
} catch (Exception e) {
|
||||
// 镜像推送失败不阻断冻结主流程(与分配/交接链路同口径),由OMS侧定时对账兜底
|
||||
log.error("下单冻结推送OMS镜像库存失败:relateNo={}, kctzType={}", paramDO.getRelateNo(), kctzType, e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OutOrderFreezeRecord> listActiveRecords(String businessOrderNumber) {
|
||||
return outOrderFreezeRecordService.list(new LambdaQueryWrapper<OutOrderFreezeRecord>()
|
||||
.eq(OutOrderFreezeRecord::getBusinessOrderNumber, businessOrderNumber)
|
||||
.in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList(
|
||||
OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN,
|
||||
OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE,
|
||||
OutOrderFreezeRecord.STATUS_HANDOVER))
|
||||
.eq(OutOrderFreezeRecord::getDelFlag, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 本单该行的活跃冻结记录(status 1/2,承接/校验/取消分配判据共用)
|
||||
*/
|
||||
private List<OutOrderFreezeRecord> listOrderRowActiveRecords(String businessOrderNumber, Long materialInventoryId) {
|
||||
return outOrderFreezeRecordService.list(new LambdaQueryWrapper<OutOrderFreezeRecord>()
|
||||
.eq(OutOrderFreezeRecord::getBusinessOrderNumber, businessOrderNumber)
|
||||
.eq(OutOrderFreezeRecord::getMaterialInventoryId, materialInventoryId)
|
||||
.in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList(
|
||||
OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN,
|
||||
OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE))
|
||||
.eq(OutOrderFreezeRecord::getDelFlag, 1));
|
||||
}
|
||||
|
||||
private String buildRelateNo(String businessOrderNumber, Long uniqueId) {
|
||||
return uniqueId != null ? businessOrderNumber + "#" + uniqueId : businessOrderNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feign调用可能无登录上下文(SecurityUtils抛异常),容错获取并兜底(与 reverseExecutionOrder 同模式)
|
||||
*/
|
||||
private LoginUserHolder resolveLoginUser(Long operatorId, String operatorName) {
|
||||
LoginUserHolder holder = new LoginUserHolder();
|
||||
try {
|
||||
holder.loginUser = SecurityUtils.getLoginUser();
|
||||
} catch (Exception e) {
|
||||
holder.loginUser = new LoginUser();
|
||||
holder.loginUser.setUserid(operatorId != null ? operatorId : 0L);
|
||||
holder.loginUser.setUsername(StringUtils.hasText(operatorName) ? operatorName : "system");
|
||||
}
|
||||
holder.operatorName = StringUtils.hasText(operatorName) ? operatorName
|
||||
: (holder.loginUser.getUsername() != null ? holder.loginUser.getUsername() : "system");
|
||||
return holder;
|
||||
}
|
||||
|
||||
private static class LoginUserHolder {
|
||||
private LoginUser loginUser;
|
||||
private String operatorName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 待落库的冻结占位(先全部扣减成功,再统一写冻结记录+推镜像)
|
||||
*/
|
||||
private static class PendingFreeze {
|
||||
private final MaterialInventory row;
|
||||
private final BigDecimal quantity;
|
||||
private final Long uniqueId;
|
||||
private final String relateNo;
|
||||
|
||||
private PendingFreeze(MaterialInventory row, BigDecimal quantity, Long uniqueId, String relateNo) {
|
||||
this.row = row;
|
||||
this.quantity = quantity;
|
||||
this.uniqueId = uniqueId;
|
||||
this.relateNo = relateNo;
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-11
@@ -123,6 +123,8 @@ public class StockOutOrderDomainService {
|
||||
@Autowired
|
||||
private IStockOutOrderService stockOutOrderService;
|
||||
@Autowired
|
||||
private com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService;
|
||||
@Autowired
|
||||
private IOutMaterialDetailService outMaterialDetailService;
|
||||
@Autowired
|
||||
private IDeliveryDetailsLinkService deliveryDetailsLinkService;
|
||||
@@ -2611,6 +2613,13 @@ public class StockOutOrderDomainService {
|
||||
stockOutOrderService.updateById(stockOutOrderDB);
|
||||
return;
|
||||
}
|
||||
// 下单冻结模式判断:该执行单持有承接/已交接冻结记录时,分配时数量未动(下单时已冻结),
|
||||
// 取消下发不还原库存数量,仅解除承接(冻结归还业务单);无冻结记录走现有分配占用还原。
|
||||
// 两分支互斥:双走会虚增可用
|
||||
boolean freezeMode = outOrderFreezeRecordDomainService.hasFreezeForExecutionOrder(outOrderNumber);
|
||||
if (freezeMode) {
|
||||
outOrderFreezeRecordDomainService.releaseExecutionUndertake(outOrderNumber);
|
||||
}
|
||||
for (OutMaterialDetail materialDetail : materialDetails) {
|
||||
//还原库存
|
||||
BigDecimal allocationQuantity = materialDetail.getAllocationQuantity();
|
||||
@@ -2619,7 +2628,7 @@ public class StockOutOrderDomainService {
|
||||
//以下两种情况都无可还原的冻结量,跳过还原直接删单,保证此类单据也能被取消/补偿清理:
|
||||
//1.库存不存在(如OMS漏传库存ID的历史脏单);
|
||||
//2.未分配的明细(allocationQuantity为空或0,如下发失败后的补偿清理)
|
||||
if (materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (!freezeMode && materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) {
|
||||
materialInventory.setAllocationQuantity((materialInventory.getAllocationQuantity() == null ? BigDecimal.ZERO : materialInventory.getAllocationQuantity()).add(allocationQuantity));
|
||||
materialInventory.setFreezeQuantity((materialInventory.getFreezeQuantity() == null ? BigDecimal.ZERO : materialInventory.getFreezeQuantity()).subtract(allocationQuantity));
|
||||
materialInventoryMapper.updateById(materialInventory);
|
||||
@@ -2702,12 +2711,17 @@ public class StockOutOrderDomainService {
|
||||
// 出库/未出库分支: 未出库单据(WMS状态非9)实物库存未被扣减, 只还原分配占用(可用+冻结-),
|
||||
// 不做实物还原也不推OMS——否则会虚增库存; 已出库(9)分支还原实物并推OMS(防御保留, 业务单反审已拦截已出库单)
|
||||
boolean shipped = stockOutOrderDB.getStatus() != null && stockOutOrderDB.getStatus() == 9;
|
||||
// 下单冻结模式判断:该执行单持有承接/已交接冻结记录时按下单冻结语义还原
|
||||
// (数量从"可用"口径转为"冻结"口径——分配时数量未动、交接扣的是下单冻结量,还原必须回到冻结待出库态)
|
||||
boolean freezeMode = outOrderFreezeRecordDomainService.hasFreezeForExecutionOrder(outOrderNumber);
|
||||
for (OutMaterialDetail materialDetail : materialDetails) {
|
||||
MaterialInventory materialInventory = materialInventoryMapper.selectById(materialDetail.getMaterialInventoryId());
|
||||
if (!shipped) {
|
||||
// ===== 未出库: 还原分配占用(与 cancelExecutionOrder 的取消分配同口径) =====
|
||||
// 下单冻结模式:分配时数量未动(下单时已冻结),跳过数量还原,
|
||||
// 冻结归还业务单由循环后统一处理(2→1)
|
||||
BigDecimal allocationQuantity = materialDetail.getAllocationQuantity();
|
||||
if (materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (!freezeMode && materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) {
|
||||
materialInventory.setAllocationQuantity((materialInventory.getAllocationQuantity() == null ? BigDecimal.ZERO : materialInventory.getAllocationQuantity()).add(allocationQuantity));
|
||||
materialInventory.setFreezeQuantity((materialInventory.getFreezeQuantity() == null ? BigDecimal.ZERO : materialInventory.getFreezeQuantity()).subtract(allocationQuantity));
|
||||
materialInventory.setUpdateTime(new Date());
|
||||
@@ -2761,15 +2775,31 @@ public class StockOutOrderDomainService {
|
||||
//库存调整记录【出库反审】(kctzjl 内部取调整前快照, 与交接扣减先记录后改库存的顺序一致)
|
||||
restoreParam.setRelateNo(outOrderNumber + "#" + materialDetail.getUniqueId());
|
||||
materialInventoryService.kctzjl(restoreParam, "出库反审", loginUser);
|
||||
//type=1 上架还原: 实物库存/可用数量加回, 冻结数量不动
|
||||
restoreParam.setType("1");
|
||||
materialInventoryService.xgkc(restoreParam);
|
||||
//推送OMS还原预约库存(type=1: OMS侧实物/可用加回, 与交接 omsEdit 扣减互逆)
|
||||
MaterialInventoryOmsParamDO omsParam = new MaterialInventoryOmsParamDO();
|
||||
BeanUtils.copyProperties(restoreParam, omsParam);
|
||||
omsParam.setKctzType("出库反审");
|
||||
omsParam.setLoginUser(loginUser);
|
||||
omsServiceFeign.omsEdit(omsParam);
|
||||
if (freezeMode) {
|
||||
//下单冻结模式:还原为"下单已冻结待出库"——实物+、冻结+(可用不变),与交接扣减(库存-冻结-)互逆
|
||||
materialInventoryMapper.omsRestoreInventoryToFrozen(materialInventoryId,
|
||||
effectiveQuantity, netWeight, grossWeight, volume, area);
|
||||
//推送OMS镜像(两步合成"实物+冻结+",缺一不可):type=1 实物/可用加回 → type=7 可用转冻结
|
||||
MaterialInventoryOmsParamDO omsFrozen = new MaterialInventoryOmsParamDO();
|
||||
BeanUtils.copyProperties(restoreParam, omsFrozen);
|
||||
omsFrozen.setKctzType("出库反审");
|
||||
omsFrozen.setLoginUser(loginUser);
|
||||
omsFrozen.setType("1");
|
||||
omsServiceFeign.omsEdit(omsFrozen);
|
||||
omsFrozen.setType("7");
|
||||
omsFrozen.setKctzType("出库反审-还原冻结");
|
||||
omsServiceFeign.omsEdit(omsFrozen);
|
||||
} else {
|
||||
//type=1 上架还原: 实物库存/可用数量加回, 冻结数量不动
|
||||
restoreParam.setType("1");
|
||||
materialInventoryService.xgkc(restoreParam);
|
||||
//推送OMS还原预约库存(type=1: OMS侧实物/可用加回, 与交接 omsEdit 扣减互逆)
|
||||
MaterialInventoryOmsParamDO omsParam = new MaterialInventoryOmsParamDO();
|
||||
BeanUtils.copyProperties(restoreParam, omsParam);
|
||||
omsParam.setKctzType("出库反审");
|
||||
omsParam.setLoginUser(loginUser);
|
||||
omsServiceFeign.omsEdit(omsParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
//逻辑删除出库单明细
|
||||
@@ -2779,6 +2809,15 @@ public class StockOutOrderDomainService {
|
||||
materialDetail.setReverseFlag(1);
|
||||
outMaterialDetailService.updateById(materialDetail);
|
||||
}
|
||||
// 下单冻结模式:冻结记录归还业务单(未出库承接2→1;已出库已交接3→1重激活),
|
||||
// 业务单回 status=1 可编辑/再下发/取消,冻结语义闭环
|
||||
if (freezeMode) {
|
||||
if (shipped) {
|
||||
outOrderFreezeRecordDomainService.reactivateHandoverFreeze(outOrderNumber);
|
||||
} else {
|
||||
outOrderFreezeRecordDomainService.releaseExecutionUndertake(outOrderNumber);
|
||||
}
|
||||
}
|
||||
//逻辑删除出库单(用按单号查出的DB对象更新: 传入对象无主键, 直接updateById不生效, 与 cancelExecutionOrder 一致)
|
||||
stockOutOrderDB.setDelFlag(2);
|
||||
stockOutOrderDB.setReverseFlag(1);
|
||||
|
||||
+47
-2
@@ -6,6 +6,7 @@ import com.mhd.common.core.web.controller.BaseController;
|
||||
import com.mhd.common.core.web.domain.AjaxResult;
|
||||
import com.mhd.common.core.web.page.TableDataInfo;
|
||||
import com.mhd.system.api.domain.DeliveryDetailsLink;
|
||||
import com.mhd.system.api.domain.OmsFreezeInventoryParam;
|
||||
import com.mhd.wms.application.service.handoverTaskOrder.HandoverTaskOrderApplicationService;
|
||||
import com.mhd.wms.application.service.stockOutOrder.StockOutOrderApplicationService;
|
||||
import com.mhd.wms.domain.copyCodeReference.entity.CopyCodeReference;
|
||||
@@ -25,6 +26,7 @@ import com.mhd.wms.domain.stockOutOrder.repository.facade.IStockOutOrderService;
|
||||
import com.mhd.wms.domain.stockOutOrder.repository.listener.ExcelParseService;
|
||||
import com.mhd.wms.domain.stockOutOrder.repository.po.StockOutOrderPO;
|
||||
import com.mhd.wms.domain.stockOutOrder.repository.todo.StockOutOrderDO;
|
||||
import com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService;
|
||||
import com.mhd.wms.interfaces.assember.stockOutOrder.StockOutOrderAssembler;
|
||||
import com.mhd.wms.interfaces.dto.outMaterialDetail.OutMaterialDetailDTO;
|
||||
import com.mhd.wms.interfaces.dto.stockOutOrder.StockOutOrderDTO;
|
||||
@@ -58,6 +60,8 @@ import java.util.Map;
|
||||
public class StockOutOrderApi extends BaseController {
|
||||
@Autowired
|
||||
private StockOutOrderApplicationService stockOutOrderApplicationService;
|
||||
@Autowired
|
||||
private OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService;
|
||||
|
||||
@Resource
|
||||
private StockOutOrderAssembler stockOutOrderAssembler;
|
||||
@@ -561,8 +565,9 @@ public class StockOutOrderApi extends BaseController {
|
||||
*/
|
||||
@ApiOperation("oms下发前库存校验")
|
||||
@PostMapping(value = "/checkOmsAssignInventory")
|
||||
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails) {
|
||||
stockOutOrderApplicationService.checkOmsAssignInventory(outMaterialDetails);
|
||||
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails,
|
||||
@RequestParam(value = "businessOrderNumber", required = false) String businessOrderNumber) {
|
||||
stockOutOrderApplicationService.checkOmsAssignInventory(outMaterialDetails, businessOrderNumber);
|
||||
return AjaxResult.success("库存校验通过");
|
||||
}
|
||||
|
||||
@@ -584,6 +589,46 @@ public class StockOutOrderApi extends BaseController {
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* oms出库业务单下单冻结库存(防超卖): 按明细冻结WMS库存(可用-、冻结+,库存数量不变),
|
||||
* 条件更新防并发超卖,任一行可用不足整体失败;同业务单号幂等
|
||||
*/
|
||||
@ApiOperation("oms出库业务单下单冻结库存")
|
||||
@PostMapping(value = "/omsFreezeInventory")
|
||||
public AjaxResult omsFreezeInventory(@RequestBody OmsFreezeInventoryParam param) {
|
||||
return AjaxResult.success(outOrderFreezeRecordDomainService.omsFreezeInventory(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* oms出库业务单取消/删除-释放冻结库存: 可用+、冻结-(库存数量不变);按业务单号幂等;
|
||||
* 冻结已被执行单承接时拒绝(须先取消下发)
|
||||
*/
|
||||
@ApiOperation("oms出库业务单释放冻结库存")
|
||||
@PostMapping(value = "/omsUnfreezeInventory")
|
||||
public AjaxResult omsUnfreezeInventory(@RequestBody OmsFreezeInventoryParam param) {
|
||||
return AjaxResult.success(outOrderFreezeRecordDomainService.omsUnfreezeInventory(
|
||||
param.getOutOrderNumber(), param.getOperatorId(), param.getOperatorName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* oms出库业务单编辑原子重锁: 同一事务先释放原冻结再按新明细重新冻结,
|
||||
* 任一新明细可用不足整体回滚(旧冻结同样还原)
|
||||
*/
|
||||
@ApiOperation("oms出库业务单编辑重锁库存")
|
||||
@PostMapping(value = "/omsRelockInventory")
|
||||
public AjaxResult omsRelockInventory(@RequestBody OmsFreezeInventoryParam param) {
|
||||
return AjaxResult.success(outOrderFreezeRecordDomainService.omsRelockInventory(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓库是否存在活跃冻结记录(仓库"库存锁定模式"切换守卫:在途冻结单存在时禁止切换)
|
||||
*/
|
||||
@ApiOperation("仓库是否存在活跃冻结记录")
|
||||
@GetMapping(value = "/hasActiveFreezeByWarehouse/{warehouseId}")
|
||||
public AjaxResult hasActiveFreezeByWarehouse(@PathVariable("warehouseId") Long warehouseId) {
|
||||
return AjaxResult.success(outOrderFreezeRecordDomainService.hasActiveFreezeByWarehouse(warehouseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* oms下发单配送明细保存
|
||||
*/
|
||||
|
||||
@@ -1894,4 +1894,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
1 = 1
|
||||
AND MATERIAL_CLASSIFY_ID = #{id} AND DEL_FLAG = 1
|
||||
</select>
|
||||
|
||||
<!-- OMS下单冻结-条件扣减:可用-、冻结+,库存数量不变;可用不足时影响行数为0,
|
||||
由调用方抛异常回滚(防并发超卖,绝不能用 GREATEST 截断版) -->
|
||||
<update id="omsFreezeDeductAvailable">
|
||||
UPDATE material_inventory
|
||||
SET allocation_quantity = allocation_quantity - #{quantity},
|
||||
freeze_quantity = freeze_quantity + #{quantity},
|
||||
update_time = SYSDATE
|
||||
WHERE material_inventory_id = #{materialInventoryId}
|
||||
AND del_flag = 1
|
||||
AND allocation_quantity >= #{quantity}
|
||||
</update>
|
||||
|
||||
<!-- OMS下单解冻-条件释放:可用+、冻结-,库存数量不变;冻结不足时影响行数为0,
|
||||
由调用方抛异常回滚(防重复解冻/负数) -->
|
||||
<update id="omsReleaseFreezeToAvailable">
|
||||
UPDATE material_inventory
|
||||
SET allocation_quantity = allocation_quantity + #{quantity},
|
||||
freeze_quantity = freeze_quantity - #{quantity},
|
||||
update_time = SYSDATE
|
||||
WHERE material_inventory_id = #{materialInventoryId}
|
||||
AND del_flag = 1
|
||||
AND freeze_quantity >= #{quantity}
|
||||
</update>
|
||||
|
||||
<!-- OMS下单冻结单出库反审-还原为冻结:实物+、冻结+(可用不变)+重量体积面积还原,
|
||||
与交接扣减 ckjj(库存-冻结-)互逆;整行加回无条件(还原语义明确) -->
|
||||
<update id="omsRestoreInventoryToFrozen">
|
||||
UPDATE material_inventory
|
||||
SET inventory_quantity = inventory_quantity + #{quantity},
|
||||
freeze_quantity = freeze_quantity + #{quantity},
|
||||
net_weight = net_weight + #{netWeight},
|
||||
gross_weight = gross_weight + #{grossWeight},
|
||||
volume = volume + #{volume},
|
||||
area = area + #{area},
|
||||
update_time = SYSDATE
|
||||
WHERE material_inventory_id = #{materialInventoryId}
|
||||
AND del_flag = 1
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -0,0 +1,33 @@
|
||||
-- ============================================================
|
||||
-- 功能:仓库表新增"库存锁定模式"字段(OMS下单冻结库存/防超卖功能开关)
|
||||
-- 数据库:达梦(DM8),兼容 Oracle 语法,表名/字段名统一大写
|
||||
-- 模式:NGWL_TEST_SYSTEM(mhd-system 服务对应业务库;生产环境按实际模式名替换)
|
||||
-- 表名:WAREHOUSE
|
||||
-- 日期:2026-09-12
|
||||
-- 说明:
|
||||
-- 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存(可用-、冻结+,库存数量不变)
|
||||
-- 2-分配库存冻结:现有模式,WMS「分配库存」时才冻结(默认值,存量仓库行为零变化)
|
||||
-- 不变式:库存数量 = 可用数量 + 冻结数量(出库交接前全程成立)
|
||||
-- NULL 视为 2(代码侧 isOrderFreezeWarehouse 仅 =1 才走下单冻结,兜底降级安全)
|
||||
-- ============================================================
|
||||
|
||||
-- 0. 执行前检查(确认列是否已存在,已存在则跳过本脚本)
|
||||
-- SELECT OWNER, TABLE_NAME, COLUMN_NAME, DATA_TYPE
|
||||
-- FROM ALL_TAB_COLUMNS
|
||||
-- WHERE OWNER = 'NGWL_TEST_SYSTEM'
|
||||
-- AND TABLE_NAME = 'WAREHOUSE'
|
||||
-- AND COLUMN_NAME = 'STOCK_LOCK_MODE';
|
||||
|
||||
-- 1. 添加字段(达梦 ADD COLUMN 不支持 AFTER,列追加在表尾;带 DEFAULT 时存量行自动填默认值)
|
||||
ALTER TABLE NGWL_TEST_SYSTEM.WAREHOUSE ADD STOCK_LOCK_MODE TINYINT DEFAULT 2;
|
||||
|
||||
-- 2. 添加注释(达梦必须单独写)
|
||||
COMMENT ON COLUMN NGWL_TEST_SYSTEM.WAREHOUSE.STOCK_LOCK_MODE IS '库存锁定模式: 1-下单冻结 2-分配库存冻结';
|
||||
|
||||
-- 3. 验证
|
||||
-- SELECT WAREHOUSE_ID, WAREHOUSE_CODE, STOCK_LOCK_MODE FROM NGWL_TEST_SYSTEM.WAREHOUSE WHERE ROWNUM <= 10;
|
||||
|
||||
-- ----------------------------
|
||||
-- 回滚脚本(按需,须确认无 status∈(1,2) 的在途冻结单后再执行)
|
||||
-- ALTER TABLE NGWL_TEST_SYSTEM.WAREHOUSE DROP COLUMN STOCK_LOCK_MODE;
|
||||
-- ----------------------------
|
||||
@@ -0,0 +1,77 @@
|
||||
-- ============================================================
|
||||
-- 功能:WMS出库单库存冻结记录表(OMS下单冻结模式/防超卖)
|
||||
-- 数据库:达梦(DM8),兼容 Oracle 语法,表名/字段名统一大写
|
||||
-- 模式:NGWL_TEST_WMS(mhd-wms 服务对应业务库,与 MATERIAL_INVENTORY 同库;
|
||||
-- 生产环境按实际模式名替换)
|
||||
-- 表名:OUT_ORDER_FREEZE_RECORD
|
||||
-- 日期:2026-09-12
|
||||
-- 说明:
|
||||
-- 记录「业务单↔库存行↔冻结数量」占用关系与状态流转,是编辑重锁/取消解冻/
|
||||
-- 分配承接/交接标记/反审还原的对账依据与单据级隔离判据。
|
||||
-- 状态流转:1-业务单冻结(下单) → 2-执行单承接(WMS分配,数量不动)
|
||||
-- → 3-已交接(出库交接扣减库存后终态);任一环节取消 → 4-已释放;
|
||||
-- 反审(已出库) 3→1 重新激活。
|
||||
-- ============================================================
|
||||
|
||||
-- 0. 执行前检查(确认表是否已存在,已存在则跳过本脚本)
|
||||
-- SELECT OWNER, TABLE_NAME FROM ALL_TABLES
|
||||
-- WHERE OWNER = 'NGWL_TEST_WMS' AND TABLE_NAME = 'OUT_ORDER_FREEZE_RECORD';
|
||||
|
||||
-- 1. 建表
|
||||
CREATE TABLE OUT_ORDER_FREEZE_RECORD (
|
||||
FREEZE_RECORD_ID BIGINT IDENTITY(1,1) NOT NULL,
|
||||
BUSINESS_ORDER_NUMBER VARCHAR(50) DEFAULT NULL,
|
||||
EXECUTION_ORDER_NUMBER VARCHAR(50) DEFAULT NULL,
|
||||
MATERIAL_DETAIL_UNIQUE_ID VARCHAR(64) DEFAULT NULL,
|
||||
MATERIAL_INVENTORY_ID BIGINT NOT NULL,
|
||||
WAREHOUSE_ID BIGINT DEFAULT NULL,
|
||||
MATERIAL_BASE_INFO_ID BIGINT DEFAULT NULL,
|
||||
FREEZE_QUANTITY DECIMAL(24,6) NOT NULL,
|
||||
FREEZE_STATUS INT DEFAULT 1 NOT NULL,
|
||||
RELATE_NO VARCHAR(100) DEFAULT NULL,
|
||||
REMARK VARCHAR(500) DEFAULT NULL,
|
||||
CREATE_BY BIGINT DEFAULT NULL,
|
||||
CREATE_BY_NAME VARCHAR(50) DEFAULT NULL,
|
||||
CREATE_TIME DATETIME DEFAULT NULL,
|
||||
UPDATE_BY BIGINT DEFAULT NULL,
|
||||
UPDATE_BY_NAME VARCHAR(50) DEFAULT NULL,
|
||||
UPDATE_TIME DATETIME DEFAULT NULL,
|
||||
DEL_FLAG INT DEFAULT 1,
|
||||
CONSTRAINT PK_OUT_ORDER_FREEZE_RECORD PRIMARY KEY (FREEZE_RECORD_ID)
|
||||
);
|
||||
|
||||
-- 2. 表注释
|
||||
COMMENT ON TABLE OUT_ORDER_FREEZE_RECORD IS 'WMS出库单库存冻结记录表(OMS下单冻结模式)';
|
||||
|
||||
-- 3. 列注释(达梦必须单独写)
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.FREEZE_RECORD_ID IS '编号';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.BUSINESS_ORDER_NUMBER IS 'OMS出库业务单号';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.EXECUTION_ORDER_NUMBER IS '执行出库单号(分配承接时回填,可空)';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.MATERIAL_DETAIL_UNIQUE_ID IS '明细唯一标识(OMS明细uniqueId)';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.MATERIAL_INVENTORY_ID IS '库存行ID(MATERIAL_INVENTORY.MATERIAL_INVENTORY_ID)';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.WAREHOUSE_ID IS '仓库ID';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.MATERIAL_BASE_INFO_ID IS '物料基础信息ID';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.FREEZE_QUANTITY IS '冻结数量';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.FREEZE_STATUS IS '冻结状态: 1-业务单冻结 2-执行单承接 3-已交接 4-已释放';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.RELATE_NO IS '关联单号(业务单号#明细uniqueId,与库存调整记录RELATE_NO对应)';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.REMARK IS '备注';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.CREATE_BY IS '创建者';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.CREATE_BY_NAME IS '创建者姓名';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.CREATE_TIME IS '创建时间';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.UPDATE_BY IS '更新者';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.UPDATE_BY_NAME IS '更新人姓名';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.UPDATE_TIME IS '更新时间';
|
||||
COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.DEL_FLAG IS '删除标记:0-无状态,1-正常,2-已删除';
|
||||
|
||||
-- 4. 索引(最热路径:按业务单号取记录[幂等/解冻/重锁/承接校验];按执行单号[解除承接/交接标记/重激活];按库存行[分配/取消分配判据])
|
||||
CREATE INDEX IDX_OOFR_BIZ_ORDER ON OUT_ORDER_FREEZE_RECORD (BUSINESS_ORDER_NUMBER, DEL_FLAG);
|
||||
CREATE INDEX IDX_OOFR_EXEC_ORDER ON OUT_ORDER_FREEZE_RECORD (EXECUTION_ORDER_NUMBER);
|
||||
CREATE INDEX IDX_OOFR_INV_ID ON OUT_ORDER_FREEZE_RECORD (MATERIAL_INVENTORY_ID);
|
||||
|
||||
-- 5. 验证
|
||||
-- SELECT FREEZE_STATUS, COUNT(*) FROM OUT_ORDER_FREEZE_RECORD GROUP BY FREEZE_STATUS;
|
||||
|
||||
-- ----------------------------
|
||||
-- 回滚脚本(按需,须确认无在途冻结单[status∈(1,2)]后再执行)
|
||||
-- DROP TABLE OUT_ORDER_FREEZE_RECORD;
|
||||
-- ----------------------------
|
||||
@@ -0,0 +1,24 @@
|
||||
-- ============================================================
|
||||
-- 功能:修复冻结记录表主键列名(实体字段 freezeRecordId → 列 FREEZE_RECORD_ID,
|
||||
-- 建表脚本误写为 ID,导致 MyBatis-Plus 查询报"无效的列名[FREEZE_RECORD_ID]")
|
||||
-- 数据库:达梦(DM8)
|
||||
-- 模式:NGWL_TEST_WMS(mhd-wms 服务数据源;生产按实际模式名替换)
|
||||
-- 表名:OUT_ORDER_FREEZE_RECORD
|
||||
-- 日期:2026-09-12
|
||||
-- ============================================================
|
||||
|
||||
-- 0. 执行前检查(确认当前列名)
|
||||
-- SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
|
||||
-- WHERE OWNER = 'NGWL_TEST_WMS' AND TABLE_NAME = 'OUT_ORDER_FREEZE_RECORD'
|
||||
-- ORDER BY COLUMN_ID;
|
||||
|
||||
-- 1. 重命名主键列 ID → FREEZE_RECORD_ID(自增属性/主键约束随列保留)
|
||||
ALTER TABLE NGWL_TEST_WMS.OUT_ORDER_FREEZE_RECORD RENAME COLUMN ID TO FREEZE_RECORD_ID;
|
||||
|
||||
-- 2. 验证(应能看到 FREEZE_RECORD_ID 且不再有 ID 列)
|
||||
-- SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
|
||||
-- WHERE OWNER = 'NGWL_TEST_WMS' AND TABLE_NAME = 'OUT_ORDER_FREEZE_RECORD'
|
||||
-- ORDER BY COLUMN_ID;
|
||||
-- SELECT FREEZE_RECORD_ID, BUSINESS_ORDER_NUMBER, FREEZE_STATUS FROM NGWL_TEST_WMS.OUT_ORDER_FREEZE_RECORD;
|
||||
|
||||
-- 回滚: ALTER TABLE NGWL_TEST_WMS.OUT_ORDER_FREEZE_RECORD RENAME COLUMN FREEZE_RECORD_ID TO ID;
|
||||
Reference in New Issue
Block a user