feat:入库、移库功能页面完善

This commit is contained in:
zhouz
2026-07-31 09:57:09 +08:00
parent 04ffd865a3
commit 79017f74f1
21 changed files with 861 additions and 26 deletions

View File

@@ -21,6 +21,11 @@ public interface ErrorCodeConstants {
ErrorCode IOSTOR_INV_UPDATE_STATUS_INVALID = new ErrorCode(6_000_8, "只有生成状态的出库单允许修改"); ErrorCode IOSTOR_INV_UPDATE_STATUS_INVALID = new ErrorCode(6_000_8, "只有生成状态的出库单允许修改");
ErrorCode IOSTOR_INV_NO_PENDING_ALLOCATION = new ErrorCode(6_000_9, "当前明细没有生成状态的分配记录"); ErrorCode IOSTOR_INV_NO_PENDING_ALLOCATION = new ErrorCode(6_000_9, "当前明细没有生成状态的分配记录");
ErrorCode IOSTOR_INV_NO_MOVE_LOCATION = new ErrorCode(6_001_0, "没有可用于移库的空闲仓位"); ErrorCode IOSTOR_INV_NO_MOVE_LOCATION = new ErrorCode(6_001_0, "没有可用于移库的空闲仓位");
ErrorCode INBOUND_GROUP_PLATE_INVALID = new ErrorCode(6_001_1, "组盘数据不存在或不是生成状态");
ErrorCode INBOUND_STATUS_INVALID = new ErrorCode(6_001_2, "只有生成状态的入库单允许修改或分配");
ErrorCode INBOUND_TARGET_INVALID = new ErrorCode(6_001_3, "目标货位不可用或与所选库区、仓库不一致");
ErrorCode INBOUND_NOT_ALLOCATED = new ErrorCode(6_001_4, "入库单尚未完成货位分配");
ErrorCode INBOUND_ALLOCATION_TASK_CREATED = new ErrorCode(6_001_5, "入库任务已创建,不允许取消货位分配");
// ================ 移库单相关错误码 ================= // ================ 移库单相关错误码 =================
ErrorCode MOVE_INV_NOT_EXISTS = new ErrorCode(6_002_0, "移库单不存在"); ErrorCode MOVE_INV_NOT_EXISTS = new ErrorCode(6_002_0, "移库单不存在");

View File

@@ -46,6 +46,80 @@ public class IostorInvController {
return success(iostorInvService.createOutbound(reqVO)); return success(iostorInvService.createOutbound(reqVO));
} }
@PostMapping("/create-inbound")
@Operation(summary = "创建入库单及明细")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')")
public CommonResult<Long> createInbound(@Valid @RequestBody InboundSaveReqVO reqVO) {
return success(iostorInvService.createInbound(reqVO));
}
@PutMapping("/update-inbound")
@Operation(summary = "修改入库单及明细")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')")
public CommonResult<Boolean> updateInbound(@Valid @RequestBody InboundSaveReqVO reqVO) {
iostorInvService.updateInbound(reqVO);
return success(true);
}
@DeleteMapping("/delete-inbound")
@Operation(summary = "删除生成状态的入库单")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:delete')")
public CommonResult<Boolean> deleteInbound(@RequestParam("iostorinvId") Long iostorinvId) {
iostorInvService.deleteInbound(iostorinvId);
return success(true);
}
@GetMapping("/inbound-group-plates")
@Operation(summary = "查询生成状态的待入库组盘数据")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')")
public CommonResult<List<cn.code.nl.module.wms.dal.dataobject.groupplate.GroupPlateDO>>
getInboundGroupPlates(@RequestParam("storId") String storId) {
return success(iostorInvService.getInboundGroupPlates(storId));
}
@PostMapping("/allocate-inbound")
@Operation(summary = "分配入库货位并按库区类型创建任务")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')")
public CommonResult<Boolean> allocateInbound(@Valid @RequestBody InboundAllocationReqVO reqVO) {
iostorInvService.allocateInbound(reqVO);
return success(true);
}
@DeleteMapping("/cancel-inbound-allocation")
@Operation(summary = "取消单条入库货位分配并解锁货位")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')")
public CommonResult<Boolean> cancelInboundAllocation(
@RequestParam("iostorinvId") Long iostorinvId,
@RequestParam("iostorinvdtlId") Long iostorinvdtlId) {
iostorInvService.cancelInboundAllocation(iostorinvId, iostorinvdtlId);
return success(true);
}
@GetMapping("/inbound-available-structs")
@Operation(summary = "查询入库可用货位")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')")
public CommonResult<List<cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO>>
getInboundAvailableStructs(@RequestParam("storId") String storId,
@RequestParam("sectId") String sectId) {
return success(iostorInvService.getInboundAvailableStructs(storId, sectId));
}
@PostMapping("/set-inbound-point")
@Operation(summary = "设置入库起点并创建入库任务")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')")
public CommonResult<Boolean> setInboundPoint(@Valid @RequestBody InboundSetPointReqVO reqVO) {
iostorInvService.setInboundPoint(reqVO);
return success(true);
}
@PostMapping("/complete-inbound")
@Operation(summary = "完成入库并绑定木箱与仓位")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')")
public CommonResult<Boolean> completeInbound(@RequestParam("iostorinvId") Long iostorinvId) {
iostorInvService.completeInbound(iostorinvId);
return success(true);
}
@GetMapping("/availableInventoryPage") @GetMapping("/availableInventoryPage")
@Operation(summary = "获得可用库存分页") @Operation(summary = "获得可用库存分页")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')")

View File

@@ -15,6 +15,9 @@ import static cn.code.nl.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_
@Data @Data
public class IostorInvPageReqVO extends PageParam { public class IostorInvPageReqVO extends PageParam {
@Schema(description = "出入类型0入库1出库")
private String ioType;
@Schema(description = "单据编号") @Schema(description = "单据编号")
private String billCode; private String billCode;
@@ -22,7 +25,7 @@ public class IostorInvPageReqVO extends PageParam {
private String billType; private String billType;
@Schema(description = "业务日期") @Schema(description = "业务日期")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] bizDate; private LocalDateTime[] bizDate;
@Schema(description = "仓库标识", example = "12635") @Schema(description = "仓库标识", example = "12635")
@@ -86,4 +89,4 @@ public class IostorInvPageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private String[] uploadTime; private String[] uploadTime;
} }

View File

@@ -17,6 +17,9 @@ public class OutboundAllocationRespVO {
private String qtyUnitName; private String qtyUnitName;
private String sectId; private String sectId;
private String sectName; private String sectName;
private String structId;
private String structCode; private String structCode;
private String structName; private String structName;
private String pointCode;
private String taskId;
} }

View File

@@ -1,6 +1,7 @@
package cn.code.nl.module.wms.controller.admin.moveinv.vo; package cn.code.nl.module.wms.controller.admin.moveinv.vo;
import cn.code.nl.module.wms.dal.dataobject.moveinvdtl.MoveInvDtlDO; import cn.code.nl.module.wms.dal.dataobject.moveinvdtl.MoveInvDtlDO;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data; import lombok.Data;
import cn.code.nl.module.system.api.user.AdminUserApi; import cn.code.nl.module.system.api.user.AdminUserApi;
import org.dromara.core.trans.anno.Trans; import org.dromara.core.trans.anno.Trans;
@@ -17,6 +18,10 @@ import java.util.List;
@Data @Data
public class MoveInvRespVO implements VO { public class MoveInvRespVO implements VO {
/**
* EasyTrans 根据主键识别并缓存当前 VO缺少该注解会导致分页结果翻译失败。
*/
@TableId
private Long moveinvId; private Long moveinvId;
private String billCode; private String billCode;
private String billType; private String billType;

View File

@@ -60,6 +60,11 @@ public class IostorInvDO extends BaseDO {
* 仓库名称 * 仓库名称
*/ */
private String storName; private String storName;
/**
* 创建人姓名,仅用于列表返回,不对应数据库字段。
*/
@TableField(exist = false)
private String creatorName;
/** /**
* 来源方标识 * 来源方标识
*/ */

View File

@@ -19,6 +19,7 @@ public interface IostorInvMapper extends BaseMapperX<IostorInvDO> {
default PageResult<IostorInvDO> selectPage(IostorInvPageReqVO reqVO) { default PageResult<IostorInvDO> selectPage(IostorInvPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<IostorInvDO>() return selectPage(reqVO, new LambdaQueryWrapperX<IostorInvDO>()
.eqIfPresent(IostorInvDO::getIoType, reqVO.getIoType())
.eqIfPresent(IostorInvDO::getBillCode, reqVO.getBillCode()) .eqIfPresent(IostorInvDO::getBillCode, reqVO.getBillCode())
.eqIfPresent(IostorInvDO::getBillType, reqVO.getBillType()) .eqIfPresent(IostorInvDO::getBillType, reqVO.getBillType())
.betweenIfPresent(IostorInvDO::getBizDate, reqVO.getBizDate()) .betweenIfPresent(IostorInvDO::getBizDate, reqVO.getBizDate())
@@ -44,4 +45,4 @@ public interface IostorInvMapper extends BaseMapperX<IostorInvDO> {
.orderByDesc(IostorInvDO::getIostorinvId)); .orderByDesc(IostorInvDO::getIostorinvId));
} }
} }

View File

@@ -7,6 +7,7 @@ import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO; import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Delete;
import cn.code.nl.module.wms.controller.admin.iostorinvdis.vo.*; import cn.code.nl.module.wms.controller.admin.iostorinvdis.vo.*;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.OutboundAllocationRespVO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.OutboundAllocationRespVO;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -24,6 +25,18 @@ public interface IostorinvDisMapper extends BaseMapperX<IostorinvDisDO> {
@Param("iostorinvdtlId") Long iostorinvdtlId, @Param("iostorinvdtlId") Long iostorinvdtlId,
@Param("sectId") String sectId); @Param("sectId") String sectId);
@Delete("""
DELETE FROM wms_iostorinvdis
WHERE iostorinvdis_id = #{allocationId}
""")
int physicallyDeleteInboundAllocation(@Param("allocationId") String allocationId);
@Delete("""
DELETE FROM wms_iostorinvdis
WHERE iostorinv_id = #{iostorinvId}
""")
int physicallyDeleteByIostorinvId(@Param("iostorinvId") Long iostorinvId);
default PageResult<IostorinvDisDO> selectPage(IostorinvDisPageReqVO reqVO) { default PageResult<IostorinvDisDO> selectPage(IostorinvDisPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<IostorinvDisDO>() return selectPage(reqVO, new LambdaQueryWrapperX<IostorinvDisDO>()
.eqIfPresent(IostorinvDisDO::getIostorinvId, reqVO.getIostorinvId()) .eqIfPresent(IostorinvDisDO::getIostorinvId, reqVO.getIostorinvId())

View File

@@ -10,6 +10,7 @@ import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Delete;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryPageReqVO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryPageReqVO;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvDetailRespVO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvDetailRespVO;
@@ -24,6 +25,12 @@ import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*;
@Mapper @Mapper
public interface IostorinvDtlMapper extends BaseMapperX<IostorinvDtlDO> { public interface IostorinvDtlMapper extends BaseMapperX<IostorinvDtlDO> {
@Delete("""
DELETE FROM wms_iostorinvdtl
WHERE iostorinv_id = #{iostorinvId}
""")
int physicallyDeleteByIostorinvId(@Param("iostorinvId") Long iostorinvId);
/** /**
* 分页查询指定仓库的可用库存。 * 分页查询指定仓库的可用库存。
*/ */

View File

@@ -83,6 +83,68 @@ public interface StrucAttrMapper extends BaseMapperX<StrucAttrDO> {
int lockForTask(@Param("structId") String structId, int lockForTask(@Param("structId") String structId,
@Param("taskCode") String taskCode); @Param("taskCode") String taskCode);
@Update("""
UPDATE wms_structattr
SET lock_type = '2',
inv_type = '0',
inv_id = #{invId},
inv_code = #{invCode}
WHERE struct_id = #{structId}
AND deleted = '0'
AND is_used = 1
AND lock_type = '1'
AND (storagevehicle_code IS NULL OR storagevehicle_code = '')
""")
int lockForInbound(@Param("structId") String structId,
@Param("invId") String invId,
@Param("invCode") String invCode);
@Update("""
UPDATE wms_structattr
SET storagevehicle_code = #{vehicleCode},
storagevehicle_qty = 1,
lock_type = '1',
inv_type = NULL,
inv_id = NULL,
inv_code = NULL,
task_code = NULL
WHERE struct_id = #{structId}
AND deleted = '0'
AND lock_type = '2'
AND inv_id = #{invId}
""")
int completeInbound(@Param("structId") String structId,
@Param("invId") String invId,
@Param("vehicleCode") String vehicleCode);
@Update("""
UPDATE wms_structattr
SET lock_type = '1',
inv_type = NULL,
inv_id = NULL,
inv_code = NULL,
task_code = NULL
WHERE deleted = '0'
AND inv_id = #{invId}
AND lock_type = '2'
""")
int unlockInbound(@Param("invId") String invId);
@Update("""
UPDATE wms_structattr
SET lock_type = '1',
inv_type = NULL,
inv_id = NULL,
inv_code = NULL,
task_code = NULL
WHERE struct_id = #{structId}
AND deleted = '0'
AND inv_id = #{invId}
AND lock_type = '2'
""")
int unlockInboundStruct(@Param("structId") String structId,
@Param("invId") String invId);
default PageResult<StrucAttrDO> selectPage(StrucAttrPageReqVO reqVO) { default PageResult<StrucAttrDO> selectPage(StrucAttrPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<StrucAttrDO>() return selectPage(reqVO, new LambdaQueryWrapperX<StrucAttrDO>()
.eqIfPresent(StrucAttrDO::getStructCode, reqVO.getStructCode()) .eqIfPresent(StrucAttrDO::getStructCode, reqVO.getStructCode())

View File

@@ -2,6 +2,7 @@ package cn.code.nl.module.wms.framework.rpc.config;
import cn.code.nl.module.base.api.codegen.CodeGenApi; import cn.code.nl.module.base.api.codegen.CodeGenApi;
import cn.code.nl.module.infra.api.config.ConfigApi; import cn.code.nl.module.infra.api.config.ConfigApi;
import cn.code.nl.module.system.api.user.AdminUserApi;
import cn.code.nl.module.task.api.TransportTaskApi; import cn.code.nl.module.task.api.TransportTaskApi;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -12,6 +13,6 @@ import org.springframework.context.annotation.Configuration;
* @Date: 2026/7/22 13:50 * @Date: 2026/7/22 13:50
*/ */
@Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false) @Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false)
@EnableFeignClients(clients = {CodeGenApi.class, TransportTaskApi.class, ConfigApi.class}) @EnableFeignClients(clients = {CodeGenApi.class, TransportTaskApi.class, ConfigApi.class, AdminUserApi.class})
public class RpcConfiguration { public class RpcConfiguration {
} }

View File

@@ -23,6 +23,25 @@ public interface IostorInvService {
*/ */
Long createOutbound(@Valid IostorInvCreateReqVO reqVO); Long createOutbound(@Valid IostorInvCreateReqVO reqVO);
Long createInbound(@Valid InboundSaveReqVO reqVO);
void updateInbound(@Valid InboundSaveReqVO reqVO);
void deleteInbound(Long iostorinvId);
List<cn.code.nl.module.wms.dal.dataobject.groupplate.GroupPlateDO> getInboundGroupPlates(String storId);
void allocateInbound(@Valid InboundAllocationReqVO reqVO);
void cancelInboundAllocation(Long iostorinvId, Long iostorinvdtlId);
List<cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO>
getInboundAvailableStructs(String storId, String sectId);
void setInboundPoint(@Valid InboundSetPointReqVO reqVO);
void completeInbound(Long iostorinvId);
/** /**
* 获得指定仓库的可用库存分页。 * 获得指定仓库的可用库存分页。
* *

View File

@@ -8,8 +8,14 @@ import cn.code.nl.module.base.api.codegen.CodeGenApi;
import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO;
import cn.code.nl.module.task.api.TransportTaskApi; import cn.code.nl.module.task.api.TransportTaskApi;
import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO;
import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateReqDTO;
import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateRespDTO;
import cn.code.nl.module.system.api.user.AdminUserApi;
import cn.code.nl.module.system.api.user.dto.AdminUserRespDTO;
import cn.code.nl.module.wms.dal.dataobject.bsrealstorattr.BsrealStorAttrDO; import cn.code.nl.module.wms.dal.dataobject.bsrealstorattr.BsrealStorAttrDO;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO;
import cn.code.nl.module.wms.dal.dataobject.moveinvdtl.MoveInvDtlDO;
import cn.code.nl.module.wms.service.moveinv.MoveInvService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -18,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.*; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.*;
import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO; import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO;
import cn.code.nl.framework.common.pojo.PageResult; import cn.code.nl.framework.common.pojo.PageResult;
@@ -30,9 +37,11 @@ import cn.code.nl.module.wms.dal.mysql.bsrealstorattr.BsrealStorAttrMapper;
import cn.code.nl.module.wms.dal.mysql.groupplate.GroupPlateMapper; import cn.code.nl.module.wms.dal.mysql.groupplate.GroupPlateMapper;
import cn.code.nl.module.wms.dal.mysql.iostorinvdis.IostorinvDisMapper; import cn.code.nl.module.wms.dal.mysql.iostorinvdis.IostorinvDisMapper;
import cn.code.nl.module.wms.dal.mysql.structAttr.StrucAttrMapper; import cn.code.nl.module.wms.dal.mysql.structAttr.StrucAttrMapper;
import cn.code.nl.module.wms.dal.mysql.sectattr.SectAttrMapper;
import cn.code.nl.module.wms.dal.dataobject.groupplate.GroupPlateDO; import cn.code.nl.module.wms.dal.dataobject.groupplate.GroupPlateDO;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO; import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO;
import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO;
import cn.code.nl.module.wms.dal.dataobject.sectattr.SectAttrDO;
import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -74,10 +83,453 @@ public class IostorInvServiceImpl implements IostorInvService {
@Resource @Resource
private StrucAttrMapper strucAttrMapper; private StrucAttrMapper strucAttrMapper;
@Resource
private SectAttrMapper sectAttrMapper;
@Resource @Resource
private TransportTaskApi transportTaskApi; private TransportTaskApi transportTaskApi;
@Resource
private MoveInvService moveInvService;
@Resource
private AdminUserApi adminUserApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createInbound(InboundSaveReqVO reqVO) {
List<GroupPlateDO> groups = validateInboundGroups(reqVO.getGroupIds(), null);
IostorInvDO header = new IostorInvDO();
header.setBillCode(generateIoBillCode("createInbound"));
header.setIoType("0");
header.setBillType(reqVO.getBillType());
header.setBizDate(reqVO.getBizDate());
header.setStorId(reqVO.getStorId());
header.setBillStatus("10");
header.setCreateMode("01");
header.setRemark(reqVO.getRemark());
header.setDetailCount(groups.size());
header.setTotalQty(groups.stream().map(GroupPlateDO::getQty)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
header.setTotalWeight(header.getTotalQty());
iostorInvMapper.insert(header);
insertInboundDetails(header.getIostorinvId(), groups);
insertInboundAllocationPlaceholders(header, groups);
groups.forEach(group -> updateGroupStatus(group.getGroupId(), "01"));
return header.getIostorinvId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateInbound(InboundSaveReqVO reqVO) {
IostorInvDO header = requireInbound(reqVO.getIostorinvId(), "10");
List<IostorinvDtlDO> oldDetails = iostorinvDtlMapper.selectList(
new LambdaQueryWrapperX<IostorinvDtlDO>()
.eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId()));
Set<Long> oldGroupIds = oldDetails.stream().map(IostorinvDtlDO::getSourceBilldtlId)
.filter(StrUtil::isNotBlank).map(Long::valueOf).collect(Collectors.toSet());
List<GroupPlateDO> groups = validateInboundGroups(reqVO.getGroupIds(), oldGroupIds);
oldGroupIds.forEach(id -> updateGroupStatus(id, "00"));
strucAttrMapper.unlockInbound(String.valueOf(header.getIostorinvId()));
iostorinvDisMapper.delete(new LambdaQueryWrapperX<IostorinvDisDO>()
.eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId())));
iostorinvDtlMapper.delete(new LambdaQueryWrapperX<IostorinvDtlDO>()
.eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId()));
IostorInvDO update = new IostorInvDO();
update.setIostorinvId(header.getIostorinvId());
update.setBillType(reqVO.getBillType());
update.setBizDate(reqVO.getBizDate());
update.setStorId(reqVO.getStorId());
update.setRemark(reqVO.getRemark());
update.setDetailCount(groups.size());
update.setTotalQty(groups.stream().map(GroupPlateDO::getQty)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
update.setTotalWeight(update.getTotalQty());
iostorInvMapper.updateById(update);
insertInboundDetails(header.getIostorinvId(), groups);
insertInboundAllocationPlaceholders(header, groups);
groups.forEach(group -> updateGroupStatus(group.getGroupId(), "01"));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteInbound(Long iostorinvId) {
IostorInvDO header = requireInbound(iostorinvId, "10");
List<IostorinvDtlDO> details = iostorinvDtlMapper.selectList(
new LambdaQueryWrapperX<IostorinvDtlDO>()
.eq(IostorinvDtlDO::getIostorinvId, iostorinvId));
// 入库单删除后,原组盘数据恢复为生成状态,允许再次选择建单。
details.stream()
.map(IostorinvDtlDO::getSourceBilldtlId)
.filter(StrUtil::isNotBlank)
.map(Long::valueOf)
.forEach(groupId -> updateGroupStatus(groupId, "00"));
// 分配表、明细表按业务要求物理删除,避免留下孤立记录。
iostorinvDisMapper.physicallyDeleteByIostorinvId(iostorinvId);
iostorinvDtlMapper.physicallyDeleteByIostorinvId(iostorinvId);
// 主表沿用 MyBatis-Plus 逻辑删除,仅更新 deleted 字段。
iostorInvMapper.deleteById(header.getIostorinvId());
}
@Override
public List<GroupPlateDO> getInboundGroupPlates(String storId) {
// 生成状态的组盘数据尚未进入仓库,因此此处不按 storId 过滤。
return groupPlateMapper.selectList(new LambdaQueryWrapperX<GroupPlateDO>()
.eq(GroupPlateDO::getStatus, "00")
.orderByDesc(GroupPlateDO::getGroupId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void allocateInbound(InboundAllocationReqVO reqVO) {
IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId());
if (header == null || !"0".equals(header.getIoType())
|| !Set.of("10", "20").contains(header.getBillStatus())) {
throw exception(INBOUND_STATUS_INVALID);
}
for (InboundAllocationReqVO.Item item : reqVO.getItems()) {
IostorinvDtlDO detail = iostorinvDtlMapper.selectById(item.getIostorinvdtlId());
SectAttrDO sect = sectAttrMapper.selectById(item.getSectId());
if (detail == null || !Objects.equals(detail.getIostorinvId(), header.getIostorinvId())
|| sect == null || !Objects.equals(header.getStorId(), sect.getStorId())) {
throw exception(INBOUND_TARGET_INVALID);
}
StrucAttrDO target;
if (Boolean.TRUE.equals(reqVO.getAutoAllocate())) {
target = getInboundAvailableStructs(header.getStorId(), item.getSectId()).stream()
.findFirst().orElseThrow(() -> exception(INBOUND_TARGET_INVALID));
} else {
if (StrUtil.isBlank(item.getStructId())) {
throw exception(INBOUND_TARGET_INVALID);
}
target = strucAttrMapper.selectById(item.getStructId());
}
if (target == null
|| !Objects.equals(header.getStorId(), target.getStorId())
|| !Objects.equals(item.getSectId(), target.getSectId())
|| !"1".equals(target.getLockType())
|| StrUtil.isNotBlank(target.getStoragevehicleCode())) {
throw exception(INBOUND_TARGET_INVALID);
}
if (strucAttrMapper.lockForInbound(target.getStructId(),
String.valueOf(header.getIostorinvId()), header.getBillCode()) != 1) {
throw exception(INBOUND_TARGET_INVALID);
}
IostorinvDisDO allocation = iostorinvDisMapper.selectOne(
new LambdaQueryWrapperX<IostorinvDisDO>()
.eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId()))
.eq(IostorinvDisDO::getIostorinvdtlId,
String.valueOf(detail.getIostorinvdtlId())));
if (allocation == null || StrUtil.isNotBlank(allocation.getStructId())) {
throw exception(INBOUND_TARGET_INVALID);
}
allocation.setSectId(sect.getSectId());
allocation.setSectCode(sect.getSectCode());
allocation.setSectName(sect.getSectName());
allocation.setStructId(target.getStructId());
allocation.setStructCode(target.getStructCode());
allocation.setStructName(target.getStructName());
iostorinvDisMapper.updateById(allocation);
detail.setAssignQty(detail.getPlanQty());
detail.setUnassignQty(BigDecimal.ZERO);
detail.setBillStatus("30");
iostorinvDtlMapper.updateById(detail);
}
refreshInboundAllocationStatus(header.getIostorinvId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelInboundAllocation(Long iostorinvId, Long iostorinvdtlId) {
IostorInvDO header = iostorInvMapper.selectById(iostorinvId);
if (header == null || !"0".equals(header.getIoType())
|| !Set.of("20", "30").contains(header.getBillStatus())) {
throw exception(INBOUND_STATUS_INVALID);
}
IostorinvDtlDO detail = iostorinvDtlMapper.selectById(iostorinvdtlId);
IostorinvDisDO allocation = iostorinvDisMapper.selectOne(
new LambdaQueryWrapperX<IostorinvDisDO>()
.eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId))
.eq(IostorinvDisDO::getIostorinvdtlId, String.valueOf(iostorinvdtlId)));
if (detail == null || !Objects.equals(detail.getIostorinvId(), iostorinvId)
|| allocation == null || StrUtil.isBlank(allocation.getStructId())) {
throw exception(INBOUND_NOT_ALLOCATED);
}
// 任务一旦创建并下发,不能只释放货位;需走后续的任务撤销流程。
if (StrUtil.isNotBlank(allocation.getTaskId())) {
throw exception(INBOUND_ALLOCATION_TASK_CREATED);
}
if (strucAttrMapper.unlockInboundStruct(allocation.getStructId(),
String.valueOf(iostorinvId)) != 1) {
throw exception(INBOUND_TARGET_INVALID);
}
// 取消分配要求直接物理删除分配记录,不保留逻辑删除数据。
iostorinvDisMapper.physicallyDeleteInboundAllocation(allocation.getIostorinvdisId());
detail.setAssignQty(BigDecimal.ZERO);
detail.setUnassignQty(detail.getPlanQty());
detail.setBillStatus("10");
iostorinvDtlMapper.updateById(detail);
refreshInboundAllocationStatus(iostorinvId);
}
@Override
public List<StrucAttrDO> getInboundAvailableStructs(String storId, String sectId) {
return strucAttrMapper.selectList(new LambdaQueryWrapperX<StrucAttrDO>()
.eq(StrucAttrDO::getStorId, storId)
.eq(StrucAttrDO::getSectId, sectId)
.eq(StrucAttrDO::getIsUsed, true)
.eq(StrucAttrDO::getLockType, "1")
.and(wrapper -> wrapper.isNull(StrucAttrDO::getStoragevehicleCode)
.or().eq(StrucAttrDO::getStoragevehicleCode, ""))
.orderByAsc(StrucAttrDO::getRowNum, StrucAttrDO::getColNum,
StrucAttrDO::getLayerNum, StrucAttrDO::getStructId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void setInboundPoint(InboundSetPointReqVO reqVO) {
IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId());
if (header == null || !"0".equals(header.getIoType())
|| !Set.of("10", "20", "30").contains(header.getBillStatus())) {
throw exception(INBOUND_STATUS_INVALID);
}
IostorinvDisDO allocation = iostorinvDisMapper.selectOne(
new LambdaQueryWrapperX<IostorinvDisDO>()
.eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId()))
.eq(IostorinvDisDO::getIostorinvdtlId,
String.valueOf(reqVO.getIostorinvdtlId())));
if (allocation == null || StrUtil.isBlank(allocation.getStructId())) {
throw exception(INBOUND_NOT_ALLOCATED);
}
if (StrUtil.isNotBlank(allocation.getTaskId())) {
throw exception(INBOUND_ALLOCATION_TASK_CREATED);
}
allocation.setPointCode(reqVO.getPointCode());
SectAttrDO sect = sectAttrMapper.selectById(allocation.getSectId());
if (sect != null && "主存区".equals(sect.getSectTypeAttr())) {
IostorinvDtlDO detail = iostorinvDtlMapper.selectById(reqVO.getIostorinvdtlId());
GroupPlateDO group = groupPlateMapper.selectById(Long.valueOf(detail.getSourceBilldtlId()));
Long taskId = createAndIssueInboundTask(reqVO.getPointCode(), allocation.getStructCode(),
group, header.getIostorinvId(), detail.getSeqNo(), allocation.getIostorinvdisId());
allocation.setTaskId(String.valueOf(taskId));
allocation.setIsIssued("1");
allocation.setWorkStatus("20");
}
iostorinvDisMapper.updateById(allocation);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void completeInbound(Long iostorinvId) {
IostorInvDO header = requireInbound(iostorinvId, "30");
List<IostorinvDisDO> allocations = iostorinvDisMapper.selectList(
new LambdaQueryWrapperX<IostorinvDisDO>()
.eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId)));
if (allocations.isEmpty()) {
throw exception(INBOUND_NOT_ALLOCATED);
}
for (IostorinvDisDO allocation : allocations) {
if (strucAttrMapper.completeInbound(allocation.getStructId(),
String.valueOf(iostorinvId), allocation.getStoragevehicleCode()) != 1) {
throw exception(INBOUND_TARGET_INVALID);
}
IostorinvDtlDO detail = iostorinvDtlMapper.selectById(
Long.valueOf(allocation.getIostorinvdtlId()));
updateGroupStatus(Long.valueOf(detail.getSourceBilldtlId()), "02");
allocation.setWorkStatus("99");
allocation.setRealQty(allocation.getPlanQty());
iostorinvDisMapper.updateById(allocation);
detail.setBillStatus("99");
detail.setRealQty(detail.getPlanQty());
iostorinvDtlMapper.updateById(detail);
}
IostorInvDO update = new IostorInvDO();
update.setIostorinvId(iostorinvId);
update.setBillStatus("99");
update.setConfirmTime(java.time.LocalDateTime.now());
iostorInvMapper.updateById(update);
}
private List<GroupPlateDO> validateInboundGroups(List<Long> groupIds, Set<Long> editableIds) {
Set<Long> ids = new LinkedHashSet<>(groupIds);
if (ids.size() != groupIds.size()) {
throw exception(INBOUND_GROUP_PLATE_INVALID);
}
List<GroupPlateDO> groups = groupPlateMapper.selectBatchIds(ids);
if (groups.size() != ids.size() || groups.stream().anyMatch(group ->
!("00".equals(group.getStatus())
|| editableIds != null && editableIds.contains(group.getGroupId())))) {
throw exception(INBOUND_GROUP_PLATE_INVALID);
}
return groups;
}
private void insertInboundDetails(Long headerId, List<GroupPlateDO> groups) {
int seq = 1;
for (GroupPlateDO group : groups) {
IostorinvDtlDO detail = new IostorinvDtlDO();
detail.setIostorinvId(headerId);
detail.setSeqNo(seq++);
detail.setMaterialId(group.getMaterialId());
detail.setMaterialCode(group.getMaterialCode());
detail.setPcsn(group.getPcsn());
detail.setQtyUnitId(StrUtil.blankToDefault(group.getQtyUnitId(), "0"));
detail.setPlanQty(group.getQty());
detail.setAssignQty(BigDecimal.ZERO);
detail.setUnassignQty(group.getQty());
detail.setBillStatus("10");
detail.setSourceBillCode(group.getExtCode());
detail.setSourceBillType(group.getExtType());
detail.setSourceBilldtlId(String.valueOf(group.getGroupId()));
// 入库明细表没有独立载具字段,使用来源装载点保存木箱号。
detail.setSourceLoadPort(group.getVehicleCode());
iostorinvDtlMapper.insert(detail);
}
}
/**
* 入库单创建时即生成分配占位记录。此时没有库区、货位和起点,状态统一为生成。
*/
private void insertInboundAllocationPlaceholders(IostorInvDO header, List<GroupPlateDO> groups) {
List<IostorinvDtlDO> details = iostorinvDtlMapper.selectList(
new LambdaQueryWrapperX<IostorinvDtlDO>()
.eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId())
.orderByAsc(IostorinvDtlDO::getSeqNo));
Map<String, GroupPlateDO> groupMap = groups.stream().collect(
Collectors.toMap(group -> String.valueOf(group.getGroupId()), group -> group));
for (IostorinvDtlDO detail : details) {
insertInboundAllocationPlaceholder(header, detail, groupMap.get(detail.getSourceBilldtlId()));
}
}
private void insertInboundAllocationPlaceholder(IostorInvDO header, IostorinvDtlDO detail) {
GroupPlateDO group = StrUtil.isBlank(detail.getSourceBilldtlId())
? null : groupPlateMapper.selectById(Long.valueOf(detail.getSourceBilldtlId()));
insertInboundAllocationPlaceholder(header, detail, group);
}
private void insertInboundAllocationPlaceholder(IostorInvDO header, IostorinvDtlDO detail,
GroupPlateDO group) {
IostorinvDisDO row = new IostorinvDisDO();
row.setIostorinvdisId(IdUtil.getSnowflakeNextIdStr());
row.setIostorinvId(String.valueOf(header.getIostorinvId()));
row.setIostorinvdtlId(String.valueOf(detail.getIostorinvdtlId()));
row.setSeqNo(String.valueOf(detail.getSeqNo()));
row.setMaterialId(detail.getMaterialId());
row.setMaterialCode(detail.getMaterialCode());
row.setPcsn(StrUtil.blankToDefault(detail.getPcsn(), ""));
row.setWorkStatus("10");
row.setStoragevehicleCode(group == null ? detail.getSourceLoadPort() : group.getVehicleCode());
row.setIsIssued("0");
row.setQtyUnitId(detail.getQtyUnitId());
row.setQtyUnitName(group == null ? "" : StrUtil.blankToDefault(group.getQtyUnitName(), ""));
row.setPlanQty(detail.getPlanQty());
row.setRealQty(BigDecimal.ZERO);
row.setHandType(false);
iostorinvDisMapper.insert(row);
}
private void refreshInboundAllocationStatus(Long iostorinvId) {
List<IostorinvDisDO> allocations = iostorinvDisMapper.selectList(
new LambdaQueryWrapperX<IostorinvDisDO>()
.eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId)));
boolean allAllocated = !allocations.isEmpty()
&& allocations.stream().allMatch(item -> StrUtil.isNotBlank(item.getStructId()));
IostorInvDO update = new IostorInvDO();
update.setIostorinvId(iostorinvId);
update.setBillStatus(allAllocated ? "30" : "20");
update.setDisTime(java.time.LocalDateTime.now());
iostorInvMapper.updateById(update);
}
private IostorInvDO requireInbound(Long id, String status) {
IostorInvDO header = iostorInvMapper.selectById(id);
if (header == null || !"0".equals(header.getIoType())) {
throw exception(IOSTOR_INV_NOT_EXISTS);
}
if (!status.equals(header.getBillStatus())) {
throw exception(INBOUND_STATUS_INVALID);
}
return header;
}
private void updateGroupStatus(Long groupId, String status) {
GroupPlateDO update = new GroupPlateDO();
update.setGroupId(groupId);
update.setStatus(status);
groupPlateMapper.updateById(update);
}
private String generateIoBillCode(String scene) {
try {
CommonResult<String> result = codeGenApi.generate(
new CodeGenerateReqDTO().setRuleCode("IO_CODE"));
if (result != null && result.isSuccess() && StrUtil.isNotBlank(result.getData())) {
return result.getData();
}
} catch (RuntimeException ex) {
log.error("[{}][生成入库单号异常]", scene, ex);
}
throw exception(IOSTOR_INV_CODE_GENERATE_FAILED);
}
private IostorinvDisDO buildInboundAllocation(IostorInvDO header, IostorinvDtlDO detail,
GroupPlateDO group, SectAttrDO sect,
StrucAttrDO target, String pointCode) {
IostorinvDisDO row = new IostorinvDisDO();
row.setIostorinvdisId(IdUtil.getSnowflakeNextIdStr());
row.setIostorinvId(String.valueOf(header.getIostorinvId()));
row.setIostorinvdtlId(String.valueOf(detail.getIostorinvdtlId()));
row.setSeqNo(String.valueOf(detail.getSeqNo()));
row.setSectId(sect.getSectId());
row.setSectCode(sect.getSectCode());
row.setSectName(sect.getSectName());
row.setStructId(target.getStructId());
row.setStructCode(target.getStructCode());
row.setStructName(target.getStructName());
row.setMaterialId(detail.getMaterialId());
row.setMaterialCode(detail.getMaterialCode());
row.setPcsn(StrUtil.blankToDefault(detail.getPcsn(), ""));
row.setWorkStatus("10");
row.setStoragevehicleCode(group.getVehicleCode());
row.setIsIssued("0");
row.setQtyUnitId(detail.getQtyUnitId());
row.setQtyUnitName(StrUtil.blankToDefault(group.getQtyUnitName(), ""));
row.setPlanQty(detail.getPlanQty());
row.setRealQty(BigDecimal.ZERO);
row.setPointCode(pointCode);
row.setHandType(false);
return row;
}
private Long createAndIssueInboundTask(String sourcePoint, String targetPoint,
GroupPlateDO group, long taskGroupId,
long sortSeq, String bizId) {
TransportTaskCreateReqDTO task = new TransportTaskCreateReqDTO();
task.setTaskName("入库任务");
task.setOwnerService("WMS");
task.setBizType("INBOUND");
task.setBizId(bizId);
task.setHandleCode("INTASK");
task.setTaskType("010501");
task.setAcsTaskType("7");
task.setAgvSystemType("1");
task.setPointCode1(sourcePoint);
task.setPointCode2(targetPoint);
task.setVehicleCode(group.getVehicleCode());
if (StrUtil.isNotBlank(group.getMaterialId())) {
task.setMaterialId(Long.valueOf(group.getMaterialId()));
}
task.setMaterialCode(group.getMaterialCode());
task.setTaskGroupId(taskGroupId);
task.setSortSeq(sortSeq);
task.setIsAutoIssue("0");
task.setProductArea("LK");
task.setIsCreateFinish(true);
Long taskId = transportTaskApi.createTransportTask(task).getCheckedData();
transportTaskApi.issueTransportTaskRollBack(taskId).getCheckedData();
return taskId;
}
/** /**
* 创建出库单及其明细。 * 创建出库单及其明细。
*/ */
@@ -300,9 +752,24 @@ public class IostorInvServiceImpl implements IostorInvService {
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public List<OutboundAllocationRespVO> getOutboundAllocations( public List<OutboundAllocationRespVO> getOutboundAllocations(
Long iostorinvId, Long iostorinvdtlId, String sectId) { Long iostorinvId, Long iostorinvdtlId, String sectId) {
return iostorinvDisMapper.selectAllocationList(iostorinvId, iostorinvdtlId, sectId); List<OutboundAllocationRespVO> result =
iostorinvDisMapper.selectAllocationList(iostorinvId, iostorinvdtlId, sectId);
// 兼容入库功能上线前已经创建的单据:旧数据没有同步生成分配占位记录,
// 首次点击明细查询时自动补建,避免页面无法继续分配货位。
if (result.isEmpty() && iostorinvdtlId != null) {
IostorInvDO header = iostorInvMapper.selectById(iostorinvId);
IostorinvDtlDO detail = iostorinvDtlMapper.selectById(iostorinvdtlId);
if (header != null && "0".equals(header.getIoType()) && detail != null
&& Objects.equals(detail.getIostorinvId(), iostorinvId)) {
insertInboundAllocationPlaceholder(header, detail);
result = iostorinvDisMapper.selectAllocationList(
iostorinvId, iostorinvdtlId, sectId);
}
}
return result;
} }
@Override @Override
@@ -413,6 +880,7 @@ public class IostorInvServiceImpl implements IostorInvService {
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void oneClickSet(OutboundOneClickSetReqVO reqVO) { public void oneClickSet(OutboundOneClickSetReqVO reqVO) {
// 1. 校验出库单和当前选中明细,避免为其他单据的明细生成任务。 // 1. 校验出库单和当前选中明细,避免为其他单据的明细生成任务。
IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId()); IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId());
@@ -449,9 +917,7 @@ public class IostorInvServiceImpl implements IostorInvService {
Map<String, List<StrucAttrDO>> targetGroups = targetStructs.stream() Map<String, List<StrucAttrDO>> targetGroups = targetStructs.stream()
.collect(java.util.stream.Collectors.groupingBy(this::placementGroupKey, .collect(java.util.stream.Collectors.groupingBy(this::placementGroupKey,
LinkedHashMap::new, java.util.stream.Collectors.toList())); LinkedHashMap::new, java.util.stream.Collectors.toList()));
// 6. 预先准备移库空位、两个出库点当前负载,以及本批任务共用的任务组和执行序号。 // 6. 预先查询两个出库点当前负载,并生成本批任务共用的任务组和执行序号。
List<StrucAttrDO> freeMoveLocations = selectFreeMoveLocations(header.getStorId(), targetStructIds);
Iterator<StrucAttrDO> freeLocationIterator = freeMoveLocations.iterator();
long[] destinationLoads = loadDestinationCounts(); long[] destinationLoads = loadDestinationCounts();
long taskGroupId = IdUtil.getSnowflakeNextId(); long taskGroupId = IdUtil.getSnowflakeNextId();
long sortSeq = 0; long sortSeq = 0;
@@ -484,31 +950,36 @@ public class IostorInvServiceImpl implements IostorInvService {
.findFirst().orElseThrow(); .findFirst().orElseThrow();
} }
if ("MOVE".equals(action.type())) { if ("MOVE".equals(action.type())) {
// 9. 挡路箱创建移库任务,并锁定起点、目的仓位,防止规划后被其他任务占用 // 9. 先按挡路载具自动创建移库单,由移库单统一选择同库区同层目标仓位并加移出、移入锁
if (!freeLocationIterator.hasNext()) { MoveInvAutoCreateReqDTO moveReq = new MoveInvAutoCreateReqDTO();
throw exception(IOSTOR_INV_NO_MOVE_LOCATION); moveReq.setVehicleCodes(List.of(action.vehicleCode()));
} MoveInvAutoCreateRespDTO moveResult = moveInvService.autoCreate(moveReq);
StrucAttrDO destination = freeLocationIterator.next(); MoveInvDtlDO moveDetail = moveInvService.get(moveResult.getMoveinvId()).getDetails().stream()
Long taskId = createAndIssueTask("010505", "移库任务", source.getStructCode(), .filter(item -> action.vehicleCode().equals(item.getStoragevehicleCode()))
destination.getStructCode(), action.vehicleCode(), null, null, .findFirst()
taskGroupId, ++sortSeq, "MOVE", action.structId()); .orElseThrow();
strucAttrMapper.lockForTask(source.getStructId(), String.valueOf(taskId)); // 10. 任务起点、目的点严格使用移库单明细,保证移库任务与移库单据完全一致。
strucAttrMapper.lockForTask(destination.getStructId(), String.valueOf(taskId)); Long taskId = createAndIssueTask("010505", "移库任务",
moveDetail.getTurnoutStructCode(), moveDetail.getTurninStructCode(),
action.vehicleCode(), null, null, taskGroupId, ++sortSeq,
"MOVE", String.valueOf(moveDetail.getMoveinvdtlId()));
// 11. 将任务标识和下发状态回写移库明细,便于后续任务回调按明细处理移库完成。
moveInvService.bindTask(moveDetail.getMoveinvdtlId(), taskId);
continue; continue;
} }
// 10. 非本次分配的载具只参与通道规划,不创建出库任务。 // 12. 非本次分配的载具只参与通道规划,不创建出库任务。
List<IostorinvDisDO> vehicleAllocations = allocationsByVehicle.get(action.vehicleCode()); List<IostorinvDisDO> vehicleAllocations = allocationsByVehicle.get(action.vehicleCode());
if (CollUtil.isEmpty(vehicleAllocations)) { if (CollUtil.isEmpty(vehicleAllocations)) {
continue; continue;
} }
// 11. 比较两个出库点未完成任务数,选择当前负载较小的目的点。 // 13. 比较两个出库点未完成任务数,选择当前负载较小的目的点。
String destinationPoint = chooseDestination(destinationLoads); String destinationPoint = chooseDestination(destinationLoads);
Long taskId = createAndIssueTask("010503", "出库任务", source.getStructCode(), Long taskId = createAndIssueTask("010503", "出库任务", source.getStructCode(),
destinationPoint, action.vehicleCode(), detail.getMaterialId(), destinationPoint, action.vehicleCode(), detail.getMaterialId(),
detail.getMaterialCode(), taskGroupId, ++sortSeq, "OUT", detail.getMaterialCode(), taskGroupId, ++sortSeq, "OUT",
vehicleAllocations.get(0).getIostorinvdisId()); vehicleAllocations.get(0).getIostorinvdisId());
destinationLoads[destinationPoint.equals(OUTBOUND_POINT_1.getPointCode()) ? 0 : 1]++; destinationLoads[destinationPoint.equals(OUTBOUND_POINT_1.getPointCode()) ? 0 : 1]++;
// 12. 同载具的全部分配记录关联同一个任务,并推进到“执行中/已下发”状态。 // 14. 同载具的全部分配记录关联同一个任务,并推进到“执行中/已下发”状态。
vehicleAllocations.forEach(allocation -> { vehicleAllocations.forEach(allocation -> {
IostorinvDisDO update = new IostorinvDisDO(); IostorinvDisDO update = new IostorinvDisDO();
update.setIostorinvdisId(allocation.getIostorinvdisId()); update.setIostorinvdisId(allocation.getIostorinvdisId());
@@ -518,7 +989,7 @@ public class IostorInvServiceImpl implements IostorInvService {
update.setIsIssued("1"); update.setIsIssued("1");
iostorinvDisMapper.updateById(update); iostorinvDisMapper.updateById(update);
}); });
// 13. 锁定出库起点仓位,直到任务回调完成后再由后续流程释放。 // 15. 锁定出库起点仓位,直到任务回调完成后再由后续流程释放。
strucAttrMapper.lockForTask(source.getStructId(), String.valueOf(taskId)); strucAttrMapper.lockForTask(source.getStructId(), String.valueOf(taskId));
} }
} }
@@ -798,6 +1269,20 @@ public class IostorInvServiceImpl implements IostorInvService {
.in(BsrealStorAttrDO::getStorId, storIds)), .in(BsrealStorAttrDO::getStorId, storIds)),
BsrealStorAttrDO::getStorId, BsrealStorAttrDO::getStorName); BsrealStorAttrDO::getStorId, BsrealStorAttrDO::getStorName);
pageResult.getList().forEach(item -> item.setStorName(storNameMap.get(item.getStorId()))); pageResult.getList().forEach(item -> item.setStorName(storNameMap.get(item.getStorId())));
Set<Long> creatorIds = pageResult.getList().stream()
.map(IostorInvDO::getCreator)
.filter(StrUtil::isNotBlank)
.map(Long::valueOf)
.collect(Collectors.toSet());
if (CollUtil.isNotEmpty(creatorIds)) {
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(creatorIds);
pageResult.getList().forEach(item -> {
if (StrUtil.isNotBlank(item.getCreator())) {
AdminUserRespDTO user = userMap.get(Long.valueOf(item.getCreator()));
item.setCreatorName(user == null ? null : user.getNickname());
}
});
}
return pageResult; return pageResult;
} }

View File

@@ -31,4 +31,12 @@ public interface MoveInvService {
List<StrucAttrDO> getAvailableTargetStructs(String sourceStructCode); List<StrucAttrDO> getAvailableTargetStructs(String sourceStructCode);
MoveInvAutoCreateRespDTO autoCreate(MoveInvAutoCreateReqDTO reqDTO); MoveInvAutoCreateRespDTO autoCreate(MoveInvAutoCreateReqDTO reqDTO);
/**
* 将已创建并下发的搬运任务绑定到移库明细。
*
* @param moveinvdtlId 移库明细标识
* @param taskId 搬运任务标识
*/
void bindTask(Long moveinvdtlId, Long taskId);
} }

View File

@@ -1,11 +1,14 @@
package cn.code.nl.module.wms.service.moveinv; package cn.code.nl.module.wms.service.moveinv;
import cn.hutool.core.util.StrUtil;
import cn.code.nl.framework.common.pojo.CommonResult; import cn.code.nl.framework.common.pojo.CommonResult;
import cn.code.nl.framework.common.pojo.PageResult; import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.module.base.api.codegen.CodeGenApi; import cn.code.nl.module.base.api.codegen.CodeGenApi;
import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO;
import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateReqDTO; import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateReqDTO;
import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateRespDTO; import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateRespDTO;
import cn.code.nl.module.system.api.user.AdminUserApi;
import cn.code.nl.module.system.api.user.dto.AdminUserRespDTO;
import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvDetailSaveReqVO; import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvDetailSaveReqVO;
import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvInventoryRespVO; import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvInventoryRespVO;
import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvPageReqVO; import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvPageReqVO;
@@ -56,6 +59,8 @@ public class MoveInvServiceImpl implements MoveInvService {
@Resource @Resource
private MoveInvDtlMapper moveInvDtlMapper; private MoveInvDtlMapper moveInvDtlMapper;
@Resource @Resource
private AdminUserApi adminUserApi;
@Resource
private StrucAttrMapper strucAttrMapper; private StrucAttrMapper strucAttrMapper;
@Resource @Resource
private CodeGenApi codeGenApi; private CodeGenApi codeGenApi;
@@ -129,6 +134,7 @@ public class MoveInvServiceImpl implements MoveInvService {
throw exception(MOVE_INV_NOT_EXISTS); throw exception(MOVE_INV_NOT_EXISTS);
} }
MoveInvRespVO respVO = convert(moveInv); MoveInvRespVO respVO = convert(moveInv);
fillCreatorNames(List.of(respVO));
respVO.setDetails(moveInvDtlMapper.selectByMoveinvId(id)); respVO.setDetails(moveInvDtlMapper.selectByMoveinvId(id));
return respVO; return respVO;
} }
@@ -140,9 +146,31 @@ public class MoveInvServiceImpl implements MoveInvService {
public PageResult<MoveInvRespVO> getPage(MoveInvPageReqVO reqVO) { public PageResult<MoveInvRespVO> getPage(MoveInvPageReqVO reqVO) {
PageResult<MoveInvDO> page = moveInvMapper.selectPage(reqVO); PageResult<MoveInvDO> page = moveInvMapper.selectPage(reqVO);
List<MoveInvRespVO> list = page.getList().stream().map(this::convert).toList(); List<MoveInvRespVO> list = page.getList().stream().map(this::convert).toList();
fillCreatorNames(list);
return new PageResult<>(list, page.getTotal()); return new PageResult<>(list, page.getTotal());
} }
/**
* 创建人字段由框架自动填充为字符串,这里显式批量查询姓名,避免自动翻译类型不匹配。
*/
private void fillCreatorNames(List<MoveInvRespVO> list) {
Set<Long> creatorIds = list.stream()
.map(MoveInvRespVO::getCreator)
.filter(StrUtil::isNotBlank)
.map(Long::valueOf)
.collect(Collectors.toSet());
if (creatorIds.isEmpty()) {
return;
}
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(creatorIds);
list.forEach(item -> {
if (StrUtil.isNotBlank(item.getCreator())) {
AdminUserRespDTO user = userMap.get(Long.valueOf(item.getCreator()));
item.setCreatorName(user == null ? null : user.getNickname());
}
});
}
/** /**
* 查询移库可用库存。 * 查询移库可用库存。
*/ */
@@ -204,6 +232,19 @@ public class MoveInvServiceImpl implements MoveInvService {
return new MoveInvAutoCreateRespDTO(moveInv.getMoveinvId(), moveInv.getBillCode()); return new MoveInvAutoCreateRespDTO(moveInv.getMoveinvId(), moveInv.getBillCode());
} }
/**
* 将任务标识回写到移库明细,并标记为已下发。
*/
@Override
public void bindTask(Long moveinvdtlId, Long taskId) {
MoveInvDtlDO update = new MoveInvDtlDO();
update.setMoveinvdtlId(moveinvdtlId);
update.setTaskId(taskId);
update.setWorkStatus("20");
update.setIsIssued("1");
moveInvDtlMapper.updateById(update);
}
/** /**
* 创建主表、明细并锁定来源和目标仓位。 * 创建主表、明细并锁定来源和目标仓位。
*/ */

View File

@@ -15,8 +15,11 @@
dis.qty_unit_name, dis.qty_unit_name,
dis.sect_id, dis.sect_id,
dis.sect_name, dis.sect_name,
dis.struct_id,
dis.struct_code, dis.struct_code,
dis.struct_name dis.struct_name,
dis.point_code,
dis.task_id
FROM wms_iostorinvdis dis FROM wms_iostorinvdis dis
LEFT JOIN base_materialbase mb LEFT JOIN base_materialbase mb
ON mb.material_id = dis.material_id ON mb.material_id = dis.material_id

View File

@@ -108,6 +108,7 @@
d.source_bill_code, d.source_bill_code,
d.source_bill_type, d.source_bill_type,
d.source_billdtl_id, d.source_billdtl_id,
d.source_load_port AS vehicle_code,
d.remark d.remark
FROM wms_iostorinvdtl d FROM wms_iostorinvdtl d
LEFT JOIN base_materialbase mb LEFT JOIN base_materialbase mb

View File

@@ -1,10 +1,24 @@
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import type { PageParam, PageResult } from '@vben/request'; import type { PageParam, PageResult } from '@vben/request';
import type { WmsStrucAttrApi } from '#/api/wms/structAttr';
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
export namespace WmsIostorInvApi { export namespace WmsIostorInvApi {
export interface InboundGroupPlate {
groupId: string;
vehicleCode?: string;
status: string;
materialId?: string;
materialCode: string;
pcsn?: string;
qty: number;
qtyUnitId?: string;
qtyUnitName?: string;
extCode?: string;
}
/** 出入库单主表信息 */ /** 出入库单主表信息 */
export interface IostorInv { export interface IostorInv {
iostorinvId: string; // 出入单标识 iostorinvId: string; // 出入单标识
@@ -127,8 +141,11 @@ export namespace WmsIostorInvApi {
qtyUnitName?: string; qtyUnitName?: string;
sectId?: string; sectId?: string;
sectName?: string; sectName?: string;
structId?: string;
structCode?: string; structCode?: string;
structName?: string; structName?: string;
pointCode?: string;
taskId?: string;
} }
export interface ManualAllocationInventory { export interface ManualAllocationInventory {
@@ -157,6 +174,75 @@ export function getIostorInvPage(params: PageParam) {
); );
} }
export function getInboundGroupPlates(storId: string) {
return requestClient.get<WmsIostorInvApi.InboundGroupPlate[]>(
'/wms/iostor-inv/inbound-group-plates',
{ params: { storId } },
);
}
export function createInbound(data: {
billType: string;
bizDate: string;
groupIds: string[];
remark?: string;
storId: string;
}) {
return requestClient.post('/wms/iostor-inv/create-inbound', data);
}
export function updateInbound(data: {
billType: string;
bizDate: string;
groupIds: string[];
iostorinvId: string;
remark?: string;
storId: string;
}) {
return requestClient.put('/wms/iostor-inv/update-inbound', data);
}
export function deleteInbound(iostorinvId: string) {
return requestClient.delete('/wms/iostor-inv/delete-inbound', {
params: { iostorinvId },
});
}
export function allocateInbound(data: {
autoAllocate?: boolean;
iostorinvId: string;
items: { iostorinvdtlId: string; sectId: string; structId: string }[];
}) {
return requestClient.post('/wms/iostor-inv/allocate-inbound', data);
}
export function cancelInboundAllocation(iostorinvId: string, iostorinvdtlId: string) {
return requestClient.delete('/wms/iostor-inv/cancel-inbound-allocation', {
params: { iostorinvId, iostorinvdtlId },
});
}
export function getInboundAvailableStructs(storId: string, sectId: string) {
return requestClient.get<WmsStrucAttrApi.StrucAttr[]>(
'/wms/iostor-inv/inbound-available-structs',
{ params: { storId, sectId } },
);
}
export function setInboundPoint(data: {
iostorinvId: string;
iostorinvdtlId: string;
pointCode: string;
}) {
return requestClient.post('/wms/iostor-inv/set-inbound-point', data);
}
export function completeInbound(iostorinvId: string) {
return requestClient.post('/wms/iostor-inv/complete-inbound', undefined, {
params: { iostorinvId },
});
}
/** 查询出入库单主表详情 */ /** 查询出入库单主表详情 */
export function getIostorInv(id: string) { export function getIostorInv(id: string) {
return requestClient.get<WmsIostorInvApi.IostorInv>( return requestClient.get<WmsIostorInvApi.IostorInv>(

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; import type { WmsIostorInvApi } from '#/api/wms/iostorinv';
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
@@ -17,6 +17,8 @@ const header = ref<WmsIostorInvApi.IostorInv>();
const details = ref<WmsIostorInvApi.OutboundDisplayDetail[]>([]); const details = ref<WmsIostorInvApi.OutboundDisplayDetail[]>([]);
const allocations = ref<WmsIostorInvApi.OutboundAllocation[]>([]); const allocations = ref<WmsIostorInvApi.OutboundAllocation[]>([]);
const loading = ref(false); const loading = ref(false);
const isInbound = computed(() => header.value?.ioType === '0');
const modalTitle = computed(() => isInbound.value ? '入库单详情' : '出库单详情');
const detailColumns = [ const detailColumns = [
{ key: 'index', title: '序号', width: 60 }, { key: 'index', title: '序号', width: 60 },
@@ -38,7 +40,7 @@ const allocationColumns = [
{ dataIndex: 'materialName', title: '物料名称', width: 170 }, { dataIndex: 'materialName', title: '物料名称', width: 170 },
{ dataIndex: 'storagevehicleCode', title: '箱号', width: 150 }, { dataIndex: 'storagevehicleCode', title: '箱号', width: 150 },
{ dataIndex: 'pcsn', title: '批次', width: 140 }, { dataIndex: 'pcsn', title: '批次', width: 140 },
{ dataIndex: 'planQty', title: '出库重量', width: 120 }, { dataIndex: 'planQty', key: 'allocationQty', title: '分配重量', width: 120 },
{ dataIndex: 'sectName', title: '库区', width: 130 }, { dataIndex: 'sectName', title: '库区', width: 130 },
{ dataIndex: 'structCode', title: '仓位编码', width: 130 }, { dataIndex: 'structCode', title: '仓位编码', width: 130 },
{ dataIndex: 'structName', title: '仓位名称', width: 160 }, { dataIndex: 'structName', title: '仓位名称', width: 160 },
@@ -80,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal class="w-[1500px] max-w-[98vw]" title="出入库单详情"> <Modal class="w-[1500px] max-w-[98vw]" :title="modalTitle">
<div class="px-3 pb-3"> <div class="px-3 pb-3">
<Descriptions bordered class="mb-4" size="small" :column="4"> <Descriptions bordered class="mb-4" size="small" :column="4">
<DescriptionsItem label="单据编号">{{ header?.billCode }}</DescriptionsItem> <DescriptionsItem label="单据编号">{{ header?.billCode }}</DescriptionsItem>
@@ -130,6 +132,9 @@ const [Modal, modalApi] = useVbenModal({
<template v-else-if="column.dataIndex === 'workStatus'"> <template v-else-if="column.dataIndex === 'workStatus'">
{{ statusLabel(record.workStatus) }} {{ statusLabel(record.workStatus) }}
</template> </template>
<template v-else-if="column.key === 'allocationQty'">
{{ record.planQty }}
</template>
</template> </template>
</Table> </Table>
</div> </div>

View File

@@ -393,6 +393,13 @@ export function useGridColumns(): VxeTableGridOptions<WmsStrucAttrApi.StrucAttr>
{ field: 'blockNum', title: '块', minWidth: 120 }, { field: 'blockNum', title: '块', minWidth: 120 },
{ field: 'isTempstruct', title: '是否临时仓位', minWidth: 120, formatter: boolFormatter }, { field: 'isTempstruct', title: '是否临时仓位', minWidth: 120, formatter: boolFormatter },
{ field: 'isUsed', title: '是否启用', minWidth: 120, formatter: boolFormatter }, { field: 'isUsed', title: '是否启用', minWidth: 120, formatter: boolFormatter },
{ field: 'storagevehicleCode', title: '存储载具号', minWidth: 150 },
{
field: 'lockType',
title: '锁定类型',
minWidth: 120,
cellRender: { name: 'CellDict', props: { type: DICT_TYPE.WMS_LOCK_TYPE } },
},
{ field: 'isZdepth', title: '是否判断高度', minWidth: 120, formatter: boolFormatter }, { field: 'isZdepth', title: '是否判断高度', minWidth: 120, formatter: boolFormatter },
{ field: 'createTime', title: '创建时间', minWidth: 120, formatter: 'formatDateTime' }, { field: 'createTime', title: '创建时间', minWidth: 120, formatter: 'formatDateTime' },
{ field: 'remark', title: '备注', minWidth: 120 }, { field: 'remark', title: '备注', minWidth: 120 },

View File

@@ -289,6 +289,7 @@ const WMS_DICT = {
WMS_VEHICLE_TYPE: 'wms_vehicle_type', // 载具类型 WMS_VEHICLE_TYPE: 'wms_vehicle_type', // 载具类型
WMS_OVER_STRUCT_TYPE: 'wms_over_struct_type', // 是否超限 WMS_OVER_STRUCT_TYPE: 'wms_over_struct_type', // 是否超限
WMS_OUT_BILL_TYPE: 'wms_out_bill_type', // 出库单据类型 WMS_OUT_BILL_TYPE: 'wms_out_bill_type', // 出库单据类型
WMS_IN_BILL_TYPE: 'wms_in_bill_type', // 入库单据类型
WMS_IO_BILL_STATUS: 'wms_io_bill_status', // 出入库单据状态 WMS_IO_BILL_STATUS: 'wms_io_bill_status', // 出入库单据状态
WMS_CREATE_TYPE: 'wms_create_type', // 生成方式 WMS_CREATE_TYPE: 'wms_create_type', // 生成方式
} as const; } as const;