Merge branch 'dev' into dev-bms-260826

# Conflicts:
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/BmsServiceFeign.java
#	mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/facade/IReservationStockInOrderService.java
#	mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/mapper/ReservationStockInOrderMapper.java
#	mhd_oms/src/main/resources/mapper/reservationStockInOrder/ReservationStockInOrderMapper.xml
#	mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/po/MaterialInventoryPO.java
#	mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/todo/MaterialInventoryDO.java
#	mhd_wms/src/main/java/com/mhd/wms/interfaces/dto/materialInventory/MaterialInventoryDTO.java
#	mhd_wms/src/main/resources/mapper/materialInventory/MaterialInventoryMapper.xml
This commit is contained in:
王奎兴
2026-09-05 15:14:35 +08:00
211 changed files with 9989 additions and 706 deletions
@@ -103,10 +103,14 @@ public class BusinessDocumentOrderApplicationService {
public List<BusinessDocumentOrderPO> queryList(BusinessDocumentOrderDO businessDocumentOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null){
if (loginUser != null && loginUser.getUserPo() != null){
// 前端未传组织ID时,查询当前登录组织的数据
if (businessDocumentOrderDO.getOrganizationId() == null){
businessDocumentOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
}
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1){
businessDocumentOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
businessDocumentOrderDO.setTopOrganizationId(topOrganizationId);
}
}
return businessDocumentOrderDomainService.queryList(businessDocumentOrderDO);
@@ -0,0 +1,390 @@
package com.mhd.oms.application.service.outEntrustOrder;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
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.oms.constant.OutEntrustOrderConstant;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderDetailPO;
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.system.api.SystemServiceFeign;
import com.mhd.system.api.WmsServiceFeign;
import com.mhd.system.api.domain.NoticeMaterialDetailDTO;
import com.mhd.system.api.domain.NoticeOrderDTO;
import com.mhd.system.api.domain.WarehouseFeignPO;
import com.mhd.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 出仓委托单ApplicationService
*
* @author rcx
* @date 2026-09-02
*/
@Service
@Slf4j
public class OutEntrustOrderApplicationService {
@Resource
private OutEntrustOrderDomainService outEntrustOrderDomainService;
@Resource
private SystemServiceFeign systemServiceFeign;
@Resource
private WmsServiceFeign wmsServiceFeign;
/**
* 分页查询出仓委托单列表(不做默认状态过滤,显示全部数据)
*/
public List<OutEntrustOrderPO> queryList(OutEntrustOrderDO outEntrustOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1) {
outEntrustOrderDO.setTopOrganizationId(topOrganizationId);
}
}
//结束时间只传年月日(解析为当天00:00:00),归一为当天最后一秒,保证查询包含结束日当天
if (outEntrustOrderDO.getEndCreateTime() != null) {
outEntrustOrderDO.setEndCreateTime(DateUtil.endOfDay(outEntrustOrderDO.getEndCreateTime()));
}
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryList(outEntrustOrderDO);
fillInvoiceNo(orderList);
return orderList;
}
/**
* 列表回填发票号(明细发票号去重逗号拼接,供列表列展示)
*/
private void fillInvoiceNo(List<OutEntrustOrderPO> orderList) {
if (orderList == null || orderList.isEmpty()) {
return;
}
List<String> entrustNumbers = orderList.stream()
.map(OutEntrustOrderPO::getEntrustNumber).collect(Collectors.toList());
Map<String, String> invoiceNoMap = outEntrustOrderDomainService.getDetailByEntrustNumbers(entrustNumbers).stream()
.filter(detail -> detail.getInvoiceNo() != null && !detail.getInvoiceNo().trim().isEmpty())
.collect(Collectors.toMap(OutEntrustOrderDetailPO::getEntrustNumber, detail -> detail.getInvoiceNo().trim(),
(existing, replacement) -> existing.contains(replacement) ? existing
: existing + "," + replacement));
for (OutEntrustOrderPO order : orderList) {
order.setInvoiceNo(invoiceNoMap.get(order.getEntrustNumber()));
}
}
/**
* 新增出仓委托单(必填:客户、业务员、仓库、商品名称、申报数量)
*/
public Boolean insert(OutEntrustOrderDO outEntrustOrderDO) {
//必填项校验
validateRequired(outEntrustOrderDO);
//订单类型未维护时默认普通出库(落库即生效,列表可见)
setDefaultOrderType(outEntrustOrderDO);
//仓库信息回填(校验仓库存在)
setWarehouseInfo(outEntrustOrderDO);
//组织信息填充
setOrganizationInfo(outEntrustOrderDO);
//汇总申报总数量
outEntrustOrderDO.setDeclareTotalQuantity(sumDeclareQuantity(outEntrustOrderDO.getMaterialDetailList()));
return outEntrustOrderDomainService.insert(outEntrustOrderDO);
}
/**
* 修改出仓委托单(仅待审核/已驳回且未下发可修改;已驳回单编辑后状态重置为待审核)
*/
public Boolean update(OutEntrustOrderDO outEntrustOrderDO) {
//必填项校验
validateRequired(outEntrustOrderDO);
//状态类字段由审核/下发/完成入库接口推进,编辑不允许改动(置null后updateById跳过这些列,防前端误传/绕过审核改状态)
outEntrustOrderDO.setOrderStatus(null);
outEntrustOrderDO.setIssueStatus(null);
outEntrustOrderDO.setAuditRemark(null);
//订单类型未维护时默认普通出库(落库即生效,列表可见)
setDefaultOrderType(outEntrustOrderDO);
//仓库信息回填(校验仓库存在)
setWarehouseInfo(outEntrustOrderDO);
//明细全量替换时明细创建信息随主单更新人
if (outEntrustOrderDO.getCreateBy() == null) {
outEntrustOrderDO.setCreateBy(outEntrustOrderDO.getUpdateBy());
outEntrustOrderDO.setCreateByName(outEntrustOrderDO.getUpdateByName());
outEntrustOrderDO.setCreateTime(outEntrustOrderDO.getUpdateTime());
}
//汇总申报总数量
outEntrustOrderDO.setDeclareTotalQuantity(sumDeclareQuantity(outEntrustOrderDO.getMaterialDetailList()));
return outEntrustOrderDomainService.update(outEntrustOrderDO);
}
/**
* 订单类型未维护时默认普通出库(与下发兜底保持一致,保存即落库)
*/
private void setDefaultOrderType(OutEntrustOrderDO outEntrustOrderDO) {
if (outEntrustOrderDO.getOrderTypeCode() == null || outEntrustOrderDO.getOrderTypeCode().trim().isEmpty()) {
outEntrustOrderDO.setOrderTypeCode(OutEntrustOrderConstant.WMS_ORDER_TYPE_CODE);
outEntrustOrderDO.setOrderTypeName(OutEntrustOrderConstant.WMS_ORDER_TYPE_NAME);
}
}
/**
* 获取出仓委托单详情(含明细)
*/
public OutEntrustOrderPO getInfo(Long id) {
return outEntrustOrderDomainService.getInfo(id);
}
/**
* 批量删除出仓委托单(仅待审核/已驳回且未下发可删除)
*/
public Boolean delete(Long[] ids) {
if (ids == null || ids.length == 0) {
throw new ServiceException("传递的ID值缺失或为空");
}
return outEntrustOrderDomainService.delete(ids);
}
/**
* 批量审核:待审核/已驳回 → 已审核(2)或已驳回(3)
*
* @param dto.idList 委托单ID集合
* @param dto.orderStatus 审核结果:2-审核通过 3-驳回
* @param dto.auditRemark 审核备注(驳回原因,选填)
*/
public Boolean audit(OutEntrustOrderDO outEntrustOrderDO) {
List<Long> ids = outEntrustOrderDO.getIdList();
if (ids == null || ids.isEmpty()) {
throw new ServiceException("传递的ID值缺失或为空");
}
Integer targetStatus = outEntrustOrderDO.getOrderStatus();
if (!OutEntrustOrderConstant.ORDER_STATUS_AUDITED.equals(targetStatus)
&& !OutEntrustOrderConstant.ORDER_STATUS_REJECTED.equals(targetStatus)) {
throw new ServiceException("审核结果参数错误,只能为已审核或已驳回");
}
//只有待审核/已驳回的委托单才可以审核
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryByIds(ids);
List<Long> unableAuditIds = orderList.stream()
.filter(order -> !OutEntrustOrderConstant.ORDER_STATUS_PENDING_AUDIT.equals(order.getOrderStatus())
&& !OutEntrustOrderConstant.ORDER_STATUS_REJECTED.equals(order.getOrderStatus()))
.map(OutEntrustOrderPO::getId).collect(Collectors.toList());
if (!unableAuditIds.isEmpty()) {
throw new ServiceException("选中数据中存在进行中/已审核/已入库的委托单,只有待审核或已驳回的委托单方可审核");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Long auditBy = loginUser == null ? null : loginUser.getUserid();
String auditByName = loginUser == null ? null : loginUser.getUsername();
return outEntrustOrderDomainService.audit(ids, targetStatus, auditBy, auditByName, outEntrustOrderDO.getAuditRemark());
}
/**
* 批量下发:已审核的委托单下发到WMS(生成出库通知单),下发后订单状态保持已审核,仅置下发状态为已下发
*/
@Transactional(rollbackFor = Exception.class)
public Boolean issueOrder(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw new ServiceException("传递的ID值缺失或为空");
}
//只有已审核且未下发的委托单才可以下发
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryByIds(ids);
for (OutEntrustOrderPO order : orderList) {
if (!OutEntrustOrderConstant.ORDER_STATUS_AUDITED.equals(order.getOrderStatus())) {
throw new ServiceException(String.format("委托单[%s]未审核,已审核的委托单方可下发", order.getEntrustNumber()));
}
if (!OutEntrustOrderConstant.ISSUE_STATUS_1.equals(order.getIssueStatus())) {
throw new ServiceException(String.format("委托单[%s]已下发,不允许重复下发", order.getEntrustNumber()));
}
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Long issueBy = loginUser == null ? null : loginUser.getUserid();
String issueByName = loginUser == null ? null : loginUser.getUsername();
for (OutEntrustOrderPO order : orderList) {
//组装出库通知单
NoticeOrderDTO noticeOrderDTO = createNoticeOrderInfo(order);
//推送给WMS
AjaxResult ajaxResult = wmsServiceFeign.stockOutOrInInsert(noticeOrderDTO);
//WMS返回成功才允许置为已下发,避免下发失败被静默吞掉
if (ajaxResult == null || !"200".equals(String.valueOf(ajaxResult.get("code")))) {
String errorMsg = ajaxResult == null ? "WMS服务调用失败" : String.valueOf(ajaxResult.get("msg"));
log.error("出仓委托单[{}]下发WMS失败:{}", order.getEntrustNumber(), errorMsg);
throw new ServiceException(String.format("委托单[%s]下发WMS失败:%s", order.getEntrustNumber(), errorMsg));
}
log.info("出仓委托单[{}]下发WMS成功", order.getEntrustNumber());
}
return outEntrustOrderDomainService.issue(ids, issueBy, issueByName);
}
/**
* 批量完成:已审核(已下发) → 已入库(OMS人工推进,WMS出库完成后由人工确认)
*/
public Boolean finish(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw new ServiceException("传递的ID值缺失或为空");
}
//只有已审核且已下发的委托单才可以置为已入库(兼容存量:旧版本曾自动置为进行中的也允许)
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryByIds(ids);
List<Long> unableFinishIds = orderList.stream()
.filter(order -> !(OutEntrustOrderConstant.ORDER_STATUS_IN_PROGRESS.equals(order.getOrderStatus())
|| (OutEntrustOrderConstant.ORDER_STATUS_AUDITED.equals(order.getOrderStatus())
&& OutEntrustOrderConstant.ISSUE_STATUS_2.equals(order.getIssueStatus()))))
.map(OutEntrustOrderPO::getId).collect(Collectors.toList());
if (!unableFinishIds.isEmpty()) {
throw new ServiceException("选中数据中存在待审核/已驳回或未下发的委托单,只有已审核且已下发的委托单方可置为已入库");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Long updateBy = loginUser == null ? null : loginUser.getUserid();
String updateByName = loginUser == null ? null : loginUser.getUsername();
return outEntrustOrderDomainService.finish(ids, updateBy, updateByName);
}
/**
* 组装出库通知单信息(客户=货主)
*/
private NoticeOrderDTO createNoticeOrderInfo(OutEntrustOrderPO order) {
NoticeOrderDTO noticeOrderDTO = new NoticeOrderDTO();
noticeOrderDTO.setType(OutEntrustOrderConstant.NOTICE_TYPE_OUT);
//客户订单号记录委托单号,便于WMS侧追溯
noticeOrderDTO.setCustomerOrderNumber(order.getEntrustNumber());
noticeOrderDTO.setWarehouseId(order.getWarehouseId());
//出仓委托单的客户即WMS货主,货主为物料档案/库存作业的归属
noticeOrderDTO.setShipperId(order.getShipperId());
noticeOrderDTO.setShipperName(order.getShipperName());
noticeOrderDTO.setCustomerId(order.getShipperId());
noticeOrderDTO.setCustomerName(order.getShipperName());
//订单类型:未维护时默认普通出库(WMS侧按出库单据类型字典翻译)
noticeOrderDTO.setOrderTypeCode(order.getOrderTypeCode() == null
? OutEntrustOrderConstant.WMS_ORDER_TYPE_CODE : order.getOrderTypeCode());
noticeOrderDTO.setOrderTypeName(order.getOrderTypeName());
noticeOrderDTO.setExpectTime(order.getExpectTime() == null ? new Date() : order.getExpectTime());
noticeOrderDTO.setNullify(2);
//出库通知单仅有限几个业务字段:业务员及委托/报关关键信息拼入备注留存(超长截断)
noticeOrderDTO.setRemark(buildNoticeRemark(order));
//物料明细:申报数量作为通知单计划数量
List<OutEntrustOrderDetailPO> detailList = outEntrustOrderDomainService.getDetailByEntrustNumber(order.getEntrustNumber());
if (detailList == null || detailList.isEmpty()) {
throw new ServiceException(String.format("委托单[%s]缺少商品明细,无法下发", order.getEntrustNumber()));
}
List<NoticeMaterialDetailDTO> materialDetailList = new ArrayList<>();
for (OutEntrustOrderDetailPO detail : detailList) {
NoticeMaterialDetailDTO materialDetail = new NoticeMaterialDetailDTO();
materialDetail.setMaterialBaseInfoId(detail.getMaterialBaseInfoId());
materialDetail.setMaterialCode(detail.getMaterialCode());
//物料名称:档案名优先,无档案时用委托单品名
materialDetail.setMaterialName(detail.getMaterialName() != null && !detail.getMaterialName().trim().isEmpty()
? detail.getMaterialName() : detail.getCommodityName());
materialDetail.setBarCode(detail.getBarCode());
materialDetail.setQuantity(detail.getDeclareQuantity());
materialDetailList.add(materialDetail);
}
noticeOrderDTO.setMaterialDetailList(materialDetailList);
return noticeOrderDTO;
}
/**
* 出库通知单备注:拼入业务员及委托/报关关键信息(通知单无对应字段)
*/
private String buildNoticeRemark(OutEntrustOrderPO order) {
StringBuilder remark = new StringBuilder();
if (order.getRemark() != null && !order.getRemark().trim().isEmpty()) {
remark.append(order.getRemark().trim());
}
appendIfPresent(remark, "业务员", order.getSalesmanName());
appendIfPresent(remark, "委托编号", order.getEntrustNo());
appendIfPresent(remark, "柜号", order.getContainerNo());
appendIfPresent(remark, "承运商", order.getCarrierName() != null ? order.getCarrierName() : order.getCarrier());
appendIfPresent(remark, "To", order.getToReceiver());
appendIfPresent(remark, "联系人", order.getContactPerson());
appendIfPresent(remark, "电话", order.getTel());
appendIfPresent(remark, "申报地关区", order.getDeclareCustoms());
appendIfPresent(remark, "进出境关别", order.getEntryExitCustoms());
//备注列长度保护
return remark.length() > 480 ? remark.substring(0, 480) : remark.toString();
}
private void appendIfPresent(StringBuilder remark, String label, String value) {
if (value != null && !value.trim().isEmpty()) {
remark.append(remark.length() > 0 ? "" : "").append(label).append("").append(value.trim());
}
}
/**
* 必填项校验:客户、业务员、仓库、商品名称、申报数量,其余字段不强制
*/
private void validateRequired(OutEntrustOrderDO outEntrustOrderDO) {
if (outEntrustOrderDO.getShipperId() == null) {
throw new ServiceException("客户不能为空");
}
if (outEntrustOrderDO.getSalesmanId() == null || outEntrustOrderDO.getSalesmanId().trim().isEmpty()) {
throw new ServiceException("业务员不能为空");
}
if (outEntrustOrderDO.getWarehouseId() == null) {
throw new ServiceException("仓库不能为空");
}
if (outEntrustOrderDO.getMaterialDetailList() == null || outEntrustOrderDO.getMaterialDetailList().isEmpty()) {
throw new ServiceException("商品明细不能为空");
}
int index = 1;
for (OutEntrustOrderDetailDO detail : outEntrustOrderDO.getMaterialDetailList()) {
//商品名称:物料档案名/委托单品名至少其一
boolean nameBlank = (detail.getMaterialName() == null || detail.getMaterialName().trim().isEmpty())
&& (detail.getCommodityName() == null || detail.getCommodityName().trim().isEmpty());
if (nameBlank) {
throw new ServiceException(String.format("商品明细第%d行商品名称不能为空", index));
}
if (detail.getDeclareQuantity() == null || detail.getDeclareQuantity().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException(String.format("商品明细第%d行申报数量必须大于0", index));
}
index++;
}
}
/**
* 仓库信息校验并回填编码/名称
*/
private void setWarehouseInfo(OutEntrustOrderDO outEntrustOrderDO) {
AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(outEntrustOrderDO.getWarehouseId());
if (ajaxResult == null || !"200".equals(String.valueOf(ajaxResult.get("code")))) {
throw new ServiceException("获取仓库信息失败,请检查仓库是否有效");
}
WarehouseFeignPO warehouseFeignPO = JSONUtil.toBean(JSONUtil.toJsonStr(ajaxResult.get("data")), WarehouseFeignPO.class);
if (warehouseFeignPO == null || Objects.isNull(warehouseFeignPO.getWarehouseId())) {
throw new ServiceException("仓库不存在,请重新选择");
}
outEntrustOrderDO.setWarehouseCode(warehouseFeignPO.getWarehouseCode());
outEntrustOrderDO.setWarehouseName(warehouseFeignPO.getWarehouseName());
}
/**
* 组织信息填充
*/
private void setOrganizationInfo(OutEntrustOrderDO outEntrustOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
outEntrustOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
outEntrustOrderDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
outEntrustOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
}
}
/**
* 汇总明细申报数量
*/
private BigDecimal sumDeclareQuantity(List<OutEntrustOrderDetailDO> detailList) {
return detailList.stream()
.map(OutEntrustOrderDetailDO::getDeclareQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
@@ -0,0 +1,33 @@
package com.mhd.oms.config;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.mhd.common.core.jackson.FlexibleDateDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Date;
/**
* JSON 日期反序列化兼容配置:Date 字段兼容 yyyy-MM-dd(纯日期)等格式。
*
* 背景:库存查询日期只保留年月日后,前端回传纯日期(如 createTime=2026-07-06)时,
* 实体/DTO 上 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 严格格式反序列化报
* InvalidFormatException。注册 FlexibleDateDeserializer 后优先按字段 @JsonFormat
* 严格解析,失败时兼容纯日期等常见格式(与 WMS 库存查询一致)。
*
* Spring Boot 会自动把容器中的 Module Bean 注册进 MVC 使用的 ObjectMapper。
*
* @author rcx
* @date 2026-09-05
*/
@Configuration
public class DateCompatibilityConfig {
@Bean
public Module flexibleDateModule() {
SimpleModule module = new SimpleModule("oms-flexible-date-module");
module.addDeserializer(Date.class, new FlexibleDateDeserializer());
return module;
}
}
@@ -0,0 +1,37 @@
package com.mhd.oms.constant;
/**
* 出仓委托单常量
*
* @author rcx
* @date 2026-09-02
*/
public interface OutEntrustOrderConstant {
/** 出仓委托单号前缀 */
String OUT_ENTRUST_NO_PREFIX = "CKWT";
/** 订单状态:1-待审核(新增默认值;已驳回单重新编辑后也回到此状态) */
Integer ORDER_STATUS_PENDING_AUDIT = 1;
/** 订单状态:2-已审核 */
Integer ORDER_STATUS_AUDITED = 2;
/** 订单状态:3-已驳回 */
Integer ORDER_STATUS_REJECTED = 3;
/** 订单状态:4-进行中(已下发WMS) */
Integer ORDER_STATUS_IN_PROGRESS = 4;
/** 订单状态:5-已入库 */
Integer ORDER_STATUS_STORED = 5;
/** 下发状态:1-未下发 */
Integer ISSUE_STATUS_1 = 1;
/** 下发状态:2-已下发 */
Integer ISSUE_STATUS_2 = 2;
/** 下发WMS出库通知单-订单类型编码 */
String WMS_ORDER_TYPE_CODE = "pu_tong_chu_ku";
/** 下发WMS出库通知单-订单类型名称 */
String WMS_ORDER_TYPE_NAME = "普通出库";
/** 出库通知单类型(NoticeOrderDTO.type1-入库通知单 2-出库通知单) */
Integer NOTICE_TYPE_OUT = 2;
}
@@ -749,6 +749,16 @@ public class BusinessDocumentOrderDO extends BaseVOEntity{
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date layDate;
@ApiModelProperty(value = "预计起运时间起")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date layDateStart;
@ApiModelProperty(value = "预计起运时间止")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date layDateEnd;
@ApiModelProperty("收款账户信息")
private String accountInformation;
@@ -737,9 +737,9 @@ public class BusinessDocumentOrderDomainService {
// boolean istrue=businessDocumentOrderService.update(businessDocumentOrderDO);
//生成货源单号(yyMMdd + 当日顺序号,每日重置) 原更新操作会将 电脑更细为当日的日期。默认和之前的逻辑一样保持不变
String orderCode = goodsNumberGenerator.generateUniqueGoodsNumber(tmsOrder.getDocumentDate());
tmsOrder.setGoodsNumber(orderCode);
//更新操作保留原业务单号,不做重新生成(单号随单据日期与当日顺序生成,编辑时不应变化)
//恢复原单号,避免 BeanUtils.copyProperties 时被前端传入值覆盖
tmsOrder.setGoodsNumber(businessDocumentOrderService.getById(businessDocumentOrderDO.getId()).getGoodsNumber());
ReservationStockOutOrder reservationStockOutOrder = reservationStockOutOrderMapper.selectOne(new LambdaQueryWrapper<ReservationStockOutOrder>()
.eq(ReservationStockOutOrder::getOutOrderNumber, inOrderNumber)
@@ -5,6 +5,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -41,4 +42,10 @@ public class ExecutionOutMaterialDetailWithOrderPO extends ExecutionOutMaterialD
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date orderOutboundTime;
@ApiModelProperty("保额")
private BigDecimal insureAmount;
@ApiModelProperty("货物类型:0-物料 1-半成品 2-成品")
private Integer goodsType;
}
@@ -53,10 +53,11 @@ public interface IExecutionStockInOrderService extends IService<ExecutionStockIn
* 解析Excel文件并获取出库数据
* @param file Excel文件
* @param sheetName 工作表名称出库导入/参考数据
* @param warehouseId 当前仓库id非空时校验导入物料是否属于该仓库
* @return 出库数据DTO
* @throws Exception 解析异常
*/
DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName) throws Exception;
DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception;
/**
* 解析入库单Excel文件
@@ -35,6 +35,7 @@ import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockO
import com.mhd.oms.domain.executionStockOutOrder.service.ExecutionStockOutOrderApplicationService;
import com.mhd.oms.domain.reservationDeliveryDetailsLink.entity.ReservationDeliveryDetailsLink;
import com.mhd.oms.domain.reservationDeliveryDetailsLink.repository.facade.IReservationDeliveryDetailsLinkService;
import com.mhd.oms.domain.reservationStockInOrder.repository.mapper.ReservationStockInOrderMapper;
import com.mhd.system.api.SystemServiceFeign;
import com.mhd.system.api.UserServiceFeign;
import com.mhd.system.api.WmsServiceFeign;
@@ -80,6 +81,8 @@ public class ExecutionStockInOrderImpl extends ServiceImpl<ExecutionStockInOrder
private SystemServiceFeign systemServiceFeign;
@Autowired
private IExecutionInMaterialDetailService materialDetailService;
@Autowired
private ReservationStockInOrderMapper reservationStockInOrderMapper;
/**
* 查询入库单列表
@@ -207,7 +210,7 @@ public class ExecutionStockInOrderImpl extends ServiceImpl<ExecutionStockInOrder
}
@Override
public DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName) throws Exception {
public DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 校验文件和工作表名称逻辑不变
if (file.isEmpty()) {
@@ -263,6 +266,12 @@ public class ExecutionStockInOrderImpl extends ServiceImpl<ExecutionStockInOrder
reservationStockOutOrderDO.setShipperCode((String) shipperIds.get(0).get("shipperCode"));
reservationStockOutOrderDO.setNeedTransportFlag("1");
reservationStockOutOrderDO.setNeedDeclareFlag("1");
// 指定仓库时回填单据仓库信息
if (warehouseId != null && warehouseId > 0) {
reservationStockOutOrderDO.setWarehouseId(warehouseId);
reservationStockOutOrderDO.setWarehouseCode(reservationStockInOrderMapper.getWarehouseCode(warehouseId));
reservationStockOutOrderDO.setWarehouseName(reservationStockInOrderMapper.getWarehouseName(warehouseId));
}
reservationStockOutOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
reservationStockOutOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
reservationStockOutOrderDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
@@ -626,6 +626,11 @@ public class ExecutionStockInOrderDomainService {
inMaterialDetail.setOrganizationId(orgId);
inMaterialDetail.setOrganizationName(orgName);
inMaterialDetail.setTopOrganizationId(topOrgId);
// 过期日期为空且饮用日期有值时用饮用日期补齐过期日期食品业务饮用日期即效期
// 确保下发WMS后入库/收货/库存链路的过期日期能正常回显
if (inMaterialDetail.getExpiryDate() == null && inMaterialDetail.getDrinkDate() != null) {
inMaterialDetail.setExpiryDate(inMaterialDetail.getDrinkDate());
}
inMaterialDetailList.add(inMaterialDetail);
}
wmsServiceFeign.omsInOrderAddDetail(inMaterialDetailList);
@@ -146,6 +146,8 @@ public class ExecutionStockOutOrderDomainService {
*/
@Transactional(rollbackFor = Exception.class)
public Boolean insert(ExecutionStockOutOrderDO stockOutOrderDO) {
//建单校验明细数量必须大于00数量单下发WMS后自动分配必失败出库单会卡死在已审核状态
validateMaterialDetailQuantity(stockOutOrderDO.getMaterialDetailList());
//设置单号
Long warehouseId = stockOutOrderDO.getWarehouseId();
AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(warehouseId);
@@ -252,6 +254,8 @@ public class ExecutionStockOutOrderDomainService {
stockOutOrderDO.setBusinessOrderNo(prefix + seq);
}
//设置物料信息
//建单校验明细数量必须大于00数量单下发WMS后自动分配必失败出库单会卡死在已审核状态
validateMaterialDetailQuantity(stockOutOrderDO.getMaterialDetailList());
setMaterialDetailInfo(stockOutOrderDO);
//统计种类
stockOutOrderDO.setMaterialQuantity(stockOutOrderDO.getMaterialDetailList().size());
@@ -476,27 +480,59 @@ public class ExecutionStockOutOrderDomainService {
* @param stockOutOrderDO
* @return Boolean
*/
@Transactional(rollbackFor = Exception.class)
//注意不能用方法级事务批量下发时若前一张已推送WMS成功后一张失败方法级事务回滚会把已成功单据的
//下发状态一并回滚WMS已分配库存已生成拣货单OMS却显示未下发造成两边状态撕裂
//因此改为每张单全链路推送成功后立即逐单更新并提交下发状态失败单据补偿清理后抛错不影响已成功单据
public Boolean issueOrder(ExecutionStockOutOrderDO stockOutOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 获取用户姓名优先从UserPo获取如果为空则使用realname最后使用username作为兜底
String userRealName = loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
? loginUser.getUserPo().getUserName()
: (loginUser.getRealname() != null ? loginUser.getRealname() : loginUser.getUsername());
boolean update = stockOutOrderService.update(null, new UpdateWrapper<ExecutionStockOutOrder>().lambda()
.set(ExecutionStockOutOrder::getIssueId, loginUser.getUserid())
.set(ExecutionStockOutOrder::getIssueName, userRealName)
.set(ExecutionStockOutOrder::getIssueTime, new Date())
//.set(ExecutionStockOutOrder::getIssueStatus, 1)
.set(ExecutionStockOutOrder::getUpdateBy, loginUser.getUserid())
.set(ExecutionStockOutOrder::getUpdateByName, userRealName)
.set(ExecutionStockOutOrder::getUpdateTime, new Date())
.in(ExecutionStockOutOrder::getOutOrderId, stockOutOrderDO.getOutOrderIds()));
//todo下发推送到wms
//下发前统一校验所有单据先完成全部校验并汇总所有问题一次性提示全部通过后再推送WMS
List<Long> inOrderIds = stockOutOrderDO.getOutOrderIds();
List<ExecutionStockOutOrder> issueOrderList = new ArrayList<>();
List<String> validateErrors = new ArrayList<>();
for (Long inOrderId : inOrderIds) {
ExecutionStockOutOrder byId = stockOutOrderService.getById(inOrderId);
if (byId == null) {
validateErrors.add("出库单不存在,outOrderId=" + inOrderId);
continue;
}
try {
List<ExecutionOutMaterialDetail> detailList = validateIssueOrderDetail(byId);
//下发前调WMS校验可用库存单内全部库存行校验完才返回结果问题先收集最后统一提示不推送任何数据到WMS
List<OutMaterialDetail> checkList = new ArrayList<>();
for (ExecutionOutMaterialDetail detail : detailList) {
OutMaterialDetail checkDetail = new OutMaterialDetail();
checkDetail.setOutOrderNumber(byId.getOutOrderNumber());
checkDetail.setMaterialInventoryId(detail.getMaterialInventoryId());
checkDetail.setQuantity(detail.getQuantity());
checkDetail.setMaterialCode(detail.getMaterialCode());
checkDetail.setMaterialName(detail.getMaterialName());
checkList.add(checkDetail);
}
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
validateErrors.add("出库单[" + byId.getOutOrderNumber() + "]库存校验失败:"
+ (checkResult == null ? "WMS服务不可用,已阻止下发" : checkResult.get("msg")));
continue;
}
issueOrderList.add(byId);
} catch (ServiceException e) {
//单张单据校验失败不中断继续校验其余单据最后统一提示
validateErrors.add(e.getMessage());
}
}
if (!validateErrors.isEmpty()) {
throw new ServiceException("下发校验未通过(共" + validateErrors.size() + "项):" + String.join("", validateErrors));
}
//下发推送到wms
for (ExecutionStockOutOrder byId : issueOrderList) {
String currentOrderNumber = byId.getOutOrderNumber();
try {
StockOutOrder stockOutOrder = new StockOutOrder();
BeanUtils.copyProperties(byId, stockOutOrder,"outOrderId");
Long warehouseId = stockOutOrder.getWarehouseId();
@@ -535,11 +571,97 @@ public class ExecutionStockOutOrderDomainService {
String outOrderNumber = byId.getOutOrderNumber();
com.mhd.system.api.domain.StockOutOrderDTO stockOutOrderDTO = new com.mhd.system.api.domain.StockOutOrderDTO();
stockOutOrderDTO.setOutOrderNumber(outOrderNumber);
wmsServiceFeign.execution(stockOutOrderDTO);
//WMS自动分配校验返回值此前不校验时WMS侧分配失败被Feign降级吞掉OMS显示成功但出库单卡死在WMS"已审核"状态拣货单永远不生成
AjaxResult executionResult = wmsServiceFeign.execution(stockOutOrderDTO);
if (executionResult == null || !"200".equals(String.valueOf(executionResult.get("code")))) {
throw new ServiceException("出库单[" + outOrderNumber + "]WMS自动分配失败:"
+ (executionResult == null ? "WMS服务不可用或已降级" : executionResult.get("msg")));
}
wmsServiceFeign.genPickingOrderByOms(stockOutOrderDTO);
//WMS生成拣货单同样校验返回值失败即中断并回滚让用户能感知到错误
AjaxResult genPickingResult = wmsServiceFeign.genPickingOrderByOms(stockOutOrderDTO);
if (genPickingResult == null || !"200".equals(String.valueOf(genPickingResult.get("code")))) {
throw new ServiceException("出库单[" + outOrderNumber + "]WMS生成拣货单失败:"
+ (genPickingResult == null ? "WMS服务不可用或已降级" : genPickingResult.get("msg")));
}
//该单全链路成功逐单更新并提交下发信息后续单据失败不回滚本单保持与WMS状态一致
boolean updated = stockOutOrderService.update(null, new UpdateWrapper<ExecutionStockOutOrder>().lambda()
.set(ExecutionStockOutOrder::getIssueId, loginUser.getUserid())
.set(ExecutionStockOutOrder::getIssueName, userRealName)
.set(ExecutionStockOutOrder::getIssueTime, new Date())
//.set(ExecutionStockOutOrder::getIssueStatus, 1)
.set(ExecutionStockOutOrder::getUpdateBy, loginUser.getUserid())
.set(ExecutionStockOutOrder::getUpdateByName, userRealName)
.set(ExecutionStockOutOrder::getUpdateTime, new Date())
.eq(ExecutionStockOutOrder::getOutOrderId, byId.getOutOrderId()));
if (!updated) {
log.warn("出库单[{}]WMS下发成功,但OMS下发信息更新失败,请检查", currentOrderNumber);
}
} catch (RuntimeException e) {
//补偿清理推送中途失败时WMS可能已落库单头/明细甚至已分配冻结库存调用取消下发清理
//避免残留"已审核"状态的垃圾单清理失败记error日志提示人工处理不影响向用户报出原始错误
try {
StockOutOrder cancelParam = new StockOutOrder();
cancelParam.setOutOrderNumber(currentOrderNumber);
AjaxResult cancelResult = wmsServiceFeign.omsCancelExecutionOrder(cancelParam);
if (cancelResult == null || !"200".equals(String.valueOf(cancelResult.get("code")))) {
log.error("出库单[{}]下发失败,且WMS补偿清理失败({}),请手动到WMS删除残留单据",
currentOrderNumber, cancelResult == null ? "WMS服务不可用" : cancelResult.get("msg"), e);
} else {
log.warn("出库单[{}]下发失败,已补偿清理WMS侧单据", currentOrderNumber, e);
}
} catch (Exception cleanupEx) {
log.error("出库单[{}]下发失败,且WMS补偿清理异常,请手动到WMS删除残留单据", currentOrderNumber, cleanupEx);
}
throw e;
}
}
return update;
return Boolean.TRUE;
}
/**
* 建单校验出库明细数量必须大于0
* 历史缺陷0数量明细的单下发到WMS后自动分配必然失败且异常被吞出库单卡死在"已审核"状态无法生成拣货单
* 注意明细为空时跳过校验保持原有"先建单后补明细"的行为下发时的校验会兜底拦截无明细单据
*/
private void validateMaterialDetailQuantity(List<ExecutionOutMaterialDetailDO> materialDetailList) {
if (CollectionUtils.isEmpty(materialDetailList)) {
return;
}
for (ExecutionOutMaterialDetailDO detail : materialDetailList) {
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("出库明细数量必须大于0,物料:"
+ (detail.getMaterialCode() != null ? detail.getMaterialCode() : detail.getMaterialName()));
}
}
}
/**
* 下发前校验出库单明细
* 1.必须有物料明细且数量大于0
* 2.明细必须已关联库存materialInventoryId否则WMS按库存ID自动分配会空指针
* 校验放在推送WMS之前失败直接报错回滚避免出库单卡死在WMS"已审核"状态
* @return 校验通过的明细列表供后续WMS库存校验使用
*/
private List<ExecutionOutMaterialDetail> validateIssueOrderDetail(ExecutionStockOutOrder order) {
String outOrderNumber = order.getOutOrderNumber();
List<ExecutionOutMaterialDetail> detailList = outMaterialDetailService.list(new QueryWrapper<ExecutionOutMaterialDetail>().lambda()
.eq(ExecutionOutMaterialDetail::getOutOrderNumber, outOrderNumber));
if (CollectionUtils.isEmpty(detailList)) {
throw new ServiceException("出库单[" + outOrderNumber + "]没有物料明细,无法下发");
}
for (ExecutionOutMaterialDetail detail : detailList) {
String materialDesc = detail.getMaterialCode() != null ? detail.getMaterialCode()
: (detail.getMaterialName() != null ? detail.getMaterialName() : String.valueOf(detail.getMaterialDetailId()));
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("出库单[" + outOrderNumber + "]物料[" + materialDesc + "]数量为0,无法下发");
}
if (detail.getMaterialInventoryId() == null || detail.getMaterialInventoryId() <= 0) {
throw new ServiceException("出库单[" + outOrderNumber + "]物料[" + materialDesc + "]未关联库存,无法下发");
}
}
return detailList;
}
/**
@@ -963,7 +1085,9 @@ public class ExecutionStockOutOrderDomainService {
ReservationDeliveryDetailsLink reservationDeliveryDetailsLink = list.get(0);
String orderNumber = reservationDeliveryDetailsLink.getOrderNumber();
Integer bag = reservationDeliveryDetailsLink.getBag();
ExecutionStockOutOrder stockOutOrder = stockOutOrderService.getOne(new QueryWrapper<ExecutionStockOutOrder>().lambda().eq(ExecutionStockOutOrder::getOutOrderNumber, orderNumber), false);
ExecutionStockOutOrder stockOutOrder = stockOutOrderService.getOne(new QueryWrapper<ExecutionStockOutOrder>().lambda()
.eq(ExecutionStockOutOrder::getOutOrderNumber, orderNumber)
.eq(ExecutionStockOutOrder::getDelFlag,1), false);
if (stockOutOrder != null) {
ExecutionStockInOrder reservationStockInOrder = new ExecutionStockInOrder();
reservationStockInOrder.setPictureUrl(pictureUrl);
@@ -0,0 +1,26 @@
package com.mhd.oms.domain.inboundRank.repository.facade;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import java.util.List;
/**
* 入库排名统计Service接口
*
* @author rcx
* @date 2026-08-31
*/
public interface IExecutionInboundRankService {
/**
* 商品入库排名列表仓单+货品汇总按件数降序
*/
List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO);
/**
* 商品入库排名合计不分页
*/
ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO);
}
@@ -0,0 +1,26 @@
package com.mhd.oms.domain.inboundRank.repository.mapper;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import java.util.List;
/**
* 入库排名统计Mapper接口
*
* @author rcx
* @date 2026-08-31
*/
public interface ExecutionInboundRankMapper {
/**
* 商品入库排名列表按物料编码合并按件数降序过滤0件行
*/
List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO);
/**
* 商品入库排名合计不分页当前筛选条件全量合计
*/
ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO);
}
@@ -0,0 +1,34 @@
package com.mhd.oms.domain.inboundRank.repository.persistence;
import com.mhd.oms.domain.inboundRank.repository.facade.IExecutionInboundRankService;
import com.mhd.oms.domain.inboundRank.repository.mapper.ExecutionInboundRankMapper;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 入库排名统计Service业务层处理
*
* @author rcx
* @date 2026-08-31
*/
@Service
public class ExecutionInboundRankImpl implements IExecutionInboundRankService {
@Autowired
private ExecutionInboundRankMapper executionInboundRankMapper;
@Override
public List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankMapper.queryMaterialRankList(executionInboundRankDO);
}
@Override
public ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankMapper.queryMaterialRankTotal(executionInboundRankDO);
}
}
@@ -0,0 +1,60 @@
package com.mhd.oms.domain.inboundRank.repository.po;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 入库排名统计PO一行 = 一个物料的合计按物料编码合并查询区间内全部已入库仓单按件数降序排名
*
* <p>数据源跨库取 WMS 入库表NGWL_TEST_WMS.stock_in_order / in_material_detailOMS 执行单状态/上架量不回传
* 口径与 WMS /inboundRankApi 完全一致两边数字一致
* 字段口径件数=实际上架量(SHELVES_QUANTITY)合计毛重()=明细总毛重KG/1000体积=明细合计
* 投保总额=货品档案保额(NGWL_TEST_WMS.material_base_info.insure_amount物料维度单一值取MAX不跨仓单累加)
* 仓单编号/客户编号/存货人/入仓日期为单据级字段按物料合并后单值失真仅作查询条件不返回</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "入库排名统计对象", description = "入库排名统计响应对象")
public class ExecutionInboundMaterialRankPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("物料编码(合并依据)")
@Excel(name = "物料编码")
private String materialCode;
@ApiModelProperty("货名/商品名称")
@Excel(name = "货名")
private String materialName;
@ApiModelProperty("规格型号")
@Excel(name = "规格型号")
private String specificationModel;
@ApiModelProperty("单位")
@Excel(name = "单位")
private String unitName;
@ApiModelProperty("件数(该物料实际上架量合计 SHELVES_QUANTITY,排名依据)")
@Excel(name = "件数")
private BigDecimal pieceCount;
@ApiModelProperty("毛重(吨)(该物料合计,KG/1000)")
@Excel(name = "毛重(吨)")
private BigDecimal grossWeightTon;
@ApiModelProperty("投保总额(货品档案保额,物料维度单一值)")
@Excel(name = "投保总额")
private BigDecimal insureAmount;
@ApiModelProperty("体积(CBM)(该物料合计)")
@Excel(name = "体积(CBM)")
private BigDecimal totalVolume;
}
@@ -0,0 +1,39 @@
package com.mhd.oms.domain.inboundRank.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>当前筛选条件下不分页的全量合计随分页列表一起返回
* totalPieceCount/totalGrossWeightTon/totalInsureAmount/totalVolume/totalOrderCount</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "入库排名统计合计对象", description = "入库排名统计合计响应对象")
public class ExecutionInboundRankTotalPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("件数合计(实际上架量)")
private BigDecimal totalPieceCount;
@ApiModelProperty("毛重(吨)合计")
private BigDecimal totalGrossWeightTon;
@ApiModelProperty("投保总额合计(各物料保额之和,每物料计一次)")
private BigDecimal totalInsureAmount;
@ApiModelProperty("体积(CBM)合计")
private BigDecimal totalVolume;
@ApiModelProperty("涉及仓单数(当前筛选条件下去重统计)")
private Long totalOrderCount;
}
@@ -0,0 +1,47 @@
package com.mhd.oms.domain.inboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 入库排名统计查询DO
*
* <p>topOrganizationId 由服务端按登录用户注入前端不传</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionInboundRankDO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("入仓日期-开始 yyyy-MM-dd(必填)")
private String warehouseDateStart;
@ApiModelProperty("入仓日期-结束 yyyy-MM-dd(必填)")
private String warehouseDateEnd;
@ApiModelProperty("仓单编号/入库单号(模糊)")
private String inOrderNumber;
@ApiModelProperty("客户编号(模糊)")
private String shipperCode;
@ApiModelProperty("存货人/客户名称(模糊)")
private String shipperName;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("货名/商品名称(模糊)")
private String materialName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
@ApiModelProperty("一级组织表ID(登录用户隔离,非1时注入)")
private Long topOrganizationId;
}
@@ -0,0 +1,44 @@
package com.mhd.oms.domain.inboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 入库排名统计入参DTO
*
* <p>数据源跨库取 WMS 入库链路NGWL_TEST_WMS.stock_in_order / in_material_detailOMS 执行单状态/上架量不回传</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionInboundRankDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("入仓日期-开始 yyyy-MM-dd(必填,按入仓单入仓日期筛选)")
private String warehouseDateStart;
@ApiModelProperty("入仓日期-结束 yyyy-MM-dd(必填,按入仓单入仓日期筛选)")
private String warehouseDateEnd;
@ApiModelProperty("仓单编号/入库单号(模糊)")
private String inOrderNumber;
@ApiModelProperty("客户编号(模糊)")
private String shipperCode;
@ApiModelProperty("存货人/客户名称(模糊)")
private String shipperName;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("货名/商品名称(模糊)")
private String materialName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
}
@@ -0,0 +1,69 @@
package com.mhd.oms.domain.inboundRank.service;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.utils.StringUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 入库排名统计应用服务
*
* <p>负责登录用户组织隔离注入日期区间校验查询逻辑在 ExecutionInboundRankDomainService ExecutionInboundRankMapper.xml</p>
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionInboundRankApplicationService {
@Autowired
private ExecutionInboundRankDomainService executionInboundRankDomainService;
/**
* 分页查询商品入库排名列表仓单+货品汇总按件数降序
*/
public List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO) {
fillLoginContext(executionInboundRankDO);
return executionInboundRankDomainService.queryMaterialRankList(executionInboundRankDO);
}
/**
* 商品入库排名合计当前筛选条件全量不分页
*/
public ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankDomainService.queryMaterialRankTotal(executionInboundRankDO);
}
/**
* 导出商品入库排名不分页全量
*/
public List<ExecutionInboundMaterialRankPO> exportMaterialRank(ExecutionInboundRankDO executionInboundRankDO) {
fillLoginContext(executionInboundRankDO);
return executionInboundRankDomainService.queryMaterialRankList(executionInboundRankDO);
}
/**
* 注入登录用户组织隔离并校验入仓日期区间必填
*/
private void fillLoginContext(ExecutionInboundRankDO executionInboundRankDO) {
if (StringUtils.isEmpty(executionInboundRankDO.getWarehouseDateStart()) || StringUtils.isEmpty(executionInboundRankDO.getWarehouseDateEnd())) {
throw new ServiceException("请选择统计日期区间");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1) {
executionInboundRankDO.setTopOrganizationId(topOrganizationId);
}
}
}
}
@@ -0,0 +1,36 @@
package com.mhd.oms.domain.inboundRank.service;
import cn.hutool.core.util.ObjectUtil;
import com.mhd.common.core.exception.digitalLogisticsException.DigitalLogisticsException;
import com.mhd.common.core.exception.digitalLogisticsException.UserError;
import com.mhd.common.core.utils.IgnoreNullUtil;
import com.mhd.common.core.utils.bean.BeanUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDTO;
import org.springframework.stereotype.Component;
/**
* 入库排名统计Assembler
*
* @author rcx
* @date 2026-08-31
*/
@Component
public class ExecutionInboundRankAssembler {
/**
* 转换实体
*/
public ExecutionInboundRankDO toDO(ExecutionInboundRankDTO executionInboundRankDTO) {
ExecutionInboundRankDO executionInboundRankDO = new ExecutionInboundRankDO();
// 拷贝
BeanUtils.copyProperties(executionInboundRankDTO, executionInboundRankDO, IgnoreNullUtil.getNullPropertyNames(executionInboundRankDTO));
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
return executionInboundRankDO;
}
}
@@ -0,0 +1,39 @@
package com.mhd.oms.domain.inboundRank.service;
import com.mhd.oms.domain.inboundRank.repository.facade.IExecutionInboundRankService;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 入库排名统计领域服务
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionInboundRankDomainService {
@Autowired
private IExecutionInboundRankService executionInboundRankService;
/**
* 商品入库排名列表仓单+货品汇总按件数降序
*/
public List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankService.queryMaterialRankList(executionInboundRankDO);
}
/**
* 商品入库排名合计不分页
*/
public ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankService.queryMaterialRankTotal(executionInboundRankDO);
}
}
@@ -5,6 +5,7 @@ import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMateria
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
import com.mhd.oms.domain.reservationStockInOrder.repository.mapper.ReservationStockInOrderMapper;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -13,6 +14,7 @@ import org.springframework.stereotype.Service;
* 解决客户端推单把 materialInventoryId 传进 materialBaseInfoId 字段或漏传导致明细存脏ID的问题
* 三级解析1.按baseId直查 2.按库存ID反查纠正 3.按物料编码+组织兜底
*/
@Slf4j
@Service
public class MaterialBaseInfoResolveService {
@@ -30,11 +32,23 @@ public class MaterialBaseInfoResolveService {
* @return 解析成功返回物料信息调用方需用 po.getMaterialBaseInfoId() 回填纠正后的ID全部失败返回 null
*/
public MaterialBaseInfoPO resolve(Long materialBaseInfoId, Long materialInventoryId, String materialCode, Long organizationId) {
// 1. 优先按 baseId 直查有效ID不受影响
// 1. 优先按 baseId 直查有效ID不受影响
// 防张冠李戴客户端曾把库存ID传进 materialBaseInfoId若该库存ID恰好撞上另一条有效档案的主键
// 如库存1201撞上编码=名称的脏档案"水箱"按ID查到的档案编码会与明细编码对不上
// 此时 baseId 不可信继续走库存反查/编码兜底两级都落空时才退回按ID查到的档案
MaterialBaseInfoPO byId = null;
if (materialBaseInfoId != null && materialBaseInfoId > 0) {
MaterialBaseInfoPO po = reservationStockInOrderMapper.getinfo(materialBaseInfoId);
if (po != null) {
return po;
if (StringUtils.isNotBlank(materialCode) && po.getMaterialCode() != null
&& !po.getMaterialCode().trim().equals(materialCode.trim())) {
log.warn("物料ID疑似误传:baseInfoId={} 按主键查到档案[{}({})],与明细编码[{}]不一致," +
"转库存反查/编码兜底(客户端可能把库存ID传进了materialBaseInfoId",
materialBaseInfoId, po.getMaterialCode(), po.getMaterialName(), materialCode);
byId = po;
} else {
return po;
}
}
}
// 2. baseId 无效但带了库存ID 反查库存表拿正确 baseInfoId修复"库存ID传进baseId"场景
@@ -50,8 +64,12 @@ public class MaterialBaseInfoResolveService {
}
// 3. 兜底按物料编码+组织查修复 baseId=0 漏传场景
if (StringUtils.isNotBlank(materialCode) && organizationId != null) {
return reservationStockInOrderMapper.getByCode(materialCode.trim(), organizationId);
MaterialBaseInfoPO po = reservationStockInOrderMapper.getByCode(materialCode.trim(), organizationId);
if (po != null) {
return po;
}
}
return null;
// 编码不一致已证明 baseId 不可信但库存反查/编码兜底都落空时退回按ID查到的档案保持原有兜底行为
return byId;
}
}
@@ -0,0 +1,204 @@
package com.mhd.oms.domain.outEntrustOrder.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.mhd.common.core.web.domain.BaseVOEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
* 出仓委托单对象 oms_out_entrust_order
* 表单字段对齐WMS出库管理新增(报关/委托信息块)
*
* @author rcx
* @date 2026-09-02
*/
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("oms_out_entrust_order")
public class OutEntrustOrder extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
@TableId(type = IdType.AUTO)
private Long id;
@ApiModelProperty("委托单号")
private String entrustNumber;
@ApiModelProperty("出库仓库ID")
private Long warehouseId;
@ApiModelProperty("仓库编码")
private String warehouseCode;
@ApiModelProperty("仓库名称")
private String warehouseName;
@ApiModelProperty("客户ID(货主)")
private Long shipperId;
@ApiModelProperty("客户编码")
private String shipperCode;
@ApiModelProperty("客户名称")
private String shipperName;
@ApiModelProperty("业务员ID")
private String salesmanId;
@ApiModelProperty("业务员名称")
private String salesmanName;
@ApiModelProperty("订单类型编码(字典outDocumentType)")
private String orderTypeCode;
@ApiModelProperty("订单类型名称")
private String orderTypeName;
@ApiModelProperty("结算币种")
private String settlementCurrency;
@ApiModelProperty("优先级别编码")
private String priorityLevelCode;
@ApiModelProperty("优先级别名称")
private String priorityLevelName;
@ApiModelProperty("是否直接出库: 1-是 2-否")
private Integer directWarehouse;
@ApiModelProperty("是否复核: 1-是 2-否")
private Integer review;
@ApiModelProperty("预计出货时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expectTime;
@ApiModelProperty("出仓日期")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date outboundDate;
@ApiModelProperty("orderDate")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date orderDate;
@ApiModelProperty("完成时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date completeTime;
@ApiModelProperty("To(收货人)")
@TableField("to_receiver")
private String toReceiver;
@ApiModelProperty("联系人")
private String contactPerson;
@ApiModelProperty("车型及车牌")
private String vehicleModelPlate;
@ApiModelProperty("委托编号(客户委托单号)")
private String entrustNo;
@ApiModelProperty("Tel电话")
private String tel;
@ApiModelProperty("Fax传真")
private String fax;
@ApiModelProperty("司机签名")
private String driverSign;
@ApiModelProperty("承运商编码")
private String carrier;
@ApiModelProperty("承运商名称")
private String carrierName;
@ApiModelProperty("柜号")
private String containerNo;
@ApiModelProperty("")
private String containerNoName;
@ApiModelProperty("运输方式编码")
private String transportModeCode;
@ApiModelProperty("运输方式名称")
private String transportModeName;
@ApiModelProperty("监管方式编码")
private String supervisionModeCode;
@ApiModelProperty("监管方式名称")
private String supervisionModeName;
@ApiModelProperty("申报地关区")
private String declareCustoms;
@ApiModelProperty("进出境关别")
private String entryExitCustoms;
@ApiModelProperty("起运/运抵国(地区)")
private String departureArrivalCountry;
@ApiModelProperty("海关是否查验货物")
private String customsInspectionFlag;
@ApiModelProperty("是否需要报关(预留,页面隐藏)")
private String needDeclareFlag;
@ApiModelProperty("是否需要运输(预留,页面隐藏)")
private String needTransportFlag;
@ApiModelProperty("生产商")
private String manufacturer;
@ApiModelProperty("报关员姓名及联系方式")
private String customsDeclarerInfo;
@ApiModelProperty("送货地址")
private String deliveryAddr;
@ApiModelProperty("附件url")
private String annexUrl;
@ApiModelProperty("订单状态:1-待审核 2-已审核 3-已驳回 4-进行中 5-已入库")
private Integer orderStatus;
@ApiModelProperty("审核人ID")
private Long auditBy;
@ApiModelProperty("审核人姓名")
private String auditByName;
@ApiModelProperty("审核时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date auditTime;
@ApiModelProperty("审核备注(驳回原因)")
private String auditRemark;
@ApiModelProperty("下发状态:1-未下发 2-已下发")
private Integer issueStatus;
@ApiModelProperty("下发人ID")
private Long issueBy;
@ApiModelProperty("下发人姓名")
private String issueByName;
@ApiModelProperty("下发时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date issueTime;
@ApiModelProperty("申报总数量(明细申报数量之和)")
private BigDecimal declareTotalQuantity;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("组织ID")
private Long organizationId;
@ApiModelProperty("组织名称")
private String organizationName;
@ApiModelProperty("一级组织ID")
private Long topOrganizationId;
}
@@ -0,0 +1,98 @@
package com.mhd.oms.domain.outEntrustOrder.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 lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 出仓委托单明细对象 oms_out_entrust_order_detail
* 明细列对齐WMS出库管理新增(出仓委托单明细列)
*
* @author rcx
* @date 2026-09-02
*/
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("oms_out_entrust_order_detail")
public class OutEntrustOrderDetail extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
@TableId(type = IdType.AUTO)
private Long id;
@ApiModelProperty("委托单号(关联主表)")
private String entrustNumber;
@ApiModelProperty("物料基础信息ID")
private Long materialBaseInfoId;
@ApiModelProperty("商品编码")
private String materialCode;
@ApiModelProperty("商品名称(物料档案名)")
private String materialName;
@ApiModelProperty("商品条形码")
private String barCode;
@ApiModelProperty("商品名称(委托单品名)")
private String commodityName;
@ApiModelProperty("商品编号")
private String commodityNo;
@ApiModelProperty("料号")
private String materialNo;
@ApiModelProperty("规格型号")
private String specificationModel;
@ApiModelProperty("SKU码")
private String skucode;
@ApiModelProperty("发票号")
private String invoiceNo;
@ApiModelProperty("申报单位")
private String declareUnit;
@ApiModelProperty("申报数量")
private BigDecimal declareQuantity;
@ApiModelProperty("单价")
private BigDecimal unitPrice;
@ApiModelProperty("单位")
private String unit;
@ApiModelProperty("")
private String liter;
@ApiModelProperty("尺寸")
private String size;
@ApiModelProperty("箱数")
private BigDecimal caseCount;
@ApiModelProperty("件数")
private BigDecimal pieceCount;
@ApiModelProperty("出库数量")
private BigDecimal outboundQuantity;
@ApiModelProperty("总净重(千克)")
private BigDecimal totalNetWeight;
@ApiModelProperty("总毛重(千克)")
private BigDecimal totalGrossWeight;
@ApiModelProperty("总体积(立方米)")
private BigDecimal totalVolume;
@ApiModelProperty("总面积")
private BigDecimal totalArea;
@ApiModelProperty("总金额")
private BigDecimal totalAmount;
@ApiModelProperty("币制")
private String currency;
@ApiModelProperty("批次")
private String batchRefNo;
@ApiModelProperty("单据参考号")
private String sheetRefNo;
@ApiModelProperty("原产国")
private String countryOfOrigin;
@ApiModelProperty("箱/托盘号")
private String boxPalletNo;
@ApiModelProperty("备注")
private String remark;
}
@@ -0,0 +1,42 @@
package com.mhd.oms.domain.outEntrustOrder.repository.facade;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrderDetail;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderDetailPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import java.util.List;
/**
* 出仓委托单明细Service接口
*
* @author rcx
* @date 2026-09-02
*/
public interface IOutEntrustOrderDetailService extends IService<OutEntrustOrderDetail> {
/**
* 根据委托单号查询有效明细
*/
List<OutEntrustOrderDetailPO> queryByEntrustNumber(String entrustNumber);
/**
* 根据委托单号集合查询有效明细
*/
List<OutEntrustOrderDetailPO> queryByEntrustNumbers(List<String> entrustNumbers);
/**
* 批量保存明细
*/
Boolean batchInsert(String entrustNumber, List<OutEntrustOrderDetailDO> detailDOList);
/**
* 根据委托单号逻辑删除明细
*/
Boolean deleteByEntrustNumber(String entrustNumber);
/**
* 根据委托单号集合逻辑删除明细
*/
Boolean deleteByEntrustNumbers(List<String> entrustNumbers);
}
@@ -0,0 +1,62 @@
package com.mhd.oms.domain.outEntrustOrder.repository.facade;
import com.baomidou.mybatisplus.extension.service.IService;
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 java.util.List;
/**
* 出仓委托单Service接口
*
* @author rcx
* @date 2026-09-02
*/
public interface IOutEntrustOrderService extends IService<OutEntrustOrder> {
/**
* 分页查询出仓委托单列表
*/
List<OutEntrustOrderPO> queryList(OutEntrustOrderDO outEntrustOrderDO);
/**
* 新增出仓委托单
*/
Boolean insert(OutEntrustOrderDO outEntrustOrderDO);
/**
* 修改出仓委托单
*/
Boolean update(OutEntrustOrderDO outEntrustOrderDO);
/**
* 批量删除出仓委托单
*/
Boolean delete(Long[] ids);
/**
* 获取出仓委托单详情(含明细)
*/
OutEntrustOrderPO getInfo(Long id);
/**
* 根据编号集合查询出仓委托单列表
*/
List<OutEntrustOrderPO> getInfoByIds(List<Long> ids);
/**
* 审核待审核/已驳回 已审核(2)或已驳回(3)
*/
Boolean audit(List<Long> ids, Integer targetStatus, Long auditBy, String auditByName, String auditRemark);
/**
* 下发置为已下发订单状态置为进行中
*/
Boolean issue(List<Long> ids, Long issueBy, String issueByName);
/**
* 完成进行中 已入库
*/
Boolean finish(List<Long> ids, Long updateBy, String updateByName);
}
@@ -0,0 +1,14 @@
package com.mhd.oms.domain.outEntrustOrder.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrderDetail;
/**
* 出仓委托单明细Mapper接口
*
* @author rcx
* @date 2026-09-02
*/
public interface OutEntrustOrderDetailMapper extends BaseMapper<OutEntrustOrderDetail> {
}
@@ -0,0 +1,28 @@
package com.mhd.oms.domain.outEntrustOrder.repository.mapper;
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 org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 出仓委托单Mapper接口
*
* @author rcx
* @date 2026-09-02
*/
public interface OutEntrustOrderMapper extends BaseMapper<OutEntrustOrder> {
/**
* 查询出仓委托单列表
*/
List<OutEntrustOrderPO> queryList(OutEntrustOrderDO outEntrustOrderDO);
/**
* 根据编号集合查询出仓委托单列表
*/
List<OutEntrustOrderPO> getInfoByIds(@Param("idList") List<Long> idList);
}
@@ -0,0 +1,97 @@
package com.mhd.oms.domain.outEntrustOrder.repository.persistence;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrderDetail;
import com.mhd.oms.domain.outEntrustOrder.repository.facade.IOutEntrustOrderDetailService;
import com.mhd.oms.domain.outEntrustOrder.repository.mapper.OutEntrustOrderDetailMapper;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderDetailPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 出仓委托单明细Service业务层处理
*
* @author rcx
* @date 2026-09-02
*/
@Service
public class OutEntrustOrderDetailImpl extends ServiceImpl<OutEntrustOrderDetailMapper, OutEntrustOrderDetail> implements IOutEntrustOrderDetailService {
/**
* 根据委托单号查询有效明细
*/
@Override
public List<OutEntrustOrderDetailPO> queryByEntrustNumber(String entrustNumber) {
List<OutEntrustOrderDetail> detailList = this.list(new LambdaQueryWrapper<OutEntrustOrderDetail>()
.eq(OutEntrustOrderDetail::getEntrustNumber, entrustNumber)
.eq(OutEntrustOrderDetail::getDelFlag, 1)
.orderByAsc(OutEntrustOrderDetail::getId));
return detailList.stream().map(detail -> {
OutEntrustOrderDetailPO detailPO = new OutEntrustOrderDetailPO();
BeanUtils.copyProperties(detail, detailPO);
return detailPO;
}).collect(Collectors.toList());
}
/**
* 根据委托单号集合查询有效明细
*/
@Override
public List<OutEntrustOrderDetailPO> queryByEntrustNumbers(List<String> entrustNumbers) {
if (entrustNumbers == null || entrustNumbers.isEmpty()) {
return new java.util.ArrayList<>();
}
List<OutEntrustOrderDetail> detailList = this.list(new LambdaQueryWrapper<OutEntrustOrderDetail>()
.in(OutEntrustOrderDetail::getEntrustNumber, entrustNumbers)
.eq(OutEntrustOrderDetail::getDelFlag, 1)
.orderByAsc(OutEntrustOrderDetail::getId));
return detailList.stream().map(detail -> {
OutEntrustOrderDetailPO detailPO = new OutEntrustOrderDetailPO();
BeanUtils.copyProperties(detail, detailPO);
return detailPO;
}).collect(Collectors.toList());
}
/**
* 批量保存明细
*/
@Override
public Boolean batchInsert(String entrustNumber, List<OutEntrustOrderDetailDO> detailDOList) {
List<OutEntrustOrderDetail> detailList = detailDOList.stream().map(detailDO -> {
OutEntrustOrderDetail detail = new OutEntrustOrderDetail();
BeanUtils.copyProperties(detailDO, detail);
detail.setEntrustNumber(entrustNumber);
detail.setDelFlag(1);
return detail;
}).collect(Collectors.toList());
return this.saveBatch(detailList);
}
/**
* 根据委托单号逻辑删除明细
*/
@Override
public Boolean deleteByEntrustNumber(String entrustNumber) {
return this.update(new LambdaUpdateWrapper<OutEntrustOrderDetail>()
.eq(OutEntrustOrderDetail::getEntrustNumber, entrustNumber)
.eq(OutEntrustOrderDetail::getDelFlag, 1)
.set(OutEntrustOrderDetail::getDelFlag, 2));
}
/**
* 根据委托单号集合逻辑删除明细
*/
@Override
public Boolean deleteByEntrustNumbers(List<String> entrustNumbers) {
return this.update(new LambdaUpdateWrapper<OutEntrustOrderDetail>()
.in(OutEntrustOrderDetail::getEntrustNumber, entrustNumbers)
.eq(OutEntrustOrderDetail::getDelFlag, 1)
.set(OutEntrustOrderDetail::getDelFlag, 2));
}
}
@@ -0,0 +1,159 @@
package com.mhd.oms.domain.outEntrustOrder.repository.persistence;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.constant.OutEntrustOrderConstant;
import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrder;
import com.mhd.oms.domain.outEntrustOrder.repository.facade.IOutEntrustOrderService;
import com.mhd.oms.domain.outEntrustOrder.repository.mapper.OutEntrustOrderMapper;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.system.api.model.LoginUser;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 出仓委托单Service业务层处理
*
* @author rcx
* @date 2026-09-02
*/
@Service
public class OutEntrustOrderImpl extends ServiceImpl<OutEntrustOrderMapper, OutEntrustOrder> implements IOutEntrustOrderService {
/**
* 查询出仓委托单列表
*/
@Override
public List<OutEntrustOrderPO> queryList(OutEntrustOrderDO outEntrustOrderDO) {
return baseMapper.queryList(outEntrustOrderDO);
}
/**
* 新增出仓委托单
*/
@Override
public Boolean insert(OutEntrustOrderDO outEntrustOrderDO) {
OutEntrustOrder outEntrustOrder = new OutEntrustOrder();
BeanUtils.copyProperties(outEntrustOrderDO, outEntrustOrder);
return this.save(outEntrustOrder);
}
/**
* 修改出仓委托单
*/
@Override
public Boolean update(OutEntrustOrderDO outEntrustOrderDO) {
OutEntrustOrder outEntrustOrder = new OutEntrustOrder();
BeanUtils.copyProperties(outEntrustOrderDO, outEntrustOrder);
return this.updateById(outEntrustOrder);
}
/**
* 批量删除出仓委托单(仅待审核/已驳回且未下发可删除)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean delete(Long[] ids) {
List<OutEntrustOrder> outEntrustOrders = this.listByIds(Arrays.asList(ids));
//只有待审核/已驳回且未下发的委托单才可以删除
List<OutEntrustOrder> unableDeleteList = outEntrustOrders.stream()
.filter(order -> !(OutEntrustOrderConstant.ORDER_STATUS_PENDING_AUDIT.equals(order.getOrderStatus())
|| OutEntrustOrderConstant.ORDER_STATUS_REJECTED.equals(order.getOrderStatus()))
|| !OutEntrustOrderConstant.ISSUE_STATUS_1.equals(order.getIssueStatus()))
.collect(Collectors.toList());
if (!unableDeleteList.isEmpty()) {
throw new ServiceException("已审核/进行中/已入库或已下发的委托单不允许删除");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Date now = new Date();
for (OutEntrustOrder outEntrustOrder : outEntrustOrders) {
outEntrustOrder.setDelFlag(2);
outEntrustOrder.setUpdateBy(loginUser == null ? null : loginUser.getUserid());
outEntrustOrder.setUpdateByName(loginUser == null ? null : loginUser.getUsername());
outEntrustOrder.setUpdateTime(now);
}
return this.updateBatchById(outEntrustOrders);
}
/**
* 获取出仓委托单详情
*/
@Override
public OutEntrustOrderPO getInfo(Long id) {
OutEntrustOrder outEntrustOrder = this.getOne(new LambdaQueryWrapper<OutEntrustOrder>()
.eq(OutEntrustOrder::getId, id).eq(OutEntrustOrder::getDelFlag, 1));
if (outEntrustOrder == null) {
throw new ServiceException("出仓委托单不存在");
}
OutEntrustOrderPO outEntrustOrderPO = new OutEntrustOrderPO();
BeanUtils.copyProperties(outEntrustOrder, outEntrustOrderPO);
return outEntrustOrderPO;
}
/**
* 根据编号集合查询出仓委托单列表
*/
@Override
public List<OutEntrustOrderPO> getInfoByIds(List<Long> ids) {
return baseMapper.getInfoByIds(ids);
}
/**
* 审核待审核/已驳回 已审核(2)或已驳回(3)
*/
@Override
public Boolean audit(List<Long> ids, Integer targetStatus, Long auditBy, String auditByName, String auditRemark) {
return this.update(new LambdaUpdateWrapper<OutEntrustOrder>()
.in(OutEntrustOrder::getId, ids)
.eq(OutEntrustOrder::getDelFlag, 1)
.set(OutEntrustOrder::getOrderStatus, targetStatus)
.set(OutEntrustOrder::getAuditBy, auditBy)
.set(OutEntrustOrder::getAuditByName, auditByName)
.set(OutEntrustOrder::getAuditTime, new Date())
.set(OutEntrustOrder::getAuditRemark, auditRemark)
.set(OutEntrustOrder::getUpdateBy, auditBy)
.set(OutEntrustOrder::getUpdateByName, auditByName)
.set(OutEntrustOrder::getUpdateTime, new Date()));
}
/**
* 下发下发状态置为已下发(订单状态保持已审核)
*/
@Override
public Boolean issue(List<Long> ids, Long issueBy, String issueByName) {
return this.update(new LambdaUpdateWrapper<OutEntrustOrder>()
.in(OutEntrustOrder::getId, ids)
.eq(OutEntrustOrder::getDelFlag, 1)
.set(OutEntrustOrder::getIssueStatus, OutEntrustOrderConstant.ISSUE_STATUS_2)
.set(OutEntrustOrder::getIssueBy, issueBy)
.set(OutEntrustOrder::getIssueByName, issueByName)
.set(OutEntrustOrder::getIssueTime, new Date())
.set(OutEntrustOrder::getUpdateBy, issueBy)
.set(OutEntrustOrder::getUpdateByName, issueByName)
.set(OutEntrustOrder::getUpdateTime, new Date()));
}
/**
* 完成已审核(已下发)/进行中 已入库
*/
@Override
public Boolean finish(List<Long> ids, Long updateBy, String updateByName) {
return this.update(new LambdaUpdateWrapper<OutEntrustOrder>()
.in(OutEntrustOrder::getId, ids)
.eq(OutEntrustOrder::getDelFlag, 1)
.set(OutEntrustOrder::getOrderStatus, OutEntrustOrderConstant.ORDER_STATUS_STORED)
.set(OutEntrustOrder::getUpdateBy, updateBy)
.set(OutEntrustOrder::getUpdateByName, updateByName)
.set(OutEntrustOrder::getUpdateTime, new Date()));
}
}
@@ -0,0 +1,100 @@
package com.mhd.oms.domain.outEntrustOrder.repository.po;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mhd.common.core.web.domain.BaseVOEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
* 出仓委托单明细对象 oms_out_entrust_order_detail
*
* @author rcx
* @date 2026-09-02
*/
@Data
@ApiModel(value = "出仓委托单明细对象", description = "出仓委托单明细响应对象")
public class OutEntrustOrderDetailPO extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
private Long id;
@ApiModelProperty("委托单号(关联主表)")
private String entrustNumber;
@ApiModelProperty("物料基础信息ID")
private Long materialBaseInfoId;
@ApiModelProperty("商品编码")
private String materialCode;
@ApiModelProperty("商品名称(物料档案名)")
private String materialName;
@ApiModelProperty("商品条形码")
private String barCode;
@ApiModelProperty("商品名称(委托单品名)")
private String commodityName;
@ApiModelProperty("商品编号")
private String commodityNo;
@ApiModelProperty("料号")
private String materialNo;
@ApiModelProperty("规格型号")
private String specificationModel;
@ApiModelProperty("SKU码")
private String skucode;
@ApiModelProperty("发票号")
private String invoiceNo;
@ApiModelProperty("申报单位")
private String declareUnit;
@ApiModelProperty("申报数量")
private BigDecimal declareQuantity;
@ApiModelProperty("单价")
private BigDecimal unitPrice;
@ApiModelProperty("单位")
private String unit;
@ApiModelProperty("")
private String liter;
@ApiModelProperty("尺寸")
private String size;
@ApiModelProperty("箱数")
private BigDecimal caseCount;
@ApiModelProperty("件数")
private BigDecimal pieceCount;
@ApiModelProperty("出库数量")
private BigDecimal outboundQuantity;
@ApiModelProperty("总净重(千克)")
private BigDecimal totalNetWeight;
@ApiModelProperty("总毛重(千克)")
private BigDecimal totalGrossWeight;
@ApiModelProperty("总体积(立方米)")
private BigDecimal totalVolume;
@ApiModelProperty("总面积")
private BigDecimal totalArea;
@ApiModelProperty("总金额")
private BigDecimal totalAmount;
@ApiModelProperty("币制")
private String currency;
@ApiModelProperty("批次")
private String batchRefNo;
@ApiModelProperty("单据参考号")
private String sheetRefNo;
@ApiModelProperty("原产国")
private String countryOfOrigin;
@ApiModelProperty("箱/托盘号")
private String boxPalletNo;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("创建时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -0,0 +1,212 @@
package com.mhd.oms.domain.outEntrustOrder.repository.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.mhd.common.core.web.domain.BaseVOEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 出仓委托单对象 oms_out_entrust_order
*
* @author rcx
* @date 2026-09-02
*/
@Data
@ApiModel(value = "出仓委托单对象", description = "出仓委托单响应对象")
public class OutEntrustOrderPO extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
@TableId(type = IdType.AUTO)
private Long id;
@ApiModelProperty("委托单号")
private String entrustNumber;
@ApiModelProperty("出库仓库ID")
private Long warehouseId;
@ApiModelProperty("仓库编码")
private String warehouseCode;
@ApiModelProperty("仓库名称")
private String warehouseName;
@ApiModelProperty("客户ID(货主)")
private Long shipperId;
@ApiModelProperty("客户编码")
private String shipperCode;
@ApiModelProperty("客户名称")
private String shipperName;
@ApiModelProperty("业务员ID")
private String salesmanId;
@ApiModelProperty("业务员名称")
private String salesmanName;
@ApiModelProperty("订单类型编码(字典outDocumentType)")
private String orderTypeCode;
@ApiModelProperty("订单类型名称")
private String orderTypeName;
@ApiModelProperty("结算币种")
private String settlementCurrency;
@ApiModelProperty("优先级别编码")
private String priorityLevelCode;
@ApiModelProperty("优先级别名称")
private String priorityLevelName;
@ApiModelProperty("是否直接出库: 1-是 2-否")
private Integer directWarehouse;
@ApiModelProperty("是否复核: 1-是 2-否")
private Integer review;
@ApiModelProperty("预计出货时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expectTime;
@ApiModelProperty("出仓日期")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date outboundDate;
@ApiModelProperty("orderDate")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date orderDate;
@ApiModelProperty("完成时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date completeTime;
@ApiModelProperty("To(收货人)")
@JsonProperty("to")
private String toReceiver;
@ApiModelProperty("联系人")
private String contactPerson;
@ApiModelProperty("车型及车牌")
private String vehicleModelPlate;
@ApiModelProperty("委托编号(客户委托单号)")
private String entrustNo;
@ApiModelProperty("Tel电话")
private String tel;
@ApiModelProperty("Fax传真")
private String fax;
@ApiModelProperty("司机签名")
private String driverSign;
@ApiModelProperty("承运商编码")
private String carrier;
@ApiModelProperty("承运商名称")
private String carrierName;
@ApiModelProperty("柜号")
private String containerNo;
@ApiModelProperty("")
private String containerNoName;
@ApiModelProperty("运输方式编码")
private String transportModeCode;
@ApiModelProperty("运输方式名称")
private String transportModeName;
@ApiModelProperty("监管方式编码")
private String supervisionModeCode;
@ApiModelProperty("监管方式名称")
private String supervisionModeName;
@ApiModelProperty("申报地关区")
private String declareCustoms;
@ApiModelProperty("进出境关别")
private String entryExitCustoms;
@ApiModelProperty("起运/运抵国(地区)")
private String departureArrivalCountry;
@ApiModelProperty("海关是否查验货物")
private String customsInspectionFlag;
@ApiModelProperty("是否需要报关(预留,页面隐藏)")
private String needDeclareFlag;
@ApiModelProperty("是否需要运输(预留,页面隐藏)")
private String needTransportFlag;
@ApiModelProperty("生产商")
private String manufacturer;
@ApiModelProperty("报关员姓名及联系方式")
private String customsDeclarerInfo;
@ApiModelProperty("送货地址")
private String deliveryAddr;
@ApiModelProperty("附件url")
private String annexUrl;
@ApiModelProperty("订单状态:1-待审核 2-已审核 3-已驳回 4-进行中 5-已入库")
private Integer orderStatus;
@ApiModelProperty("订单状态")
private String orderStatusStr;
@ApiModelProperty("审核人ID")
private Long auditBy;
@ApiModelProperty("审核人姓名")
private String auditByName;
@ApiModelProperty("审核时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date auditTime;
@ApiModelProperty("审核备注(驳回原因)")
private String auditRemark;
@ApiModelProperty("下发状态:1-未下发 2-已下发")
private Integer issueStatus;
@ApiModelProperty("下发状态")
private String issueStatusStr;
@ApiModelProperty("下发人ID")
private Long issueBy;
@ApiModelProperty("下发人姓名")
private String issueByName;
@ApiModelProperty("下发时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date issueTime;
@ApiModelProperty("申报总数量(明细申报数量之和)")
private BigDecimal declareTotalQuantity;
@ApiModelProperty("发票号(列表展示用,明细发票号逗号拼接;详情请看明细)")
private String invoiceNo;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("组织ID")
private Long organizationId;
@ApiModelProperty("组织名称")
private String organizationName;
@ApiModelProperty("一级组织ID")
private Long topOrganizationId;
@ApiModelProperty("商品明细")
private List<OutEntrustOrderDetailPO> materialDetailList;
}
@@ -0,0 +1,216 @@
package com.mhd.oms.domain.outEntrustOrder.repository.todo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mhd.common.core.web.domain.BaseVOEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 出仓委托单对象 oms_out_entrust_order
*
* @author rcx
* @date 2026-09-02
*/
@Data
public class OutEntrustOrderDO extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
private Long id;
@ApiModelProperty("编号集合")
private List<Long> idList;
@ApiModelProperty(name = "组织ID集合")
private List<Long> organizationIdList;
@ApiModelProperty(name = "权限菜单ID")
private Long permissionMenuId;
@ApiModelProperty("委托单号")
private String entrustNumber;
@ApiModelProperty("出库仓库ID")
private Long warehouseId;
@ApiModelProperty("仓库编码")
private String warehouseCode;
@ApiModelProperty("仓库名称")
private String warehouseName;
@ApiModelProperty("客户ID(货主)")
private Long shipperId;
@ApiModelProperty("客户编码")
private String shipperCode;
@ApiModelProperty("客户名称")
private String shipperName;
@ApiModelProperty("业务员ID")
private String salesmanId;
@ApiModelProperty("业务员名称")
private String salesmanName;
@ApiModelProperty("订单类型编码(字典outDocumentType)")
private String orderTypeCode;
@ApiModelProperty("订单类型名称")
private String orderTypeName;
@ApiModelProperty("结算币种")
private String settlementCurrency;
@ApiModelProperty("优先级别编码")
private String priorityLevelCode;
@ApiModelProperty("优先级别名称")
private String priorityLevelName;
@ApiModelProperty("是否直接出库: 1-是 2-否")
private Integer directWarehouse;
@ApiModelProperty("是否复核: 1-是 2-否")
private Integer review;
@ApiModelProperty("预计出货时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expectTime;
@ApiModelProperty("出仓日期")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date outboundDate;
@ApiModelProperty("orderDate")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date orderDate;
@ApiModelProperty("完成时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date completeTime;
@ApiModelProperty("To(收货人)")
private String toReceiver;
@ApiModelProperty("联系人")
private String contactPerson;
@ApiModelProperty("车型及车牌")
private String vehicleModelPlate;
@ApiModelProperty("委托编号(客户委托单号)")
private String entrustNo;
@ApiModelProperty("Tel电话")
private String tel;
@ApiModelProperty("Fax传真")
private String fax;
@ApiModelProperty("司机签名")
private String driverSign;
@ApiModelProperty("承运商编码")
private String carrier;
@ApiModelProperty("承运商名称")
private String carrierName;
@ApiModelProperty("柜号")
private String containerNo;
@ApiModelProperty("")
private String containerNoName;
@ApiModelProperty("运输方式编码")
private String transportModeCode;
@ApiModelProperty("运输方式名称")
private String transportModeName;
@ApiModelProperty("监管方式编码")
private String supervisionModeCode;
@ApiModelProperty("监管方式名称")
private String supervisionModeName;
@ApiModelProperty("申报地关区")
private String declareCustoms;
@ApiModelProperty("进出境关别")
private String entryExitCustoms;
@ApiModelProperty("起运/运抵国(地区)")
private String departureArrivalCountry;
@ApiModelProperty("海关是否查验货物")
private String customsInspectionFlag;
@ApiModelProperty("是否需要报关(预留,页面隐藏)")
private String needDeclareFlag;
@ApiModelProperty("是否需要运输(预留,页面隐藏)")
private String needTransportFlag;
@ApiModelProperty("生产商")
private String manufacturer;
@ApiModelProperty("报关员姓名及联系方式")
private String customsDeclarerInfo;
@ApiModelProperty("送货地址")
private String deliveryAddr;
@ApiModelProperty("附件url")
private String annexUrl;
@ApiModelProperty("订单状态:1-待审核 2-已审核 3-已驳回 4-进行中 5-已入库")
private Integer orderStatus;
@ApiModelProperty("审核人ID")
private Long auditBy;
@ApiModelProperty("审核人姓名")
private String auditByName;
@ApiModelProperty("审核时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date auditTime;
@ApiModelProperty("审核备注(驳回原因)")
private String auditRemark;
@ApiModelProperty("下发状态:1-未下发 2-已下发")
private Integer issueStatus;
@ApiModelProperty("下发人ID")
private Long issueBy;
@ApiModelProperty("下发人姓名")
private String issueByName;
@ApiModelProperty("下发时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date issueTime;
@ApiModelProperty("申报总数量(明细申报数量之和)")
private BigDecimal declareTotalQuantity;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("组织ID")
private Long organizationId;
@ApiModelProperty("组织名称")
private String organizationName;
@ApiModelProperty("一级组织ID")
private Long topOrganizationId;
@ApiModelProperty("查询-创建开始时间(年月日yyyy-MM-dd)")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date beginCreateTime;
@ApiModelProperty("查询-创建结束时间(年月日yyyy-MM-dd,含当天)")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date endCreateTime;
@ApiModelProperty("查询-发票号(模糊匹配明细发票号)")
private String invoiceNo;
@ApiModelProperty("商品明细")
private List<OutEntrustOrderDetailDO> materialDetailList;
}
@@ -0,0 +1,90 @@
package com.mhd.oms.domain.outEntrustOrder.repository.todo;
import com.mhd.common.core.web.domain.BaseVOEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* 出仓委托单明细对象 oms_out_entrust_order_detail
*
* @author rcx
* @date 2026-09-02
*/
@Data
public class OutEntrustOrderDetailDO extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
private Long id;
@ApiModelProperty("委托单号(关联主表)")
private String entrustNumber;
@ApiModelProperty("物料基础信息ID")
private Long materialBaseInfoId;
@ApiModelProperty("商品编码")
private String materialCode;
@ApiModelProperty("商品名称(物料档案名)")
private String materialName;
@ApiModelProperty("商品条形码")
private String barCode;
@ApiModelProperty("商品名称(委托单品名)")
private String commodityName;
@ApiModelProperty("商品编号")
private String commodityNo;
@ApiModelProperty("料号")
private String materialNo;
@ApiModelProperty("规格型号")
private String specificationModel;
@ApiModelProperty("SKU码")
private String skucode;
@ApiModelProperty("发票号")
private String invoiceNo;
@ApiModelProperty("申报单位")
private String declareUnit;
@ApiModelProperty("申报数量")
private BigDecimal declareQuantity;
@ApiModelProperty("单价")
private BigDecimal unitPrice;
@ApiModelProperty("单位")
private String unit;
@ApiModelProperty("")
private String liter;
@ApiModelProperty("尺寸")
private String size;
@ApiModelProperty("箱数")
private BigDecimal caseCount;
@ApiModelProperty("件数")
private BigDecimal pieceCount;
@ApiModelProperty("出库数量")
private BigDecimal outboundQuantity;
@ApiModelProperty("总净重(千克)")
private BigDecimal totalNetWeight;
@ApiModelProperty("总毛重(千克)")
private BigDecimal totalGrossWeight;
@ApiModelProperty("总体积(立方米)")
private BigDecimal totalVolume;
@ApiModelProperty("总面积")
private BigDecimal totalArea;
@ApiModelProperty("总金额")
private BigDecimal totalAmount;
@ApiModelProperty("币制")
private String currency;
@ApiModelProperty("批次")
private String batchRefNo;
@ApiModelProperty("单据参考号")
private String sheetRefNo;
@ApiModelProperty("原产国")
private String countryOfOrigin;
@ApiModelProperty("箱/托盘号")
private String boxPalletNo;
@ApiModelProperty("备注")
private String remark;
}
@@ -0,0 +1,177 @@
package com.mhd.oms.domain.outEntrustOrder.service;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.utils.OrderSequence;
import com.mhd.oms.constant.OutEntrustOrderConstant;
import com.mhd.oms.domain.outEntrustOrder.repository.facade.IOutEntrustOrderDetailService;
import com.mhd.oms.domain.outEntrustOrder.repository.facade.IOutEntrustOrderService;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderDetailPO;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 出仓委托单
*
* @author rcx
* @date 2026-09-02
*/
@Service
@Slf4j
public class OutEntrustOrderDomainService {
@Autowired
private IOutEntrustOrderService outEntrustOrderService;
@Autowired
private IOutEntrustOrderDetailService outEntrustOrderDetailService;
/**
* 分页查询出仓委托单列表
*/
public List<OutEntrustOrderPO> queryList(OutEntrustOrderDO outEntrustOrderDO) {
return outEntrustOrderService.queryList(outEntrustOrderDO);
}
/**
* 新增出仓委托单(主单+明细)
*/
@Transactional(rollbackFor = Exception.class)
public Boolean insert(OutEntrustOrderDO outEntrustOrderDO) {
//生成委托单号初始状态待审核未下发新单无审核信息
outEntrustOrderDO.setEntrustNumber(OrderSequence.getOrderCode(OutEntrustOrderConstant.OUT_ENTRUST_NO_PREFIX));
outEntrustOrderDO.setOrderStatus(OutEntrustOrderConstant.ORDER_STATUS_PENDING_AUDIT);
outEntrustOrderDO.setIssueStatus(OutEntrustOrderConstant.ISSUE_STATUS_1);
outEntrustOrderDO.setAuditRemark(null);
outEntrustOrderService.insert(outEntrustOrderDO);
outEntrustOrderDetailService.batchInsert(outEntrustOrderDO.getEntrustNumber(), buildDetailList(outEntrustOrderDO));
return true;
}
/**
* 修改出仓委托单(仅已创建且未下发可修改明细全量替换)
*/
/**
* 修改出仓委托单(仅待审核/已驳回且未下发可修改明细全量替换)
* 已驳回单重新编辑后状态重置为待审核待重新审核
*/
@Transactional(rollbackFor = Exception.class)
public Boolean update(OutEntrustOrderDO outEntrustOrderDO) {
OutEntrustOrderPO orderPO = checkEditable(outEntrustOrderDO.getId());
//已驳回 编辑后回到待审核待审核 保持不变(置null,updateById跳过该列)
outEntrustOrderDO.setOrderStatus(OutEntrustOrderConstant.ORDER_STATUS_REJECTED.equals(orderPO.getOrderStatus())
? OutEntrustOrderConstant.ORDER_STATUS_PENDING_AUDIT : null);
outEntrustOrderDO.setEntrustNumber(orderPO.getEntrustNumber());
outEntrustOrderService.update(outEntrustOrderDO);
//明细全量替换逻辑删除旧明细后重新保存
outEntrustOrderDetailService.deleteByEntrustNumber(outEntrustOrderDO.getEntrustNumber());
outEntrustOrderDetailService.batchInsert(outEntrustOrderDO.getEntrustNumber(), buildDetailList(outEntrustOrderDO));
return true;
}
/**
* 批量删除出仓委托单(仅待审核/已驳回且未下发可删除)
*/
@Transactional(rollbackFor = Exception.class)
public Boolean delete(Long[] ids) {
List<OutEntrustOrderPO> orderList = outEntrustOrderService.getInfoByIds(java.util.Arrays.stream(ids).collect(Collectors.toList()));
List<String> entrustNumbers = orderList.stream().map(OutEntrustOrderPO::getEntrustNumber).collect(Collectors.toList());
outEntrustOrderService.delete(ids);
outEntrustOrderDetailService.deleteByEntrustNumbers(entrustNumbers);
return true;
}
/**
* 获取出仓委托单详情(含明细)
*/
public OutEntrustOrderPO getInfo(Long id) {
OutEntrustOrderPO outEntrustOrderPO = outEntrustOrderService.getInfo(id);
List<OutEntrustOrderDetailPO> detailList = outEntrustOrderDetailService.queryByEntrustNumber(outEntrustOrderPO.getEntrustNumber());
outEntrustOrderPO.setMaterialDetailList(detailList);
return outEntrustOrderPO;
}
/**
* 批量审核待审核/已驳回 已审核(2)或已驳回(3)
*/
public Boolean audit(List<Long> ids, Integer targetStatus, Long auditBy, String auditByName, String auditRemark) {
return outEntrustOrderService.audit(ids, targetStatus, auditBy, auditByName, auditRemark);
}
/**
* 批量下发订单状态保持已审核下发状态置为已下发(WMS侧单据由ApplicationService推送)
*/
public Boolean issue(List<Long> ids, Long issueBy, String issueByName) {
return outEntrustOrderService.issue(ids, issueBy, issueByName);
}
/**
* 完成已审核(已下发) 已入库(OMS人工推进)
*/
public Boolean finish(List<Long> ids, Long updateBy, String updateByName) {
return outEntrustOrderService.finish(ids, updateBy, updateByName);
}
/**
* 根据委托单号查询明细
*/
public List<OutEntrustOrderDetailPO> getDetailByEntrustNumber(String entrustNumber) {
return outEntrustOrderDetailService.queryByEntrustNumber(entrustNumber);
}
/**
* 根据委托单号集合查询明细
*/
public List<OutEntrustOrderDetailPO> getDetailByEntrustNumbers(List<String> entrustNumbers) {
return outEntrustOrderDetailService.queryByEntrustNumbers(entrustNumbers);
}
/**
* 根据编号集合查询出仓委托单列表
*/
public List<OutEntrustOrderPO> queryByIds(List<Long> ids) {
return outEntrustOrderService.getInfoByIds(ids);
}
/**
* 明细补充审计信息(取主单的创建/更新人)
*/
private List<OutEntrustOrderDetailDO> buildDetailList(OutEntrustOrderDO outEntrustOrderDO) {
List<OutEntrustOrderDetailDO> detailDOList = outEntrustOrderDO.getMaterialDetailList();
for (OutEntrustOrderDetailDO detailDO : detailDOList) {
detailDO.setCreateBy(outEntrustOrderDO.getCreateBy());
detailDO.setCreateByName(outEntrustOrderDO.getCreateByName());
detailDO.setCreateTime(outEntrustOrderDO.getCreateTime());
detailDO.setUpdateBy(outEntrustOrderDO.getUpdateBy());
detailDO.setUpdateByName(outEntrustOrderDO.getUpdateByName());
detailDO.setUpdateTime(outEntrustOrderDO.getUpdateTime());
detailDO.setDelFlag(1);
}
return detailDOList;
}
/**
* 校验委托单是否可编辑/删除仅待审核/已驳回 且未下发
*
* @return 委托单信息(供调用方复用当前状态/委托单号)
*/
private OutEntrustOrderPO checkEditable(Long id) {
OutEntrustOrderPO outEntrustOrderPO = outEntrustOrderService.getInfo(id);
boolean editableStatus = OutEntrustOrderConstant.ORDER_STATUS_PENDING_AUDIT.equals(outEntrustOrderPO.getOrderStatus())
|| OutEntrustOrderConstant.ORDER_STATUS_REJECTED.equals(outEntrustOrderPO.getOrderStatus());
if (!editableStatus) {
throw new ServiceException("已审核/进行中/已入库的委托单不允许修改");
}
if (!OutEntrustOrderConstant.ISSUE_STATUS_1.equals(outEntrustOrderPO.getIssueStatus())) {
throw new ServiceException("已下发的委托单不允许修改");
}
return outEntrustOrderPO;
}
}
@@ -0,0 +1,37 @@
package com.mhd.oms.domain.outboundRank.repository.facade;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundDetailRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import java.util.List;
/**
* 出库排名统计Service接口
*
* @author rcx
* @date 2026-08-31
*/
public interface IExecutionOutboundRankService {
/**
* 商品出库排名列表货品聚合
*/
List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 商品出库排名合计不分页
*/
ExecutionOutboundDetailRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名列表线路×货品聚合
*/
List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名合计不分页
*/
ExecutionOutboundDetailRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
}
@@ -0,0 +1,37 @@
package com.mhd.oms.domain.outboundRank.repository.mapper;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundDetailRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import java.util.List;
/**
* 出库排名统计Mapper接口
*
* @author rcx
* @date 2026-08-31
*/
public interface ExecutionOutboundRankMapper {
/**
* 商品出库排名列表一行=一个货品跨单汇总按实际出库量降序
*/
List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 商品出库排名合计不分页当前筛选条件全量合计
*/
ExecutionOutboundDetailRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名列表一行=线路×货品按实际出库量降序仅统计带线路的单
*/
List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名合计不分页当前筛选条件全量合计
*/
ExecutionOutboundDetailRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
}
@@ -0,0 +1,45 @@
package com.mhd.oms.domain.outboundRank.repository.persistence;
import com.mhd.oms.domain.outboundRank.repository.facade.IExecutionOutboundRankService;
import com.mhd.oms.domain.outboundRank.repository.mapper.ExecutionOutboundRankMapper;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundDetailRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 出库排名统计Service业务层处理
*
* @author rcx
* @date 2026-08-31
*/
@Service
public class ExecutionOutboundRankImpl implements IExecutionOutboundRankService {
@Autowired
private ExecutionOutboundRankMapper executionOutboundRankMapper;
@Override
public List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryMaterialRankList(executionOutboundRankDO);
}
@Override
public ExecutionOutboundDetailRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryMaterialRankTotal(executionOutboundRankDO);
}
@Override
public List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryLineRankList(executionOutboundRankDO);
}
@Override
public ExecutionOutboundDetailRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryLineRankTotal(executionOutboundRankDO);
}
}
@@ -0,0 +1,39 @@
package com.mhd.oms.domain.outboundRank.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>当前筛选条件下不分页的全量合计随分页列表一起返回
* totalPieceCount/totalGrossWeightTon/totalVolume/totalOrderCount/totalLineCount</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "出库排名统计合计对象", description = "出库排名统计合计响应对象")
public class ExecutionOutboundDetailRankTotalPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("件数合计")
private BigDecimal totalPieceCount;
@ApiModelProperty("毛重(吨)合计")
private BigDecimal totalGrossWeightTon;
@ApiModelProperty("体积(CBM)合计")
private BigDecimal totalVolume;
@ApiModelProperty("仓单数合计(去重出库单数)")
private Long totalOrderCount;
@ApiModelProperty("线路数合计(去重线路数,线路排名使用)")
private Long totalLineCount;
}
@@ -0,0 +1,59 @@
package com.mhd.oms.domain.outboundRank.repository.po;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 线路出库排名PO一行 = 线路×货品日期区间内汇总
*
* <p>按线路+物料编码合并多张出库单合并为一行单据级字段种类/仓单编号/入库单号/客户编号/收货人/出仓日期不提供
* 仅统计带线路的出库单line_code 2026-08-20 新增冗余字段历史单为空不参与线路排名
* 件数 = SUM(CHECK_QUANTITY) 复核确认的实际出库量与入库排名的实际上架量对称
* 退保总额为占位字段后端暂无对应数据来源恒为 null</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "线路出库排名对象", description = "线路出库排名响应对象")
public class ExecutionOutboundLineRankPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("线路编码")
@Excel(name = "线路编码")
private String lineCode;
@ApiModelProperty("线路")
@Excel(name = "线路")
private String lineName;
@ApiModelProperty("商品货号")
@Excel(name = "商品货号")
private String materialCode;
@ApiModelProperty("货名")
@Excel(name = "货名")
private String materialName;
@ApiModelProperty("件数(复核确认的实际出库量合计,与入库排名的实际上架量对称)")
@Excel(name = "件数")
private BigDecimal pieceCount;
@ApiModelProperty("毛重(吨)")
@Excel(name = "毛重(吨)")
private BigDecimal grossWeightTon;
@ApiModelProperty("体积(CBM)")
@Excel(name = "体积(CBM)")
private BigDecimal totalVolume;
@ApiModelProperty("退保总额(占位字段,后端暂无数据来源,恒为 null)")
@Excel(name = "退保总额")
private BigDecimal depositRefundAmount;
}
@@ -0,0 +1,50 @@
package com.mhd.oms.domain.outboundRank.repository.po;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 商品出库排名PO一行 = 一个货品日期区间内跨出库单汇总
*
* <p>按物料编码合并一个货品会走多条线路/多张单单据级字段种类/仓单编号/入库单号/客户编号/收货人/出仓日期
* 线路字段均无单值可返不提供件数 = SUM(CHECK_QUANTITY) 复核确认的实际出库量与入库排名的实际上架量对称
* 退保总额为占位字段后端暂无对应数据来源恒为 null</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "商品出库排名对象", description = "商品出库排名响应对象")
public class ExecutionOutboundMaterialRankPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("商品货号")
@Excel(name = "商品货号")
private String materialCode;
@ApiModelProperty("货名")
@Excel(name = "货名")
private String materialName;
@ApiModelProperty("件数(复核确认的实际出库量合计,与入库排名的实际上架量对称)")
@Excel(name = "件数")
private BigDecimal pieceCount;
@ApiModelProperty("毛重(吨)")
@Excel(name = "毛重(吨)")
private BigDecimal grossWeightTon;
@ApiModelProperty("体积(CBM)")
@Excel(name = "体积(CBM)")
private BigDecimal totalVolume;
@ApiModelProperty("退保总额(占位字段,后端暂无数据来源,恒为 null)")
@Excel(name = "退保总额")
private BigDecimal depositRefundAmount;
}
@@ -0,0 +1,56 @@
package com.mhd.oms.domain.outboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 出库排名统计明细查询DO
*
* <p>topOrganizationId 由服务端按登录用户注入前端不传</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionOutboundRankDO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("出库日期-开始 yyyy-MM-dd(必填)")
private String createTimeStart;
@ApiModelProperty("出库日期-结束 yyyy-MM-dd(必填)")
private String createTimeEnd;
@ApiModelProperty("仓单编号/出库单号(模糊)")
private String outOrderNumber;
@ApiModelProperty("入库单号(模糊)")
private String inOrderNumber;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("货名/商品名称(模糊)")
private String materialName;
@ApiModelProperty("线路编码(模糊)")
private String lineCode;
@ApiModelProperty("线路名称(模糊)")
private String lineName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
@ApiModelProperty("客户名称(模糊)")
private String customerName;
@ApiModelProperty("货主名称(模糊)")
private String shipperName;
@ApiModelProperty("一级组织表ID(登录用户隔离,非1时注入)")
private Long topOrganizationId;
}
@@ -0,0 +1,54 @@
package com.mhd.oms.domain.outboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 出库排名统计明细入参DTO
*
* <p>按日期区间筛选出库明细列表一行 = 仓单×货品件数/毛重/体积按行汇总件数降序
* OMS 库出库单状态不流转"已出库"数据数据源跨库取 WMS 库出库表 WMS /outboundRankApi 同口径</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionOutboundRankDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("出库日期-开始 yyyy-MM-dd(必填,按出库单创建时间筛选)")
private String createTimeStart;
@ApiModelProperty("出库日期-结束 yyyy-MM-dd(必填,按出库单创建时间筛选)")
private String createTimeEnd;
@ApiModelProperty("仓单编号/出库单号(模糊)")
private String outOrderNumber;
@ApiModelProperty("入库单号(模糊)")
private String inOrderNumber;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("货名/商品名称(模糊)")
private String materialName;
@ApiModelProperty("线路编码(模糊)")
private String lineCode;
@ApiModelProperty("线路名称(模糊)")
private String lineName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
@ApiModelProperty("客户名称(模糊)")
private String customerName;
@ApiModelProperty("货主名称(模糊)")
private String shipperName;
}
@@ -0,0 +1,93 @@
package com.mhd.oms.domain.outboundRank.service;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.utils.StringUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundDetailRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 出库排名统计应用服务
*
* <p>负责登录用户组织隔离注入日期区间校验查询逻辑在 ExecutionOutboundRankDomainService ExecutionOutboundRankMapper.xml跨库取 WMS 出库数据</p>
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionOutboundRankApplicationService {
@Autowired
private ExecutionOutboundRankDomainService executionOutboundRankDomainService;
/**
* 分页查询商品出库排名列表货品聚合
*/
public List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryMaterialRankList(executionOutboundRankDO);
}
/**
* 商品出库排名合计当前筛选条件全量不分页
*/
public ExecutionOutboundDetailRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankDomainService.queryMaterialRankTotal(executionOutboundRankDO);
}
/**
* 导出商品出库排名不分页全量
*/
public List<ExecutionOutboundMaterialRankPO> exportMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryMaterialRankList(executionOutboundRankDO);
}
/**
* 分页查询线路出库排名列表线路×货品聚合
*/
public List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryLineRankList(executionOutboundRankDO);
}
/**
* 线路出库排名合计当前筛选条件全量不分页
*/
public ExecutionOutboundDetailRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankDomainService.queryLineRankTotal(executionOutboundRankDO);
}
/**
* 导出线路出库排名不分页全量
*/
public List<ExecutionOutboundLineRankPO> exportLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryLineRankList(executionOutboundRankDO);
}
/**
* 注入登录用户组织隔离并校验日期区间必填
*/
private void fillLoginContext(ExecutionOutboundRankDO executionOutboundRankDO) {
if (StringUtils.isEmpty(executionOutboundRankDO.getCreateTimeStart()) || StringUtils.isEmpty(executionOutboundRankDO.getCreateTimeEnd())) {
throw new ServiceException("请选择统计日期区间");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1) {
executionOutboundRankDO.setTopOrganizationId(topOrganizationId);
}
}
}
}
@@ -0,0 +1,36 @@
package com.mhd.oms.domain.outboundRank.service;
import cn.hutool.core.util.ObjectUtil;
import com.mhd.common.core.exception.digitalLogisticsException.DigitalLogisticsException;
import com.mhd.common.core.exception.digitalLogisticsException.UserError;
import com.mhd.common.core.utils.IgnoreNullUtil;
import com.mhd.common.core.utils.bean.BeanUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDTO;
import org.springframework.stereotype.Component;
/**
* 出库排名统计Assembler
*
* @author rcx
* @date 2026-08-31
*/
@Component
public class ExecutionOutboundRankAssembler {
/**
* 转换实体
*/
public ExecutionOutboundRankDO toDO(ExecutionOutboundRankDTO executionOutboundRankDTO) {
ExecutionOutboundRankDO executionOutboundRankDO = new ExecutionOutboundRankDO();
// 拷贝
BeanUtils.copyProperties(executionOutboundRankDTO, executionOutboundRankDO, IgnoreNullUtil.getNullPropertyNames(executionOutboundRankDTO));
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
return executionOutboundRankDO;
}
}
@@ -0,0 +1,54 @@
package com.mhd.oms.domain.outboundRank.service;
import com.mhd.oms.domain.outboundRank.repository.facade.IExecutionOutboundRankService;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundDetailRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 出库排名统计领域服务
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionOutboundRankDomainService {
@Autowired
private IExecutionOutboundRankService executionOutboundRankService;
/**
* 商品出库排名列表货品聚合
*/
public List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryMaterialRankList(executionOutboundRankDO);
}
/**
* 商品出库排名合计不分页
*/
public ExecutionOutboundDetailRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryMaterialRankTotal(executionOutboundRankDO);
}
/**
* 线路出库排名列表线路×货品聚合
*/
public List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryLineRankList(executionOutboundRankDO);
}
/**
* 线路出库排名合计不分页
*/
public ExecutionOutboundDetailRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryLineRankTotal(executionOutboundRankDO);
}
}
@@ -210,6 +210,12 @@ public class ReservationMaterialInventory extends BaseVOEntity {
@Excel(name = "存货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date inventoryDate;
@ApiModelProperty("进货日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "进货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date purchaseDate;
@ApiModelProperty("ExtAttr3")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -2,6 +2,7 @@ package com.mhd.oms.domain.reservationMaterialInventory.repository.facade;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialInventoryParamDO;
@@ -113,4 +114,9 @@ public interface IReservationMaterialInventoryService extends IService<Reservati
public void xgkc(MaterialInventoryOmsParamDO materialInventoryParamDO);
public void kctzjl(MaterialInventoryOmsParamDO materialInventoryParamDO,String adjustAction,LoginUser loginUser);
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号入库业务单新增页面下拉回显
*/
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode, Long topOrganizationId);
}
@@ -2,6 +2,7 @@ package com.mhd.oms.domain.reservationMaterialInventory.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBarCodePO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBaseInfo;
@@ -36,4 +37,7 @@ public interface ReservationMaterialInventoryMapper extends BaseMapper<Reservati
public MaterialBaseInfo getByInfoId(@Param("id") Long id);
public List<MaterialBarCodePO> getInfoByMaterialBaseInfoIds(@Param("id") Long id);
//入库业务单新增根据物料编号查询库存批次去重后的过期日期+批次号最近5条供前端下拉回显
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(@Param("materialCode") String materialCode, @Param("topOrganizationId") Long topOrganizationId);
}
@@ -13,6 +13,7 @@ import com.mhd.oms.domain.reservationInventoryAdjustmentRecord.repository.mapper
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService;
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBarCodePO;
@@ -120,6 +121,14 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
return materialInventoryPO;
}
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号入库业务单新增页面下拉回显
*/
@Override
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode, Long topOrganizationId) {
return materialInventoryMapper.selectRecentExpiryBatches(materialCode, topOrganizationId);
}
@Override
public Boolean omsEdit(MaterialInventoryOmsParamDO materialInventoryOmsParamDO) {
String inOrderNumber = materialInventoryOmsParamDO.getInOrderNumber();
@@ -127,6 +136,9 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
String batchNumber = materialInventoryOmsParamDO.getBatchNumber();
Long materialBaseInfoId = materialInventoryOmsParamDO.getMaterialBaseInfoId();
Long warehouseId = materialInventoryOmsParamDO.getWarehouseId();
// 进货日期参与行匹配WMS 库存行按批次属性(含进货日期)分行后OMS 同步按进货日期区分避免不同进货日期误合并
// 为空时匹配 purchase_date 为空的行存量行为不变
Date purchaseDate = materialInventoryOmsParamDO.getPurchaseDate();
LambdaQueryWrapper<ReservationMaterialInventory> param = new LambdaQueryWrapper<ReservationMaterialInventory>()
// .eq(ReservationMaterialInventory::getInOrderNumber, inOrderNumber)
//.eq(ReservationMaterialInventory::getLotNumber, lotNumber)
@@ -148,6 +160,11 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
} else {
param.isNull(ReservationMaterialInventory::getBatchNumber);
}
if (purchaseDate != null) {
param.eq(ReservationMaterialInventory::getPurchaseDate, purchaseDate);
} else {
param.isNull(ReservationMaterialInventory::getPurchaseDate);
}
List<ReservationMaterialInventory> reservationMaterialInventories = materialInventoryMapper.selectList(param);
if (reservationMaterialInventories != null && reservationMaterialInventories.size() > 0) {
ReservationMaterialInventory reservationMaterialInventory = reservationMaterialInventories.get(0);
@@ -542,6 +559,7 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
// materialInventory.setExtAttr4(materialInventoryParamDO.getExtAttr4());
materialInventory.setExpiryDate(materialInventoryParamDO.getExpiryDate());
// materialInventory.setInventoryDate(materialInventoryParamDO.getInventoryDate());
materialInventory.setPurchaseDate(materialInventoryParamDO.getPurchaseDate());
materialInventory.setBatchNumber(materialInventoryParamDO.getBatchNumber());
String batchNo = materialInventoryParamDO.getBatchNo();
if (batchNo != null) {
@@ -0,0 +1,25 @@
package com.mhd.oms.domain.reservationMaterialInventory.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 物料库存批次过期日期对象入库业务单新增页面下拉回显
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "物料库存批次过期日期", description = "物料库存批次过期日期响应对象")
public class MaterialInventoryExpiryBatchPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("过期日期 yyyy-MM-dd")
private String expiryDate;
@ApiModelProperty("批次号")
private String batchNumber;
}
@@ -174,34 +174,41 @@ public class ReservationMaterialInventoryPO extends MaterialBasePO {
@Excel(name = "扩展字段2")
private String extAttr2;
// 以下业务日期字段只保留年月日导出复用 /list 接口前端取 JSON 值直接进 Excel去掉时分秒 WMS 库存查询一致
@ApiModelProperty("生产日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "生产日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "生产日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date productionDate;
@ApiModelProperty("过期日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "过期日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "过期日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date expiryDate;
@ApiModelProperty("存货日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "存货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "存货日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date inventoryDate;
@ApiModelProperty("ExtAttr3")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@ApiModelProperty("进货日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "ExtAttr3", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "进货日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date purchaseDate;
@ApiModelProperty("ExtAttr3")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "ExtAttr3", width = 30, dateFormat = "yyyy-MM-dd")
private Date extAttr3;
@ApiModelProperty("ExtAttr4")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "ExtAttr4", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "ExtAttr4", width = 30, dateFormat = "yyyy-MM-dd")
private Date extAttr4;
@@ -225,4 +232,19 @@ public class ReservationMaterialInventoryPO extends MaterialBasePO {
@ApiModelProperty("LOT编号")
@Excel(name = "LOT编号")
private String lotNumber;
// 入库时间(createTime)/更新时间(updateTime)继承自 BaseVOEntity默认带时分秒
// 库存查询导出复用 /list 接口前端取 JSON 值直接进 Excel这里覆盖 getter 只保留年月日
// 不改 BaseVOEntity全系统基类其他模块时间列仍需时分秒仅收窄本 PO 的序列化格式 WMS 库存查询一致
@Override
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
public Date getCreateTime() {
return super.getCreateTime();
}
@Override
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
public Date getUpdateTime() {
return super.getUpdateTime();
}
}
@@ -217,6 +217,12 @@ public class ReservationMaterialInventoryDO extends MaterialBaseDO {
@Excel(name = "存货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date inventoryDate;
@ApiModelProperty("进货日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "进货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date purchaseDate;
@ApiModelProperty("ExtAttr3")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -253,9 +259,18 @@ public class ReservationMaterialInventoryDO extends MaterialBaseDO {
private String createTimeEnd;
private String expiryDateStr;
private String productionDateStr;
private String purchaseDateStr;
private String expiryDateStart;
private String expiryDateEnd;
private String purchaseDateStart;
private String purchaseDateEnd;
@ApiModelProperty("导出按到期日分组排序标识:仅 queryRule=wlExpiry 时生效,true-到期日倒序(日期近的在前、空值最后、0库存靠后);不传-保持原有排序")
private Boolean orderByExpiryDate;
@ApiModelProperty("导出排除库存为0标识:true-排除库存数量汇总为0的数据(所有查询维度 queryRule 均生效);不传-不过滤")
private Boolean excludeZeroInventoryQuantity;
@ApiModelProperty("导出标识:导出与查询共用 /list,true-本次为导出请求,启用导出融合排序(到期日倒序优先→同到期日按物料编码升序→0库存靠后→更新时间倒序);不传-保持原有排序")
private Boolean isExport;
}
@@ -193,5 +193,7 @@ public class ReservationMaterialInventoryDTO extends MaterialBaseDTO {
private String createTimeEnd;
private String expiryDate;
private String productionDate;
/** 进货日期(yyyy-MM-dd,单日精确查询) */
private String purchaseDate;
}
@@ -8,6 +8,7 @@ import com.mhd.common.core.utils.bean.BeanUtils;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBarCodePO;
@@ -185,6 +186,22 @@ public class ReservationMaterialInventoryApplicationService {
return materialInventoryDomainService.getInventoryByStorageLocationIdAndMaterialBaseInfoId(storageLocationId, materialBaseInfoId);
}
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号入库业务单新增页面下拉回显
* queryList 一致按登录用户一级组织(租户)过滤一级组织(平台)不限制
*/
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode) {
Long topOrganizationId = null;
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long loginTopOrgId = loginUser.getUserPo().getTopOrganizationId();
if (loginTopOrgId != null && loginTopOrgId != 1) {
topOrganizationId = loginTopOrgId;
}
}
return materialInventoryDomainService.selectRecentExpiryBatches(materialCode, topOrganizationId);
}
/**
* @description:库存查询列表
* @param materialInventoryQueryListDO
@@ -3,6 +3,7 @@ package com.mhd.oms.domain.reservationMaterialInventory.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.ReservationMaterialInventoryDO;
@@ -88,6 +89,13 @@ public class ReservationMaterialInventoryDomainService {
return materialInventory.getInventoryQuantity();
}
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号入库业务单新增页面下拉回显
*/
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode, Long topOrganizationId) {
return materialInventoryService.selectRecentExpiryBatches(materialCode, topOrganizationId);
}
/**
* @description:库存查询列表
* @param materialInventoryQueryListDO
@@ -6,6 +6,7 @@ import com.mhd.oms.domain.reservationStockInOrder.entity.ReservationStockInOrder
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.vo.DeliveryDataDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.SheetImportResultDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.StockInDataDTO;
import org.springframework.web.multipart.MultipartFile;
@@ -50,22 +51,26 @@ public interface IReservationStockInOrderService extends IService<ReservationSto
List<ReservationInMaterialDetailPO> queryNoticePrintDetail(ReservationStockInOrderDO stockInOrderDTO);
/**
* 解析Excel文件并获取出库数据
* 解析Excel文件并导入出库单多页签文件中每个工作表独立解析为一张出库单单页签文件行为不变
* 任一页签校验失败则整体不落库报错聚合列出所有失败页签
* @param file Excel文件
* @param sheetName 工作表名称出库导入/参考数据
* @return 出库数据DTO
* @param sheetName 兼容参数多页签模式下不再按名字筛选传空即可
* @param warehouseId 当前仓库id非空时校验导入物料是否属于该仓库并按该仓库过滤批次库存
* @return 每个工作表的导入结果含生成的出库单号与解析数据
* @throws Exception 解析异常
*/
DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName) throws Exception;
List<SheetImportResultDTO> parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception;
/**
* 解析入库单Excel文件
* 解析并导入入库单Excel多页签未指定页签名时文件中每个工作表独立解析为一张入库单单页签文件行为不变
* 任一页签校验失败则整体不落库报错聚合列出所有失败页签
* @param file Excel文件
* @param sheetName 工作表名称默认入库模版
* @return 入库单完整数据表头+明细
* @param sheetName 显式指定页签时保持单页签行为不传/为空则为多页签模式
* @return 每个工作表的导入结果含生成的入库单号与解析数据
* @throws Exception 解析异常
*/
StockInDataDTO parseStockInExcel(MultipartFile file, String sheetName) throws Exception;
List<SheetImportResultDTO> parseStockInExcel(MultipartFile file, String sheetName) throws Exception;
//StockInDataDTO parseStockInExcel(MultipartFile file, String sheetName) throws Exception;
public List<ReservationStockInOrderPO> queryPushBmsList();
}
@@ -21,20 +21,47 @@ import java.util.Map;
/**
* 维他奶出库Excel解析监听器新模板版
* 新模板特点货品明细与客户配送明细左右分栏展示
* 核心功能
* 1. 左侧栏第1-3行解析表头送货日期/送货线号/车次第5行解析纪录合共第7行起解析货品明细
* 2. 右侧栏第2行解析发票张数/小计第4行起解析客户配送明细
* 3. 自动处理文本型数字转换StringInteger/BigDecimal
* 4. 完整的异常处理和日志记录
* 核心功能行列位置均不写死模板调整行/列顺序不影响解析
* 1. 表头标签送货日期/送货线号/车次/发票张数/小计/纪录合共按标签文字定位标签所在列取右侧相邻列的值
* 2. 货品明细按列头文字貨品編號/簡稱//支等繁简体归一化定位各列列头行之后的行按数据解析
* 3. 客户配送明细按列头文字類别/發票編號/客户編號/名稱///金額等定位各列
* 4. 列头行识别失败时兜底按老模板固定布局左栏A-D列右栏F-L列行号1-7解析保证老模板不受影响
* 5. 自动处理文本型数字转换StringInteger/BigDecimal
*/
public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<String, Object>> {
// 日志实例
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDeliveryExcelListener.class);
// 关键字定义Excel标签匹配
private static final String KEYWORD_TOTAL_RECORD = "紀錄合共";
private static final String KEYWORD_INVOICE_COUNT = "發票張數";
private static final String KEYWORD_SUBTOTAL = "小計";
// ---------------------- 关键字/列头别名定义归一化后匹配兼容简繁体 ----------------------
// 表头标签关键字
private static final String[] KEYWORD_DELIVERY_DATE = {"送货日期"};
private static final String[] KEYWORD_DELIVERY_LINE = {"送货线号", "送货线路"};
private static final String[] KEYWORD_TRAIN = {"车次"};
private static final String[] KEYWORD_TOTAL_RECORD = {"纪录合共"};
private static final String[] KEYWORD_INVOICE_COUNT = {"发票张数"};
private static final String[] KEYWORD_SUBTOTAL = {"小计"};
// 货品明细列头别名
private static final String[] PRODUCT_CODE_ALIASES = {"货品编号", "产品编号", "物料编号", "货品编码", "产品编码", "物料编码", "料号"};
private static final String[] PRODUCT_NAME_ALIASES = {"简称", "货品名称", "产品名称", "物料名称", "品名", "名称"};
private static final String[] PRODUCT_BOX_ALIASES = {"箱数", ""};
private static final String[] PRODUCT_PIECE_ALIASES = {"支数", "件数", ""};
// 客户配送明细列头别名
private static final String[] CATEGORY_ALIASES = {"类别"};
private static final String[] INVOICE_NO_ALIASES = {"发票编号", "发票号", "发票"};
private static final String[] CUSTOMER_CODE_ALIASES = {"客户编号", "客户编码", "客户代码"};
private static final String[] CUSTOMER_NAME_ALIASES = {"客户名称", "名称"};
private static final String[] CUSTOMER_BOX_ALIASES = {"箱数", ""};
private static final String[] CUSTOMER_PACKAGE_ALIASES = {"包数", ""};
private static final String[] CUSTOMER_AMOUNT_ALIASES = {"金额", "总额"};
// 列头行认定门槛锚点列编号列必须识别到且识别列总数达标防止数据行误判为列头行
private static final int HEADER_MIN_MATCH_COUNT = 2;
// 老模板兜底货品明细数据固定从第7行客户配送明细数据固定从第4行开始列头未识别到时启用固定列位
private static final int PRODUCT_DATA_START_ROW = 7;
private static final int CUSTOMER_DATA_START_ROW = 4;
// 支持的日期格式String转Date
private static final String[] SUPPORTED_DATE_FORMATS = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"};
@@ -43,6 +70,26 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
private final List<ProductDetail> productDetailList = new ArrayList<>();
private final List<CustomerDeliveryDetail> customerDeliveryDetailList = new ArrayList<>();
// 货品明细列索引-1表示未识别该列
private int productCodeCol = -1;
private int productNameCol = -1;
private int productBoxCol = -1;
private int productPieceCol = -1;
private boolean productColumnsResolved = false;
// 客户配送明细列索引-1表示未识别该列
private int categoryCol = -1;
private int invoiceNoCol = -1;
private int customerCodeCol = -1;
private int customerNameCol = -1;
private int customerBoxCol = -1;
private int customerPackageCol = -1;
private int customerAmountCol = -1;
private boolean customerColumnsResolved = false;
// 发票张数/小计行是否已解析
private boolean invoiceRowParsed = false;
// 当前行号1-based与Excel行号一致
private int currentRowNum = 0;
@@ -53,10 +100,10 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
try {
LOGGER.debug("第{}行:开始解析,所有单元格数据:{}", currentRowNum, data);
// 左侧区域解析A-E列
// 左侧区域解析货品相关信息
parseLeftSection(data);
// 右侧区域解析F-M列
// 右侧区域解析客户配送相关信息
parseRightSection(data);
} catch (ExcelDataConvertException e) {
@@ -71,67 +118,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
// ---------------------- 左侧区域解析货品相关信息 ----------------------
private void parseLeftSection(Map<String, Object> data) {
switch (currentRowNum) {
case 1:
// 第1行送货日期A列=标签B列=
parseDeliveryDate(data);
break;
case 2:
// 第2行送货线号A列=标签B列=
parseDeliveryLineNo(data);
break;
case 3:
// 第3行车次A列=标签B列=
parseTrainNumber(data);
break;
case 4:
// 第4行货品明细标题行无需解析
LOGGER.debug("第{}行:货品明细标题行,跳过", currentRowNum);
break;
case 5:
// 第5行纪录合共A列=标签B列=总记录数D列=总箱数E列=总支数
parseTotalRecord(data);
break;
case 6:
// 第6行货品明细列标题貨品編號/簡稱//跳过
LOGGER.debug("第{}行:货品明细列标题行,跳过", currentRowNum);
break;
default:
// 第7行起货品明细数据行遇到空行自动结束
if (currentRowNum >= 7 && isValidProductDetailRow(data)) {
parseProductDetail(data, currentRowNum);
}
break;
// 1. 表头标签送货日期/送货线号/车次按标签文字定位行位置不限
parseHeaderLabels(data);
// 2. 纪录合共按标签文字定位行位置不限
parseTotalRecord(data);
// 3. 货品明细列布局已确定时按数据行解析
if (productColumnsResolved) {
if (isValidProductDetailRow(data)) {
parseProductDetail(data, currentRowNum);
}
return;
}
// 4. 尝试识别货品明细列头行识别成功则本行即列头行跳过数据解析
if (tryResolveProductColumns(data)) {
return;
}
// 5. 超过老模板数据起始行仍未识别到列头行启用固定列位兜底从当前行开始按数据行解析
if (currentRowNum >= PRODUCT_DATA_START_ROW) {
applyProductFallbackColumns();
if (isValidProductDetailRow(data)) {
parseProductDetail(data, currentRowNum);
}
}
}
// ---------------------- 右侧区域解析客户配送相关信息 ----------------------
private void parseRightSection(Map<String, Object> data) {
switch (currentRowNum) {
case 1:
// 第1行客户配送明细标题行无需解析
LOGGER.debug("第{}行:客户配送明细标题行,跳过", currentRowNum);
break;
case 2:
// 第2行发票张数/小计F列=發票張數标签G列=数值J列=小計标签K/L/M列=数值
parseInvoiceCount(data);
break;
case 3:
// 第3行客户配送明细列标题類别/發票編號/客户編號/名稱///金額跳过
LOGGER.debug("第{}行:客户配送明细列标题行,跳过", currentRowNum);
break;
default:
// 第4行起客户配送明细数据行遇到空行自动结束
if (currentRowNum >= 4 && isValidCustomerDetailRow(data)) {
parseCustomerDeliveryDetail(data, currentRowNum);
}
break;
// 1. 发票张数/小计按标签文字定位行位置不限解析成功后不再重复解析
if (!invoiceRowParsed) {
parseInvoiceRow(data);
}
// 2. 客户配送明细列布局已确定时按数据行解析
if (customerColumnsResolved) {
if (isValidCustomerDetailRow(data)) {
parseCustomerDeliveryDetail(data, currentRowNum);
}
return;
}
// 3. 尝试识别客户配送明细列头行识别成功则本行即列头行跳过数据解析
if (tryResolveCustomerColumns(data)) {
return;
}
// 4. 超过老模板数据起始行仍未识别到列头行启用固定列位兜底从当前行开始按数据行解析
if (currentRowNum >= CUSTOMER_DATA_START_ROW) {
applyCustomerFallbackColumns();
if (isValidCustomerDetailRow(data)) {
parseCustomerDeliveryDetail(data, currentRowNum);
}
}
}
// ---------------------- 解析完成回调 ----------------------
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 小计标签始终未识别到时保持与原逻辑一致小计相关字段置0
if (deliveryHeader.getSubtotalAmount() == null) {
LOGGER.warn("未找到【小计】标签,小计信息置空");
deliveryHeader.setSubtotalBoxCount(0);
deliveryHeader.setSubtotalPieceCount(0);
deliveryHeader.setSubtotalAmount(BigDecimal.ZERO);
}
LOGGER.info("Excel解析全部完成!最终统计结果:");
LOGGER.info("1. 表头信息:送货日期={},送货线号={},车次={},发票张数={},总记录数={},小计金额={}",
deliveryHeader.getDeliveryDate(),
@@ -144,47 +190,52 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
LOGGER.info("3. 客户配送明细:共{}条记录", customerDeliveryDetailList.size());
}
// ---------------------- 左侧表头信息解析 ----------------------
private void parseDeliveryDate(Map<String, Object> data) {
Object value = getValueSafely(data, 1); // B列
if (value != null) {
Date deliveryDate = convertToDate(value);
deliveryHeader.setDeliveryDate(deliveryDate);
LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate);
// ---------------------- 左侧表头信息解析标签定位行位置不限 ----------------------
private void parseHeaderLabels(Map<String, Object> data) {
// 送货日期
if (deliveryHeader.getDeliveryDate() == null) {
Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_DATE);
if (keywordIndex != null) {
Date deliveryDate = convertToDate(getValueSafely(data, keywordIndex + 1));
if (deliveryDate != null) {
deliveryHeader.setDeliveryDate(deliveryDate);
LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate);
}
}
}
// 送货线号
if (deliveryHeader.getDeliveryLineNo() == null || deliveryHeader.getDeliveryLineNo().trim().isEmpty()) {
Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_LINE);
if (keywordIndex != null) {
Object value = getValueSafely(data, keywordIndex + 1);
if (value != null && !value.toString().trim().isEmpty()) {
deliveryHeader.setDeliveryLineNo(value.toString().trim());
LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryHeader.getDeliveryLineNo());
}
}
}
// 车次
if (deliveryHeader.getTrainNumber() == null) {
Integer keywordIndex = findKeywordIndex(data, KEYWORD_TRAIN);
if (keywordIndex != null) {
deliveryHeader.setTrainNumber(convertToInteger(getValueSafely(data, keywordIndex + 1)));
LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, deliveryHeader.getTrainNumber());
}
}
}
private void parseDeliveryLineNo(Map<String, Object> data) {
Object value = getValueSafely(data, 1); // B列
if (value != null) {
String deliveryLineNo = value.toString().trim();
deliveryHeader.setDeliveryLineNo(deliveryLineNo);
LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryLineNo);
}
}
private void parseTrainNumber(Map<String, Object> data) {
Object value = getValueSafely(data, 1); // B列
Integer trainNumber = convertToInteger(value);
deliveryHeader.setTrainNumber(trainNumber);
LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, trainNumber);
}
// ---------------------- 左侧纪录合共解析 ----------------------
// ---------------------- 左侧纪录合共解析标签定位行位置不限 ----------------------
/**
* 第5行纪录合共
* 列对应关系0=A列=紀錄合共标签1=B列=总记录数3=D列=总箱数4=E列=总支数
* 纪录合共标签右侧第1列=总记录数第3列=总箱数第4列=总支数与老模板B/D/E列偏移一致
*/
private void parseTotalRecord(Map<String, Object> data) {
Object totalRecordLabel = getValueSafely(data, 0);
if (totalRecordLabel == null || !totalRecordLabel.toString().trim().contains(KEYWORD_TOTAL_RECORD)) {
LOGGER.warn("第{}行:不是纪录合共行,跳过解析,第一列内容:{}", currentRowNum, totalRecordLabel);
Integer keywordIndex = findKeywordIndex(data, KEYWORD_TOTAL_RECORD);
if (keywordIndex == null) {
return;
}
Integer totalRecordCount = convertToInteger(getValueSafely(data, 1)); // B列
Integer totalBoxCount = convertToInteger(getValueSafely(data, 3)); // D列
Integer totalPieceCount = convertToInteger(getValueSafely(data, 4)); // E列
Integer totalRecordCount = convertToInteger(getValueSafely(data, keywordIndex + 1));
Integer totalBoxCount = convertToInteger(getValueSafely(data, keywordIndex + 3));
Integer totalPieceCount = convertToInteger(getValueSafely(data, keywordIndex + 4));
deliveryHeader.setTotalRecordCount(totalRecordCount);
deliveryHeader.setTotalBoxCount(totalBoxCount);
@@ -194,27 +245,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
currentRowNum, totalRecordCount, totalBoxCount, totalPieceCount);
}
// ---------------------- 左侧货品明细解析 ----------------------
// ---------------------- 左侧货品明细列头识别与数据解析 ----------------------
/**
* 校验货品明细行是否有效排除表头和空行
* 尝试将当前行识别为货品明细列头行以货品编号列为锚点按列头文字简繁归一化匹配各列
*/
private boolean isValidProductDetailRow(Map<String, Object> data) {
Object productCode = getValueSafely(data, 0); // A列
return productCode != null
&& !productCode.toString().trim().isEmpty()
&& !"貨品編號".equals(productCode.toString().trim());
private boolean tryResolveProductColumns(Map<String, Object> data) {
int codeCol = findColumnByAliases(data, PRODUCT_CODE_ALIASES);
if (codeCol < 0) {
return false;
}
int nameCol = findColumnByAliases(data, PRODUCT_NAME_ALIASES);
int boxCol = findColumnByAliases(data, PRODUCT_BOX_ALIASES);
int pieceCol = findColumnByAliases(data, PRODUCT_PIECE_ALIASES);
int matched = 1 + (nameCol >= 0 ? 1 : 0) + (boxCol >= 0 ? 1 : 0) + (pieceCol >= 0 ? 1 : 0);
if (matched < HEADER_MIN_MATCH_COUNT) {
return false;
}
productCodeCol = codeCol;
productNameCol = nameCol;
productBoxCol = boxCol;
productPieceCol = pieceCol;
productColumnsResolved = true;
LOGGER.info("第{}行识别为货品明细列头行,列映射:货品编号={},名称={},箱={},支={}",
currentRowNum, productCodeCol, productNameCol, productBoxCol, productPieceCol);
return true;
}
/**
* 解析货品明细第7行起
* 列对应关系0=A列=貨品編號1=B列=簡稱2=C列=3=D列=
* 兜底列位老模板货品明细固定布局0=A列=貨品編號1=B列=簡稱2=C列=3=D列=
*/
private void applyProductFallbackColumns() {
productCodeCol = 0;
productNameCol = 1;
productBoxCol = 2;
productPieceCol = 3;
productColumnsResolved = true;
LOGGER.warn("第{}行前未识别到货品明细列头行,启用老模板固定列位兜底解析", currentRowNum);
}
/**
* 校验货品明细行是否有效排除列头行和空行
*/
private boolean isValidProductDetailRow(Map<String, Object> data) {
Object productCode = getValueSafely(data, productCodeCol);
if (productCode == null || productCode.toString().trim().isEmpty()) {
return false;
}
// 列头文字本身不算数据行防止列头行被当作数据重复解析
String text = normalizeHeaderText(productCode);
return !matchesAny(text, PRODUCT_CODE_ALIASES);
}
/**
* 解析货品明细列头行之后的行
*/
private void parseProductDetail(Map<String, Object> data, int rowNum) {
ProductDetail productDetail = new ProductDetail();
productDetail.setProductCode(convertToString(getValueSafely(data, 0)));
productDetail.setProductName(convertToString(getValueSafely(data, 1)));
productDetail.setBoxCount(convertToInteger(getValueSafely(data, 2)));
productDetail.setPieceCount(convertToInteger(getValueSafely(data, 3)));
productDetail.setProductCode(convertToString(getValueSafely(data, productCodeCol)));
productDetail.setProductName(convertToString(getValueSafely(data, productNameCol)));
productDetail.setBoxCount(convertToInteger(getValueSafely(data, productBoxCol)));
productDetail.setPieceCount(convertToInteger(getValueSafely(data, productPieceCol)));
productDetailList.add(productDetail);
LOGGER.debug("第{}行:解析货品明细完成,货品编号={},名称={},箱数={},支数={}",
@@ -222,67 +312,111 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
productDetail.getBoxCount(), productDetail.getPieceCount());
}
// ---------------------- 右侧发票张数与小计解析 ----------------------
/**
* 第2行发票张数与小计
* 通过关键字定位标签再读取右侧相邻列避免空列导致索引偏移
*/
private void parseInvoiceCount(Map<String, Object> data) {
// 1. 定位并解析发票张数
Integer invoiceCount = findValueAfterKeyword(data, KEYWORD_INVOICE_COUNT);
if (invoiceCount != null) {
deliveryHeader.setInvoiceCount(invoiceCount);
// ---------------------- 右侧发票张数与小计解析标签定位行位置不限 ----------------------
private void parseInvoiceRow(Map<String, Object> data) {
// 1. 发票张数标签右侧相邻列
Integer invoiceIndex = findKeywordIndex(data, KEYWORD_INVOICE_COUNT);
if (invoiceIndex != null) {
deliveryHeader.setInvoiceCount(convertToInteger(getValueSafely(data, invoiceIndex + 1)));
}
// 2. 定位并解析小计标签右侧第1列=第2列=第3列=金额
Integer subtotalStartIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL);
if (subtotalStartIndex != null && subtotalStartIndex >= 0) {
Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 1));
Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 2));
BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalStartIndex + 3));
// 2. 小计标签右侧第1列=第2列=第3列=金额
Integer subtotalIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL);
if (subtotalIndex != null && subtotalIndex >= 0) {
Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalIndex + 1));
Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalIndex + 2));
BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalIndex + 3));
deliveryHeader.setSubtotalBoxCount(subtotalBoxCount);
deliveryHeader.setSubtotalPieceCount(subtotalPieceCount);
deliveryHeader.setSubtotalAmount(subtotalAmount);
invoiceRowParsed = true;
LOGGER.debug("第{}行:解析小计完成,箱数={},支数={},金额={}",
currentRowNum, subtotalBoxCount, subtotalPieceCount, subtotalAmount);
} else {
LOGGER.warn("第{}行:未找到小计标签,小计信息置空", currentRowNum);
deliveryHeader.setSubtotalBoxCount(0);
deliveryHeader.setSubtotalPieceCount(0);
deliveryHeader.setSubtotalAmount(BigDecimal.ZERO);
LOGGER.debug("第{}行:解析发票张数行完成,发票张数={}小计箱数={},支数={},金额={}",
currentRowNum, deliveryHeader.getInvoiceCount(), subtotalBoxCount, subtotalPieceCount, subtotalAmount);
}
LOGGER.debug("第{}行:解析发票张数行完成,发票张数={},小计金额={}",
currentRowNum, deliveryHeader.getInvoiceCount(), deliveryHeader.getSubtotalAmount());
}
// ---------------------- 右侧客户配送明细解析 ----------------------
// ---------------------- 右侧客户配送明细列头识别与数据解析 ----------------------
/**
* 校验客户配送明细行是否有效排除表头和空行
* 尝试将当前行识别为客户配送明细列头行以客户编号列为锚点按列头文字简繁归一化匹配各列
*/
private boolean tryResolveCustomerColumns(Map<String, Object> data) {
int codeCol = findColumnByAliases(data, CUSTOMER_CODE_ALIASES);
if (codeCol < 0) {
return false;
}
int categoryColTmp = findColumnByAliases(data, CATEGORY_ALIASES);
int invoiceNoColTmp = findColumnByAliases(data, INVOICE_NO_ALIASES);
int nameColTmp = findColumnByAliases(data, CUSTOMER_NAME_ALIASES);
int boxColTmp = findColumnByAliases(data, CUSTOMER_BOX_ALIASES);
int packageColTmp = findColumnByAliases(data, CUSTOMER_PACKAGE_ALIASES);
int amountColTmp = findColumnByAliases(data, CUSTOMER_AMOUNT_ALIASES);
int matched = 1 + (categoryColTmp >= 0 ? 1 : 0) + (invoiceNoColTmp >= 0 ? 1 : 0) + (nameColTmp >= 0 ? 1 : 0)
+ (boxColTmp >= 0 ? 1 : 0) + (packageColTmp >= 0 ? 1 : 0) + (amountColTmp >= 0 ? 1 : 0);
if (matched < HEADER_MIN_MATCH_COUNT) {
return false;
}
categoryCol = categoryColTmp;
invoiceNoCol = invoiceNoColTmp;
customerCodeCol = codeCol;
customerNameCol = nameColTmp;
customerBoxCol = boxColTmp;
customerPackageCol = packageColTmp;
customerAmountCol = amountColTmp;
customerColumnsResolved = true;
// 关键列未识别到时告警避免发票编号等字段静默丢失
if (invoiceNoColTmp < 0 || amountColTmp < 0) {
LOGGER.warn("第{}行客户配送明细列头识别到但存在缺失列:发票编号列={},金额列={}," +
"对应数据将解析为空,请检查模板列头文字是否在归一化映射范围内",
currentRowNum, invoiceNoColTmp, amountColTmp);
}
LOGGER.info("第{}行识别为客户配送明细列头行,列映射:类别={},发票编号={},客户编号={},名称={},箱={},包={},金额={}",
currentRowNum, categoryCol, invoiceNoCol, customerCodeCol, customerNameCol, customerBoxCol, customerPackageCol, customerAmountCol);
return true;
}
/**
* 兜底列位老模板客户配送明细固定布局5=F列=類别6=G列=發票編號7=H列=客户編號8=I列=名稱
* 9=J列=10=K列=11=L列=金額
*/
private void applyCustomerFallbackColumns() {
categoryCol = 5;
invoiceNoCol = 6;
customerCodeCol = 7;
customerNameCol = 8;
customerBoxCol = 9;
customerPackageCol = 10;
customerAmountCol = 11;
customerColumnsResolved = true;
LOGGER.warn("第{}行前未识别到客户配送明细列头行,启用老模板固定列位兜底解析", currentRowNum);
}
/**
* 校验客户配送明细行是否有效排除列头行和空行
*/
private boolean isValidCustomerDetailRow(Map<String, Object> data) {
Object category = getValueSafely(data, 5); // F列
return category != null
&& !category.toString().trim().isEmpty()
&& !"類别".equals(category.toString().trim());
Object category = getValueSafely(data, categoryCol);
if (category == null || category.toString().trim().isEmpty()) {
return false;
}
// 列头文字本身不算数据行防止列头行被当作数据重复解析
String text = normalizeHeaderText(category);
return !matchesAny(text, CATEGORY_ALIASES);
}
/**
* 解析客户配送明细第4行起
* 列对应关系5=F列=類别6=G列=發票編號7=H列=客户編號8=I列=名稱
* 9=J列=10=K列=11=L列=金額
* 解析客户配送明细列头行之后的行
*/
private void parseCustomerDeliveryDetail(Map<String, Object> data, int rowNum) {
CustomerDeliveryDetail customerDetail = new CustomerDeliveryDetail();
customerDetail.setCategory(convertToString(getValueSafely(data, 5)));
customerDetail.setInvoiceNo(convertToString(getValueSafely(data, 6)));
customerDetail.setCustomerCode(convertToString(getValueSafely(data, 7)));
customerDetail.setCustomerName(convertToString(getValueSafely(data, 8)));
customerDetail.setBoxCount(convertToInteger(getValueSafely(data, 9)));
customerDetail.setPackageCount(convertToInteger(getValueSafely(data, 10)));
customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, 11)));
customerDetail.setCategory(convertToString(getValueSafely(data, categoryCol)));
customerDetail.setInvoiceNo(convertToString(getValueSafely(data, invoiceNoCol)));
customerDetail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol)));
customerDetail.setCustomerName(convertToString(getValueSafely(data, customerNameCol)));
customerDetail.setBoxCount(convertToInteger(getValueSafely(data, customerBoxCol)));
customerDetail.setPackageCount(convertToInteger(getValueSafely(data, customerPackageCol)));
customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, customerAmountCol)));
customerDeliveryDetailList.add(customerDetail);
LOGGER.debug("第{}行:解析客户配送明细完成,类别={},发票编号={},客户编号={},名称={},箱数={},包数={},金额={}",
@@ -299,7 +433,7 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
* 此时key不是数字字符串需跳过这些key走位置兜底逻辑
*/
private Object getValueSafely(Map<String, Object> data, int columnIndex) {
if (data == null || data.isEmpty()) {
if (columnIndex < 0 || data == null || data.isEmpty()) {
return null;
}
@@ -333,9 +467,9 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
return "未知列";
}
int index = 0;
for (String key : data.keySet()) {
for (Object key : data.keySet()) {
if (index == columnIndex) {
return key;
return String.valueOf(key);
}
index++;
}
@@ -343,22 +477,28 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
}
/**
* 在指定行中查找关键字所在的列索引
* 注意Map的key可能是非数字字符串如表头文本"客户配送明细"此时跳过该key不抛出异常
* 在指定行中查找关键字所在的列索引关键字归一化后包含匹配兼容简繁体与冒号等符号
* 注意Map的key可能是非数字字符串如表头文本此时跳过该key不抛出异常
*/
private Integer findKeywordIndex(Map<String, Object> data, String keyword) {
if (data == null || data.isEmpty() || keyword == null) {
private Integer findKeywordIndex(Map<String, Object> data, String[] keywords) {
if (data == null || data.isEmpty() || keywords == null) {
return null;
}
for (Object keyObj : data.keySet()) {
Object value = data.get(keyObj);
if (value != null && value.toString().trim().contains(keyword)) {
if (value == null) {
continue;
}
String text = normalizeHeaderText(value);
if (text.isEmpty()) {
continue;
}
if (matchesAny(text, keywords)) {
try {
return Integer.parseInt(keyObj.toString());
} catch (NumberFormatException e) {
// key不是数字可能是表头文本"客户配送明细"跳过不打印警告避免日志刷屏
LOGGER.debug("第{}行:关键字'{}'所在列key不是数字,key={},跳过",
currentRowNum, keyword, keyObj);
// key不是数字可能是表头文本跳过不打印警告避免日志刷屏
LOGGER.debug("第{}行:关键字所在列key不是数字,key={},跳过", currentRowNum, keyObj);
}
}
}
@@ -366,15 +506,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
}
/**
* 查找关键字所在列的右侧相邻列值并转为Integer
* 在指定行中按列头别名查找列索引列头文字归一化后包含匹配返回首个命中的列未命中返回-1
*/
private Integer findValueAfterKeyword(Map<String, Object> data, String keyword) {
Integer keywordIndex = findKeywordIndex(data, keyword);
if (keywordIndex == null) {
LOGGER.warn("第{}行:未找到关键字'{}'", currentRowNum, keyword);
return null;
private int findColumnByAliases(Map<String, Object> data, String[] aliases) {
if (data == null || data.isEmpty() || aliases == null) {
return -1;
}
return convertToInteger(getValueSafely(data, keywordIndex + 1));
for (Object keyObj : data.keySet()) {
Object value = data.get(keyObj);
if (value == null) {
continue;
}
String text = normalizeHeaderText(value);
if (text.isEmpty()) {
continue;
}
if (matchesAny(text, aliases)) {
try {
return Integer.parseInt(keyObj.toString());
} catch (NumberFormatException e) {
// key不是数字可能是表头文本跳过
LOGGER.debug("第{}行:列头所在列key不是数字,key={},跳过", currentRowNum, keyObj);
}
}
}
return -1;
}
/**
* 列头/标签文字归一化去空格/冒号/括号等符号转小写繁体转简体覆盖模板常见用字
*/
private String normalizeHeaderText(Object value) {
if (value == null) {
return "";
}
String s = value.toString().trim().toLowerCase();
s = s.replace("", "").replace(":", "")
.replace(" ", "").replace(" ", "")
.replace("(", "").replace(")", "")
.replace("", "").replace("", "")
.replace("/", "").replace("\\", "");
s = s.replace('貨', '货').replace('編', '编').replace('號', '号').replace('稱', '称')
.replace('數', '数').replace('飲', '饮').replace('規', '规').replace('戶', '户')
.replace('紀', '纪').replace('錄', '录').replace('張', '张').replace('計', '计')
.replace('車', '车').replace('線', '线').replace('類', '类').replace('別', '别')
.replace('簡', '简').replace('產', '产').replace('單', '单')
// 維他奶模板發票編號/發票張數/金額用字缺了这两项会导致發票編號列识别失败发票编号不落库
.replace('發', '发').replace('額', '额');
return s;
}
/**
* 判断归一化后的文字是否命中任一别名
*/
private boolean matchesAny(String text, String[] aliases) {
for (String alias : aliases) {
if (text.contains(alias)) {
return true;
}
}
return false;
}
/**
@@ -17,7 +17,11 @@ import java.util.Map;
/**
* 入库单Excel解析监听器
* 功能1. 解析第一行表头数据 2. 跳过第二行合计行第三行列名行 3. 解析第四行开始的产品明细 4. 支持文本型数字转换
* 功能1. 解析第一行表头数据入库日期OrderNO按标签定位位置不敏感
* 2. 动态定位列头行扫描前几行识别出"產品編號"等已知列头文字的行即认定为列头行行位置不敏感
* 3. 列按列头文字动态定位兼容繁体列头產品編號/客戶編號/件數/飲用日期等列顺序调整不影响解析
* 4. 列头无法识别时兜底按老模板固定列位0=编号1=名称2=规格3=件数4=饮用日期
* 5. 支持文本型数字转换
*/
public class StockInExcelListener extends AnalysisEventListener<Map<String, Object>> {
private static final Logger LOGGER = LoggerFactory.getLogger(StockInExcelListener.class);
@@ -35,6 +39,29 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
"dd.MM.yyyy", "d-MMM-yyyy", "yyyyMMddHHmmss"
};
// ====== 列头别名归一化后匹配兼容简繁体及常见叫法 ======
private static final String[] PRODUCT_CODE_ALIASES = {"产品编号", "物料编号", "产品编码", "物料编码", "料号"};
private static final String[] PRODUCT_NAME_ALIASES = {"产品名称", "物料名称", "商品名称", "品名"};
private static final String[] CUSTOMER_CODE_ALIASES = {"客户编号", "客户编码", "客户代码"};
private static final String[] DRINK_DATE_ALIASES = {"饮用日期", "饮用时间"};
private static final String[] PIECE_COUNT_ALIASES = {"件数"};
private static final String[] SPEC_ALIASES = {"规格", "箱含量"};
// 列头行至少需识别出的列数防止把日期标签行/合计行误判为列头行
private static final int HEADER_MIN_MATCH_COUNT = 2;
// 老模板列头行固定在第3行超过该行仍未识别到列头行时启用固定列位兜底
private static final int FALLBACK_AFTER_ROW_NUM = 3;
// 动态解析出的列索引-1表示未识别该列
private int productCodeCol = -1;
private int productNameCol = -1;
private int customerCodeCol = -1;
private int drinkDateCol = -1;
private int pieceCountCol = -1;
private int specCol = -1;
// 列布局是否已确定列头行识别成功或已启用兜底
private boolean columnResolved = false;
// 行号标记用于区分第一行和表格行
private int currentRowNum = 0;
@@ -48,21 +75,21 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
if (currentRowNum == 1) {
parseHeaderData(data);
}
// 2. 跳过第二行合计行Total: xxx
else if (currentRowNum == 2) {
LOGGER.debug("第{}行是合计行,跳过解析", currentRowNum);
}
// 3. 跳过第三行表格列名行產品編號產品名稱...
else if (currentRowNum == 3) {
LOGGER.debug("第{}行是表格列名行,跳过解析", currentRowNum);
}
// 4. 解析第四行及以后产品明细
else if (currentRowNum >= 4) {
if (isValidProductRow(data)) {
StockInProductDetail detail = parseProductDetail(data);
productDetailList.add(detail);
// 2. 列布局未确定尝试识别列头行识别成功则本行即为列头行跳过数据解析
else if (!columnResolved) {
if (!tryResolveHeaderRow(data)) {
// 3. 超过老模板列头行位置仍未识别到列头行启用固定列位兜底从当前行开始按数据行解析
if (currentRowNum > FALLBACK_AFTER_ROW_NUM) {
applyFallbackColumns();
parseDataRow(data);
}
// 否则合计行Total等跳过
}
}
// 4. 列布局已确定按数据行解析
else {
parseDataRow(data);
}
} catch (ExcelDataConvertException e) {
// Excel内置转换异常如日期格式错误
@@ -79,6 +106,16 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
}
/**
* 数据行解析排除合计行/空行后解析产品明细
*/
private void parseDataRow(Map<String, Object> data) {
if (isValidProductRow(data)) {
StockInProductDetail detail = parseProductDetail(data);
productDetailList.add(detail);
}
}
/**
* 解析第一行表头数据
* 模板中标签与值相邻分别搜索"入库日期""OrderNO"标签后一列的值兼容位置调整
@@ -86,15 +123,17 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
private void parseHeaderData(Map<String, Object> data) {
// 入库日期查找包含"入库日期"标签的下一个值
Object stockInDateObj = getValueAfterLabel(data, "入庫日期:");
if (stockInDateObj == null) {
stockInDateObj = getValueAfterLabel(data, "入库日期");
}
if (stockInDateObj != null) {
Date stockInDate = convertToDate(stockInDateObj);
stockInHeader.setStockInDate(stockInDate);
LOGGER.debug("第1行:入库日期解析完成,值:{}", stockInDate);
}
// OrderNO优先查找"OrderNO"标签后的值老模板布局标签与值相邻
// 新模板第1行无"OrderNO"标签单号值直接放在单元格中(如E1)
// 此时取第1行中除标签入库日期以外的第一个非空字符串值
// OrderNO优先查找"OrderNO"标签后的值标签与值相邻
// 新模板第1行无"OrderNO"标签取第1行中除标签入库日期以外的第一个非空字符串值
Object orderNoObj = getValueAfterLabel(data, "OrderNO");
if (orderNoObj == null) {
for (Object value : data.values()) {
@@ -103,7 +142,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
String str = value.toString().trim();
// 跳过标签单元格与已被识别为入库日期的值
if (str.isEmpty() || str.contains("入庫日期") || str.contains("OrderNO") || str.contains("Total")) {
if (str.isEmpty() || str.contains("入庫日期") || str.contains("入库日期") || str.contains("OrderNO") || str.contains("Total")) {
continue;
}
if (value == stockInDateObj) {
@@ -125,26 +164,113 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
}
/**
* 尝试将当前行识别为列头行按归一化后的列头文字匹配已知列别名
* 识别成功至少识别出产品编号列且总数达标返回true并记录各列索引
*/
private boolean tryResolveHeaderRow(Map<String, Object> data) {
if (data == null || data.isEmpty()) {
return false;
}
int tmpProductCodeCol = -1;
int tmpProductNameCol = -1;
int tmpCustomerCodeCol = -1;
int tmpDrinkDateCol = -1;
int tmpPieceCountCol = -1;
int tmpSpecCol = -1;
int matched = 0;
for (Object keyObj : data.keySet()) {
Object value = data.get(keyObj);
if (value == null) {
continue;
}
String text = normalizeHeaderText(value);
if (text.isEmpty()) {
continue;
}
int colIndex;
try {
colIndex = Integer.parseInt(keyObj.toString());
} catch (NumberFormatException e) {
// key不是数字如表头文本跳过
continue;
}
// 按组依次匹配先具体后一般避免同一列头命中多组别名时归属错乱
if (tmpProductCodeCol < 0 && matchesAny(text, PRODUCT_CODE_ALIASES)) {
tmpProductCodeCol = colIndex;
matched++;
} else if (tmpCustomerCodeCol < 0 && matchesAny(text, CUSTOMER_CODE_ALIASES)) {
tmpCustomerCodeCol = colIndex;
matched++;
} else if (tmpProductNameCol < 0 && matchesAny(text, PRODUCT_NAME_ALIASES)) {
tmpProductNameCol = colIndex;
matched++;
} else if (tmpDrinkDateCol < 0 && matchesAny(text, DRINK_DATE_ALIASES)) {
tmpDrinkDateCol = colIndex;
matched++;
} else if (tmpPieceCountCol < 0 && matchesAny(text, PIECE_COUNT_ALIASES)) {
tmpPieceCountCol = colIndex;
matched++;
} else if (tmpSpecCol < 0 && matchesAny(text, SPEC_ALIASES)) {
tmpSpecCol = colIndex;
matched++;
}
}
// 产品编号列是数据解析的锚点必须识别到且识别总数达标才认定为列头行
if (tmpProductCodeCol < 0 || matched < HEADER_MIN_MATCH_COUNT) {
return false;
}
productCodeCol = tmpProductCodeCol;
productNameCol = tmpProductNameCol;
customerCodeCol = tmpCustomerCodeCol;
drinkDateCol = tmpDrinkDateCol;
pieceCountCol = tmpPieceCountCol;
specCol = tmpSpecCol;
columnResolved = true;
LOGGER.info("第{}行识别为列头行,列映射:产品编号={},产品名称={},客户编号={},规格={},件数={},饮用日期={}",
currentRowNum, productCodeCol, productNameCol, customerCodeCol, specCol, pieceCountCol, drinkDateCol);
return true;
}
/**
* 兜底列位老模板固定布局0=编号1=名称2=规格3=件数4=饮用日期无客户编号列
*/
private void applyFallbackColumns() {
productCodeCol = 0;
productNameCol = 1;
customerCodeCol = -1;
specCol = 2;
pieceCountCol = 3;
drinkDateCol = 4;
columnResolved = true;
LOGGER.warn("第{}行前未识别到列头行,启用老模板固定列位兜底解析", currentRowNum);
}
/**
* 解析产品明细数据一行对应一个产品
* 新模板列顺序0=产品编号1=产品名称2=规格箱含量3=件数4=饮用日期
* 各列按列头行动态定位的索引取值未识别的列取null/默认值
*/
private StockInProductDetail parseProductDetail(Map<String, Object> data) {
StockInProductDetail detail = new StockInProductDetail();
// 产品编号第1列索引0- 支持文本型数字转换
detail.setProductCode(convertToString(getValueSafely(data, 0)));
// 产品名称第2列索引1
detail.setProductName(convertToString(getValueSafely(data, 1)));
// 箱含量/规格第3列索引2
detail.setBoxCapacity(convertToInteger(getValueSafely(data, 2)));
// 件数第4列索引3
detail.setPieceCount(convertToInteger(getValueSafely(data, 3)));
// 饮用日期第5列索引4
detail.setDrinkDate(convertToDate(getValueSafely(data, 4)));
// 产品编号可不填名称导入时按编号自动匹配库中物料名称
detail.setProductCode(convertToString(getValueSafely(data, productCodeCol)));
// 产品名称列头未识别或单元格未填时为空串由服务端按编号自动匹配覆盖
detail.setProductName(convertToString(getValueSafely(data, productNameCol)));
// 客户编号新模板新增列可不填填写后由服务端校验客户存在性并自动匹配客户名称
detail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol)));
// 箱含量/规格
detail.setBoxCapacity(convertToInteger(getValueSafely(data, specCol)));
// 件数
detail.setPieceCount(convertToInteger(getValueSafely(data, pieceCountCol)));
// 饮用日期
detail.setDrinkDate(convertToDate(getValueSafely(data, drinkDateCol)));
LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{}",
currentRowNum, detail.getProductCode(), detail.getProductName());
LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{},客户编号:{}",
currentRowNum, detail.getProductCode(), detail.getProductName(), detail.getCustomerCode());
return detail;
}
@@ -152,9 +278,6 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
* 校验产品行是否有效排除空行和合计行
*/
private boolean isValidProductRow(Map<String, Object> data) {
// 有效条件产品编号不为空 + 不是合计行
Object productCodeObj = getValueSafely(data, 0);
// 排除合计行整行包含"Total"字样
if (containsAnyValue(data, "Total")) {
LOGGER.debug("第{}行是合计行,跳过解析", currentRowNum);
@@ -162,6 +285,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
// 排除空行产品编号为空
Object productCodeObj = getValueSafely(data, productCodeCol);
return productCodeObj != null && !productCodeObj.toString().trim().isEmpty();
}
@@ -178,13 +302,24 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
// ---------------------- 工具方法类型转换+安全取值 ----------------------
/**
* 按列索引安全获取值支持列索引越界处理
* 按列索引安全获取值
* 优先按Map的key直接查找适配EasyExcel以列号为key的情况避免空单元格导致的位置偏移
* 找不到再按位置顺序查找保持与原逻辑兼容
*/
private Object getValueSafely(Map<String, Object> data, int columnIndex) {
if (data == null || data.isEmpty()) {
if (columnIndex < 0 || data == null || data.isEmpty()) {
return null;
}
String strIndex = String.valueOf(columnIndex);
// 1. 直接按key查找key为String或Integer跳过非数字key
for (Object keyObj : data.keySet()) {
if (keyObj != null && strIndex.equals(keyObj.toString())) {
return data.get(keyObj);
}
}
// 2. 兜底按顺序位置查找
int index = 0;
for (Object value : data.values()) {
if (index == columnIndex) {
@@ -193,7 +328,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
index++;
}
LOGGER.warn("第{}行列索引{}超出范围(当前行共{}列),返回null",
LOGGER.warn("第{}行列索引{}超出当前行单元格数量({}列),返回null",
currentRowNum, columnIndex, data.size());
return null;
}
@@ -207,9 +342,9 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
int index = 0;
for (String key : data.keySet()) {
for (Object key : data.keySet()) {
if (index == columnIndex) {
return key;
return String.valueOf(key);
}
index++;
}
@@ -251,6 +386,37 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
return false;
}
/**
* 列头文字归一化去空格/冒号/括号等符号转小写繁体转简体覆盖模板常见用字
*/
private String normalizeHeaderText(Object value) {
if (value == null) {
return "";
}
String s = value.toString().trim().toLowerCase();
s = s.replace("", "").replace(":", "")
.replace(" ", "").replace(" ", "")
.replace("(", "").replace(")", "")
.replace("", "").replace("", "")
.replace("/", "").replace("\\", "");
s = s.replace('產', '产').replace('編', '编').replace('號', '号').replace('稱', '称')
.replace('數', '数').replace('飲', '饮').replace('規', '规').replace('戶', '户')
.replace('貨', '货').replace('單', '单').replace('類', '类').replace('倉', '仓');
return s;
}
/**
* 判断归一化后的列头文字是否命中任一别名
*/
private boolean matchesAny(String text, String[] aliases) {
for (String alias : aliases) {
if (text.contains(alias)) {
return true;
}
}
return false;
}
/**
* 通用转换Object转String
*/
@@ -347,4 +513,4 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
public List<StockInProductDetail> getProductDetailList() {
return productDetailList;
}
}
}
@@ -35,7 +35,7 @@ public interface ReservationStockInOrderMapper extends BaseMapper<ReservationSto
public List<MaterialBaseInfoPO> getMaterialBaseInfoList(@Param("id") String id);
public List<ReservationMaterialInventoryPO> getkcId(@Param("id") Long id,@Param("q") BigDecimal q);
public List<ReservationMaterialInventoryPO> getkcId(@Param("id") Long id,@Param("q") BigDecimal q, @Param("warehouseId") Long warehouseId);
public MaterialWarehouseControlPO getInfoByMaterialBaseInfoIds(@Param("id") Long id);
@@ -78,5 +78,7 @@ public interface ReservationStockInOrderMapper extends BaseMapper<ReservationSto
int existsByOrderNumber(@Param("inOrderNumber") String inOrderNumber);
int existsByBusinessOrderNo(@Param("businessOrderNo") String businessOrderNo);
public List<ReservationStockInOrderPO> queryPushBmsList();
}
@@ -4,6 +4,8 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -57,6 +59,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
@@ -172,10 +175,10 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
String lineCode = deliveryHeader.getDeliveryLineNo();
Integer trainNumber = deliveryHeader.getTrainNumber();
if (StringUtils.isBlank(lineCode)) {
throw new Exception("Excel未识别到【送貨線號】(线路),请检查模板第2行后重新导入");
throw new Exception("Excel未识别到【送貨線號】(线路),请检查模板表头后重新导入");
}
if (trainNumber == null || trainNumber <= 0) {
throw new Exception("Excel未识别到【車次】,请检查模板第3行后重新导入");
throw new Exception("Excel未识别到【車次】,请检查模板表头后重新导入");
}
AjaxResult lineResult = wmsServiceFeign.listLineManagement(lineCode);
if (lineResult == null || lineResult.get(AjaxResult.DATA_TAG) == null) {
@@ -220,19 +223,90 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
@Override
public DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName) throws Exception {
@Transactional(rollbackFor = Exception.class)
public List<SheetImportResultDTO> parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 校验文件和工作表名称逻辑不变
// 校验文件逻辑不变
if (file.isEmpty()) {
throw new IllegalArgumentException("上传的Excel文件不能为空");
}
if (!"出库导入".equals(sheetName) && !"参考数据".equals(sheetName)) {
sheetName = "出库导入";
// 多页签模式文件中每个工作表独立解析为一张出库单老逻辑固定只读"出库导入"页签
byte[] fileBytes = file.getBytes();
List<String> sheetNames = listSheetNames(fileBytes);
if (sheetNames.isEmpty()) {
throw new IllegalArgumentException("Excel文件中没有工作表");
}
// ====== 第一阶段逐页签解析+校验+组装不落库失败按页签聚合收集全部页签处理完统一报错 ======
List<PreparedDeliveryOrder> preparedOrders = new ArrayList<>();
List<String> sheetProblems = new ArrayList<>();
for (String name : sheetNames) {
try {
PreparedDeliveryOrder prepared = buildDeliveryOrder(fileBytes, name, warehouseId, loginUser);
if (prepared != null) {
preparedOrders.add(prepared); // null=空页签跳过
}
} catch (Exception e) {
// 非模板页签的解析异常如NPEmessage 可能为空兜底用异常类名避免报错显示"页签[x]null"
sheetProblems.add("页签[" + name + "]" + (e.getMessage() == null ? e.toString() : e.getMessage()));
}
}
if (!sheetProblems.isEmpty()) {
throw new Exception(String.join("", sheetProblems));
}
if (preparedOrders.isEmpty()) {
throw new IllegalArgumentException("Excel中没有可导入的数据页签");
}
// ====== 第二阶段全部页签校验通过后统一落库同一事务任一失败整体回滚 ======
List<SheetImportResultDTO> results = new ArrayList<>();
for (PreparedDeliveryOrder prepared : preparedOrders) {
// 出库单号在落库阶段逐张生成此时上一张已入库同事务内可见取号递增不会重复
// 校验阶段统一生成会因全部页签都未入库而取到相同号
String orderNumber = reservationStockOutOrderApplicationService.generateReserveOrderNumber();
ReservationStockOutOrderDO order = prepared.order;
order.setOutOrderNumber(orderNumber);
order.setBusinessOrderNo(orderNumber);
if (order.getMaterialDetailList() != null) {
for (ReservationOutMaterialDetailDO detail : order.getMaterialDetailList()) {
detail.setOutOrderNumber(orderNumber);
}
}
reservationStockOutOrderApplicationService.insert(order);
saveDeliveryLinks(prepared);
prepared.result.setOrderNumber(orderNumber);
results.add(prepared.result);
}
return results;
}
/**
* 枚举Excel全部工作表名EasyExcel 3.xexcelExecutor().sheetList()
*/
private List<String> listSheetNames(byte[] fileBytes) {
List<String> sheetNames = new ArrayList<>();
try (ExcelReader reader = EasyExcel.read(new ByteArrayInputStream(fileBytes)).build()) {
for (ReadSheet readSheet : reader.excelExecutor().sheetList()) {
sheetNames.add(readSheet.getSheetName());
}
}
return sheetNames;
}
/** 出库单页签解析/校验/组装完成后的暂存容器(统一落库阶段使用) */
private static class PreparedDeliveryOrder {
ReservationStockOutOrderDO order;
List<CustomerDeliveryDetail> customerDeliveryDetailList;
Map<String, UserPo> customerCache;
SheetImportResultDTO result;
}
/**
* 单个页签的出库单解析+校验+组装不落库由调用方在全部页签通过后统一插入
* 空页签货品明细与客户配送明细均为空返回 null 由调用方跳过
*/
private PreparedDeliveryOrder buildDeliveryOrder(byte[] fileBytes, String sheetName, Long warehouseId, LoginUser loginUser) throws Exception {
// 关键变更使用动态行数监听器
DynamicDeliveryExcelListener listener = new DynamicDeliveryExcelListener();
try (InputStream inputStream = file.getInputStream()) {
try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
EasyExcel.read(inputStream, listener)
.sheet(sheetName)
.headRowNumber(0) // 无固定表头全部行交由监听器处理
@@ -254,22 +328,103 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
deliveryDataDTO.setCustomerDeliveryDetailList(customerDeliveryDetailList);
LOGGER.info("Excel解析完成,货品明细{}条,客户配送明细{}条",
productDetailList.size(), customerDeliveryDetailList.size());
// 空页签跳过货品明细与客户配送明细均为空时视为非数据页签说明页/空表不算失败
if (productDetailList.isEmpty() && customerDeliveryDetailList.isEmpty()) {
LOGGER.info("页签[{}]无货品明细与客户配送明细,跳过", sheetName);
return null;
}
if (productDetailList.isEmpty()) {
throw new Exception("未解析到产品明细数据,请检查模板中是否填写了產品編號");
}
LOGGER.info("页签[{}]解析完成,货品明细{}条,客户配送明细{}条",
sheetName, productDetailList.size(), customerDeliveryDetailList.size());
List<Map<String,Object>> shipperIds = new ArrayList<>();
Set<Long> processedShipperIds = new HashSet<>();
// ====== 第一步物料编号存在性校验落库前先收集全部缺失编号不立即报错 ======
Map<String, MaterialBaseInfoPO> materialCache = new LinkedHashMap<>();
List<String> missingMaterials = new ArrayList<>();
for (ProductDetail productDetail : productDetailList) {
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
if (materialBaseInfoPO != null && materialBaseInfoPO.getShipperId() != null) {
Long shipperId = materialBaseInfoPO.getShipperId();
if (processedShipperIds.add(shipperId)) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", shipperId);
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
String productCode = productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim();
if (productCode.isEmpty()) {
throw new Exception("出库明细存在未填写產品編號的行,请补全后重新导入");
}
if (materialCache.containsKey(productCode)) {
continue;
}
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId());
if (materialBaseInfoPO == null) {
missingMaterials.add(productCode);
} else {
materialCache.put(productCode, materialBaseInfoPO);
}
}
// ====== 第二步客户编号存在性校验落库前先收集全部缺失编号不立即报错 ======
Map<String, UserPo> customerCache = new HashMap<>();
List<String> missingCustomers = new ArrayList<>();
for (CustomerDeliveryDetail customerDeliveryDetail : customerDeliveryDetailList) {
String customerCode = customerDeliveryDetail.getCustomerCode() == null ? "" : customerDeliveryDetail.getCustomerCode().trim();
if (customerCode.isEmpty()) {
throw new Exception("客户配送明细存在未填写客户编号的行,请补全后重新导入");
}
if (customerCache.containsKey(customerCode)) {
continue;
}
UserPo customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId());
if (customer == null || customer.getUserId() == null) {
missingCustomers.add(customerCode);
} else {
customerCache.put(customerCode, customer);
}
}
// ====== 第三步物料/客户/库存三类校验全部完成后统一报错不再分阶段各自中断 ======
List<String> problems = new ArrayList<>();
if (!missingMaterials.isEmpty()) {
problems.add("物料[" + String.join("", missingMaterials) + "]不存在,请先维护物料信息");
}
if (!missingCustomers.isEmpty()) {
problems.add("客户[" + String.join("", missingCustomers) + "]不存在,请先维护客户信息");
}
// 库存预检只对已存在的物料比较批次库存不存在的物料问题已收集不足的同样收集
if (!productDetailList.isEmpty()) {
Set<String> insufficientMaterials = new HashSet<>();
for (ProductDetail productDetail : productDetailList) {
// materialCache key trim 过的编码取用时同样 trim避免 Excel 编号带空格导致校验被静默跳过
String productCode = productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim();
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(productCode);
if (materialBaseInfoPO == null) {
continue; // 物料不存在的问题已收集
}
BigDecimal requireQty = new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP);
List<ReservationMaterialInventoryPO> inventoryList = stockInOrderMapper.getkcId(
materialBaseInfoPO.getMaterialBaseInfoId(), requireQty, warehouseId);
if (inventoryList == null || inventoryList.isEmpty()) {
insufficientMaterials.add(productCode);
}
}
if (!insufficientMaterials.isEmpty()) {
problems.add("物料[" + String.join("", insufficientMaterials) + "]"
+ (warehouseId != null && warehouseId > 0 ? "在当前仓库" : "") + "批次库存数量少于出库数量");
}
}
if (!problems.isEmpty()) {
throw new Exception(String.join("", problems) + ",请处理后重新导入");
}
// ====== 后续处理编号均已存在货主归属校验建单落库 ======
// 物料档案全仓共享不属于任何仓库仓库约束在下方"分配批次"环节生效
// getkcId warehouseId 过滤分不到当前仓库的批次即报"批次库存数量少于出库数量"不会取到其他仓库的库存
for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) {
if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", materialBaseInfoPO.getShipperId());
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
}
}
@@ -284,10 +439,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
ReservationStockOutOrderDO reservationStockOutOrderDO = new ReservationStockOutOrderDO();
// 生成出库业务单号
String orderNumber = reservationStockOutOrderApplicationService.generateReserveOrderNumber();
reservationStockOutOrderDO.setOutOrderNumber(orderNumber);
reservationStockOutOrderDO.setBusinessOrderNo(orderNumber);
// 出库业务单号不在校验阶段生成多页签时全部页签落库前都未入库逐张生成会取到相同号
// 单号在统一落库阶段第二阶段逐张生成后回填单据/明细/结果
// 货主信息有货品明细时从解析结果取无货品明细时从登录用户取
if (!shipperIds.isEmpty()) {
reservationStockOutOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
@@ -296,6 +449,12 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
reservationStockOutOrderDO.setNeedTransportFlag("1");
reservationStockOutOrderDO.setNeedDeclareFlag("1");
// 指定仓库时回填单据仓库信息此前导入单仓库为空
if (warehouseId != null && warehouseId > 0) {
reservationStockOutOrderDO.setWarehouseId(warehouseId);
reservationStockOutOrderDO.setWarehouseCode(stockInOrderMapper.getWarehouseCode(warehouseId));
reservationStockOutOrderDO.setWarehouseName(stockInOrderMapper.getWarehouseName(warehouseId));
}
reservationStockOutOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
reservationStockOutOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
reservationStockOutOrderDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
@@ -330,19 +489,31 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
for (ProductDetail productDetail : productDetailList) {
ReservationOutMaterialDetailDO reservationOutMaterialDetailDO = new ReservationOutMaterialDetailDO();
reservationOutMaterialDetailDO.setOutOrderNumber(reservationStockOutOrderDO.getOutOrderNumber());
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
// 物料信息取自前置校验的缓存已确认存在避免逐行重复查库key 与存入侧一致使用 trim 后的编码
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(
productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim());
reservationOutMaterialDetailDO.setQuantity(new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
if (materialBaseInfoPO != null) {
Long materialBaseInfoId = materialBaseInfoPO.getMaterialBaseInfoId();
List<ReservationMaterialInventoryPO> reservationMaterialInventoryPOS = stockInOrderMapper.getkcId(materialBaseInfoId, new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
List<ReservationMaterialInventoryPO> reservationMaterialInventoryPOS = stockInOrderMapper.getkcId(materialBaseInfoId, new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP), warehouseId);
if (reservationMaterialInventoryPOS != null&&reservationMaterialInventoryPOS.size() > 0) {
ReservationMaterialInventoryPO reservationMaterialInventoryPO = reservationMaterialInventoryPOS.get(0);
Long materialInventoryId = reservationMaterialInventoryPO.getMaterialInventoryId();
String batchNumber = reservationMaterialInventoryPO.getBatchNumber();
String lotNumber = reservationMaterialInventoryPO.getLotNumber();
Long warehouseId = reservationMaterialInventoryPO.getWarehouseId();
// 库存记录所在仓库指定导入仓库时getkcId 已按该仓库过滤两者一致
Long invWarehouseId = reservationMaterialInventoryPO.getWarehouseId();
String warehouseCode = reservationMaterialInventoryPO.getWarehouseCode();
String warehouseName = reservationMaterialInventoryPO.getWarehouseName();
// 兜底部分历史库存记录只存了仓库ID未存编码/名称按ID查仓库表补齐避免明细仓库显示不全
if (invWarehouseId != null && invWarehouseId > 0) {
if (warehouseCode == null || warehouseCode.trim().isEmpty()) {
warehouseCode = stockInOrderMapper.getWarehouseCode(invWarehouseId);
}
if (warehouseName == null || warehouseName.trim().isEmpty()) {
warehouseName = stockInOrderMapper.getWarehouseName(invWarehouseId);
}
}
String inOrderNumber = reservationMaterialInventoryPO.getInOrderNumber();
BigDecimal inventoryQuantity = reservationMaterialInventoryPO.getInventoryQuantity();
String unit = reservationMaterialInventoryPO.getUnit();
@@ -350,7 +521,7 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationOutMaterialDetailDO.setMaterialInventoryId(materialInventoryId);
reservationOutMaterialDetailDO.setBatchNumber(batchNumber);
reservationOutMaterialDetailDO.setLotNo(lotNumber);
reservationOutMaterialDetailDO.setWarehouseId(warehouseId);
reservationOutMaterialDetailDO.setWarehouseId(invWarehouseId);
reservationOutMaterialDetailDO.setWarehouseCode(warehouseCode);
reservationOutMaterialDetailDO.setWarehouseName(warehouseName);
reservationOutMaterialDetailDO.setInOrderNumber(inOrderNumber);
@@ -360,7 +531,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationOutMaterialDetailDO.setInventoryQuantity(inventoryQuantity);
reservationOutMaterialDetailDO.setUnitName(unitName);
} else {
throw new RuntimeException("物料批次库存数量少于出库数量!");
throw new RuntimeException("物料[" + productDetail.getProductCode() + "]"
+ (warehouseId != null && warehouseId > 0 ? "在当前仓库" : "") + "批次库存数量少于出库数量!");
}
reservationOutMaterialDetailDO.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId());
reservationOutMaterialDetailDO.setMaterialCode(materialBaseInfoPO.getMaterialCode());
@@ -389,18 +561,44 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationOutMaterialDetailDOList.add(reservationOutMaterialDetailDO);
}
reservationStockOutOrderDO.setMaterialDetailList(reservationOutMaterialDetailDOList);
reservationStockOutOrderApplicationService.insert(reservationStockOutOrderDO);
// 组装完成暂存不落库由调用方在全部页签校验通过后统一插入整体事务任一页签失败全部回滚
PreparedDeliveryOrder prepared = new PreparedDeliveryOrder();
prepared.order = reservationStockOutOrderDO;
prepared.customerDeliveryDetailList = customerDeliveryDetailList;
prepared.customerCache = customerCache;
SheetImportResultDTO result = new SheetImportResultDTO();
result.setSheetName(sheetName);
result.setMaterialQuantity(productDetailList.size());
result.setQuantity(reservationOutMaterialDetailDOList.stream()
.map(ReservationOutMaterialDetailDO::getQuantity).filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
result.setDeliveryData(deliveryDataDTO);
prepared.result = result;
return prepared;
}
/**
* 客户配送关联落库原出库导入方法内的保存段挪到统一落库阶段逐页签执行
*/
private void saveDeliveryLinks(PreparedDeliveryOrder prepared) {
ReservationStockOutOrderDO reservationStockOutOrderDO = prepared.order;
Map<String, UserPo> customerCache = prepared.customerCache;
// 客户配送明细判空处理为空时跳过关联保存
for (CustomerDeliveryDetail customerDeliveryDetail : customerDeliveryDetailList) {
for (CustomerDeliveryDetail customerDeliveryDetail : prepared.customerDeliveryDetailList) {
ReservationDeliveryDetailsLink reservationDeliveryDetailsLink = new ReservationDeliveryDetailsLink();
reservationDeliveryDetailsLink.setInvoiceNumber(customerDeliveryDetail.getInvoiceNo());
reservationDeliveryDetailsLink.setAmount(customerDeliveryDetail.getAmount());
reservationDeliveryDetailsLink.setCustomerNumber(customerDeliveryDetail.getCustomerCode());
reservationDeliveryDetailsLink.setDeliveryName(customerDeliveryDetail.getCustomerName());
String customerCode = customerDeliveryDetail.getCustomerCode();
Long userId = stockOutOrderMapper.queryShipperId(customerCode, loginUser.getUserPo().getOrganizationId());
reservationDeliveryDetailsLink.setCustomerId(userId);
String customerCode = customerDeliveryDetail.getCustomerCode() == null ? "" : customerDeliveryDetail.getCustomerCode().trim();
// 客户已在前置校验通过客户ID取库中匹配结果名称以库中数据为准自动匹配覆盖模板填写值库中名称为空时保留模板值
UserPo customer = customerCache.get(customerCode);
reservationDeliveryDetailsLink.setCustomerId(customer.getUserId());
if (customer.getUserName() != null && !customer.getUserName().trim().isEmpty()) {
customerDeliveryDetail.setCustomerName(customer.getUserName());
reservationDeliveryDetailsLink.setDeliveryName(customer.getUserName());
}
reservationDeliveryDetailsLink.setBox(customerDeliveryDetail.getBoxCount());
reservationDeliveryDetailsLink.setBag(customerDeliveryDetail.getPackageCount());
reservationDeliveryDetailsLink.setCategory(customerDeliveryDetail.getCategory());
@@ -411,7 +609,6 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
reservationDeliveryDetailsLinkService.save(reservationDeliveryDetailsLink);
}
return deliveryDataDTO;
}
public String generateDateSerialNumber() {
@@ -429,11 +626,10 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
private static final Logger LOGGER = LoggerFactory.getLogger(ReservationStockInOrderImpl.class);
// 默认工作表名称与Excel一致
private static final String DEFAULT_SHEET_NAME = "入库模版";
@Override
public StockInDataDTO parseStockInExcel(MultipartFile file, String sheetName) throws Exception {
@Transactional(rollbackFor = Exception.class)
public List<SheetImportResultDTO> parseStockInExcel(MultipartFile file, String sheetName) throws Exception {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 1. 校验文件
if (file.isEmpty()) {
@@ -441,70 +637,188 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
throw new IllegalArgumentException("Excel文件不能为空");
}
// 2. 校验工作表名称默认使用"入库模版"
String targetSheetName = (sheetName == null || sheetName.trim().isEmpty())
? DEFAULT_SHEET_NAME : sheetName.trim();
// 2. 多页签模式未显式指定页签名时文件中每个工作表独立解析为一张入库单显式指定则保持单页签行为
byte[] fileBytes = file.getBytes();
List<String> sheetNames;
if (sheetName != null && !sheetName.trim().isEmpty()) {
sheetNames = Collections.singletonList(sheetName.trim());
} else {
sheetNames = listSheetNames(fileBytes);
}
// 3. 使用EasyExcel解析指定监听器
// ====== 第一阶段逐页签解析+校验+组装不落库失败按页签聚合收集全部页签处理完统一报错 ======
List<PreparedStockInOrder> preparedOrders = new ArrayList<>();
List<String> sheetProblems = new ArrayList<>();
// 本文件内已使用的模板OrderNO各页签都未入库时 existsByBusinessOrderNo 查不到彼此需按页签追加查重防跨页签重复
Set<String> usedBusinessOrderNos = new HashSet<>();
for (String name : sheetNames) {
try {
PreparedStockInOrder prepared = buildStockInOrder(fileBytes, name, loginUser, usedBusinessOrderNos);
if (prepared != null) {
preparedOrders.add(prepared); // null=空页签跳过
}
} catch (Exception e) {
sheetProblems.add("页签[" + name + "]" + (e.getMessage() == null ? e.toString() : e.getMessage()));
}
}
if (!sheetProblems.isEmpty()) {
throw new Exception(String.join("", sheetProblems));
}
if (preparedOrders.isEmpty()) {
throw new IllegalArgumentException("未解析到产品明细数据,请检查模板中是否填写了產品編號");
}
// ====== 第二阶段全部页签校验通过后统一落库同一事务任一失败整体回滚 ======
List<SheetImportResultDTO> results = new ArrayList<>();
for (PreparedStockInOrder prepared : preparedOrders) {
insertApplication(prepared.order);
// 入库单号在落库阶段生成insertDomain回填到页签结果
prepared.result.setOrderNumber(prepared.order.getInOrderNumber());
results.add(prepared.result);
}
return results;
}
/** 入库单页签解析/校验/组装完成后的暂存容器(统一落库阶段使用) */
private static class PreparedStockInOrder {
ReservationStockInOrderDO order;
SheetImportResultDTO result;
}
/**
* 单个页签的入库单解析+校验+组装不落库由调用方在全部页签通过后统一插入
* 空页签未解析到产品明细返回 null 由调用方跳过
*/
private PreparedStockInOrder buildStockInOrder(byte[] fileBytes, String sheetName, LoginUser loginUser, Set<String> usedBusinessOrderNos) throws Exception {
// 3. 使用EasyExcel解析指定监听器多页签模式下 sheetName 为实际页签名
StockInExcelListener listener = new StockInExcelListener();
try (InputStream inputStream = file.getInputStream()) {
try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
EasyExcel.read(inputStream, listener)
.sheet(targetSheetName) // 指定工作表
.sheet(sheetName) // 指定工作表
.headRowNumber(0) // 从第1行开始解析不跳过任何行
.doRead();
}
// 4. 封装DTO返回
// 4. 封装DTO
StockInDataDTO resultDTO = new StockInDataDTO();
resultDTO.setStockInHeader(listener.getStockInHeader());
resultDTO.setProductDetailList(listener.getProductDetailList());
LOGGER.info("Excel解析成功,工作表:{},产品明细数:{}",
targetSheetName, resultDTO.getProductDetailList().size());
LOGGER.info("页签[{}]解析成功,产品明细数:{}", sheetName, resultDTO.getProductDetailList().size());
List<StockInProductDetail> productDetailList = resultDTO.getProductDetailList();
// 空页签跳过该页签未解析到产品明细时视为非数据页签说明页/空表不算失败
if (productDetailList == null || productDetailList.isEmpty()) {
return null;
}
StockInHeader stockInHeader = resultDTO.getStockInHeader();
// ====== 第一步物料编号存在性校验模板只填物料编号即可名称按编号自动匹配 ======
// 查询结果为空说明库中没有该物料先收集全部缺失编号不立即报错
Map<String, MaterialBaseInfoPO> materialCache = new LinkedHashMap<>();
List<String> missingMaterials = new ArrayList<>();
for (StockInProductDetail productDetail : productDetailList) {
String productCode = productDetail.getProductCode();
if (materialCache.containsKey(productCode)) {
continue;
}
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId());
if (materialBaseInfoPO == null) {
missingMaterials.add(productCode);
} else {
materialCache.put(productCode, materialBaseInfoPO);
}
}
// ====== 第二步客户编号存在性校验先收集全部缺失编号不立即报错 ======
Set<String> customerCodes = new LinkedHashSet<>();
for (StockInProductDetail productDetail : productDetailList) {
if (productDetail.getCustomerCode() != null && !productDetail.getCustomerCode().trim().isEmpty()) {
customerCodes.add(productDetail.getCustomerCode().trim());
}
}
Map<String, UserPo> customerCache = new HashMap<>();
List<String> missingCustomers = new ArrayList<>();
for (String customerCode : customerCodes) {
UserPo customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId());
if (customer == null || customer.getUserId() == null) {
missingCustomers.add(customerCode);
} else {
customerCache.put(customerCode, customer);
}
}
// ====== 第三步物料/客户存在性校验全部完成后统一提示两类编号各自有哪些不存在 ======
if (!missingMaterials.isEmpty() || !missingCustomers.isEmpty()) {
List<String> problems = new ArrayList<>();
if (!missingMaterials.isEmpty()) {
problems.add("物料[" + String.join("", missingMaterials) + "]不存在,请先维护物料信息");
}
if (!missingCustomers.isEmpty()) {
problems.add("客户[" + String.join("", missingCustomers) + "]不存在,请先维护客户信息");
}
throw new ServiceException(String.join("", problems) + ",维护完成后重新导入");
}
// ====== 后续处理编号均已存在名称自动匹配货主归属与一致性校验建单 ======
// 明细回填库中匹配到的物料名称自动匹配模板中的名称列可不填/以库中为准库中名称为空时保留模板值
for (StockInProductDetail productDetail : productDetailList) {
String materialName = materialCache.get(productDetail.getProductCode()).getMaterialName();
if (materialName != null && !materialName.trim().isEmpty()) {
productDetail.setProductName(materialName);
}
}
// 物料归属货主集合用于客户编号一致性校验及未填客户编号时的兜底取值
List<Map<String,Object>> shipperIds = new ArrayList<>();
Set<Long> processedShipperIds = new HashSet<>();
for (StockInProductDetail productDetail : productDetailList) {
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
// 物料在当前登录组织下不存在时给出明确提示避免后续货主取值越界导致500
if (materialBaseInfoPO == null) {
throw new ServiceException("产品编号[" + productDetail.getProductCode() + "]在当前组织下不存在,请核对产品编号或使用物料所属组织的账号导入");
}
if (materialBaseInfoPO.getShipperId() != null) {
Long shipperId = materialBaseInfoPO.getShipperId();
if (processedShipperIds.add(shipperId)) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", shipperId);
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
}
for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) {
if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", materialBaseInfoPO.getShipperId());
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
}
}
if (shipperIds.isEmpty()) {
throw new ServiceException("导入的产品均未配置货主,请先在物料档案中维护货主信息");
if (shipperIds.size() > 1) {
StringBuilder shipperCodeStr = new StringBuilder();
for (Map<String, Object> shipper : shipperIds) {
if (shipperCodeStr.length() > 0) {
shipperCodeStr.append("");
}
shipperCodeStr.append(shipper.get("shipperCode"));
}
throw new ServiceException("导入的物料不属于同一货主(货主编号:" + shipperCodeStr + "),一个入库单只能属于一个货主");
}
// 客户取值模板填了客户编号时取库中匹配结果编号/名称均为自动匹配值
UserPo customer = null;
if (customerCodes.size() > 1) {
throw new ServiceException("导入明细中存在多个客户编号(" + String.join("", customerCodes) + "),一个入库单只能属于一个客户");
}
if (!customerCodes.isEmpty()) {
String customerCode = customerCodes.iterator().next();
customer = customerCache.get(customerCode);
// 客户编号与物料归属货主一致性校验不一致拦截提示两者编号货主编码未维护时提示货主名称
if (!shipperIds.isEmpty() && !customer.getUserId().equals(shipperIds.get(0).get("shipperId"))) {
Object materialShipperCode = shipperIds.get(0).get("shipperCode");
String materialShipperDisplay = (materialShipperCode != null && !materialShipperCode.toString().trim().isEmpty())
? "货主编号[" + materialShipperCode + "]" : "货主[" + shipperIds.get(0).get("shipperName") + "]";
throw new ServiceException("客户编号[" + customerCode + "]与物料归属" + materialShipperDisplay + "]不一致,请核对后再导入");
}
stockInHeader.setCustomerCode(customerCode);
stockInHeader.setCustomerName(customer.getUserName());
}
ReservationStockInOrderDO reservationStockInOrderDO = new ReservationStockInOrderDO();
StockInHeader stockInHeader = resultDTO.getStockInHeader();
// 业务单号优先使用模板自带OrderNO与系统已有入库业务单号重复时提示
// 模板未填单号时 R+年月日+当日累加 规则生成如R260717001R260717002
// 业务单号优先使用模板自带OrderNO不做格式规则校验与系统已有业务单号重复时提示不允许导入
// 入库业务单号(in_order_number)不使用模板单号统一由系统原本逻辑生成见insertDomain
// 模板未填单号时保持系统逻辑业务单号默认=入库业务单号
String templateOrderNo = stockInHeader.getOrderNo() == null ? "" : stockInHeader.getOrderNo().trim();
if (!templateOrderNo.isEmpty()) {
// 先校验模板单号是否符合规则R+年月日+3位及以上序号如R260717001
validateTemplateOrderNo(templateOrderNo);
// 跨两张入库单表查重与手工新增取号逻辑保持一致
// 注意本类字段命名易混淆stockInOrderMapper为reservation表reservationStockInOrderMapper为reserve表
boolean duplicated = stockInOrderMapper.existsByOrderNumber(templateOrderNo) > 0
|| reservationStockInOrderMapper.countByInOrderNumber(templateOrderNo) > 0;
if (duplicated) {
throw new ServiceException("入库业务单号已存在:" + templateOrderNo + ",请修改模板中的OrderNO后重新导入");
// 按业务单号查重入库业务单表 business_order_no 字段
if (stockInOrderMapper.existsByBusinessOrderNo(templateOrderNo) > 0) {
throw new ServiceException("业务单号已存在:" + templateOrderNo + ",请修改模板中的OrderNO后重新导入");
}
// 多页签查重各页签都未入库时上面的库查重互相看不见需对本文件内已使用的OrderNO追加拦截
if (!usedBusinessOrderNos.add(templateOrderNo)) {
throw new ServiceException("业务单号已存在:" + templateOrderNo + "(与本文件其他页签的OrderNO重复),请修改模板中的OrderNO后重新导入");
}
reservationStockInOrderDO.setBusinessOrderNo(templateOrderNo);
}
if (templateOrderNo.isEmpty()) {
templateOrderNo = reservationStockInOrderDomainService.generateReserveOrderNumber();
}
reservationStockInOrderDO.setInOrderNumber(templateOrderNo);
reservationStockInOrderDO.setBusinessOrderNo(templateOrderNo);
reservationStockInOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
reservationStockInOrderDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
reservationStockInOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
@@ -523,9 +837,19 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationStockInOrderDO.setIssueStatus(0);
reservationStockInOrderDO.setSalesmanId(loginUser.getUserPo().getUserId().toString());
reservationStockInOrderDO.setSalesmanName(loginUser.getUserPo().getUserName());
reservationStockInOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
reservationStockInOrderDO.setShipperName((String) shipperIds.get(0).get("shipperName"));
reservationStockInOrderDO.setShipperCode((String) shipperIds.get(0).get("shipperCode"));
// 单据货主模板填了客户编号时以客户为准编号/名称均为库中自动匹配值未填时取物料归属货主
if (customer != null) {
reservationStockInOrderDO.setShipperId(customer.getUserId());
reservationStockInOrderDO.setShipperName(customer.getUserName());
reservationStockInOrderDO.setShipperCode(customer.getUserMemberCode());
} else {
if (shipperIds.isEmpty()) {
throw new ServiceException("导入的产品均未配置货主,请先在物料档案中维护货主信息,或在模板中填写客户编号");
}
reservationStockInOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
reservationStockInOrderDO.setShipperName((String) shipperIds.get(0).get("shipperName"));
reservationStockInOrderDO.setShipperCode((String) shipperIds.get(0).get("shipperCode"));
}
reservationStockInOrderDO.setWarehouseDate(stockInHeader.getStockInDate());
reservationStockInOrderDO.setStorageTime(stockInHeader.getStockInDate());
reservationStockInOrderDO.setExpectTime(stockInHeader.getStockInDate());
@@ -541,7 +865,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationInMaterialDetailDO.setCreateBy(loginUser.getUserPo().getUserId());
reservationInMaterialDetailDO.setCreateByName(loginUser.getUserPo().getUserName());
reservationInMaterialDetailDO.setCreateTime(new Date());
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(stockInProductDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
// 物料信息取自前置校验的缓存已确认存在避免逐行重复查库
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(stockInProductDetail.getProductCode());
// 计划数量取模版"件数"同时件数字段落库用于明细列表回显
reservationInMaterialDetailDO.setQuantity(new BigDecimal(stockInProductDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
reservationInMaterialDetailDO.setPieceCount(new BigDecimal(stockInProductDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
@@ -568,8 +893,19 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationInMaterialDetailDOList.add(reservationInMaterialDetailDO);
}
reservationStockInOrderDO.setMaterialDetailList(reservationInMaterialDetailDOList);
insertApplication(reservationStockInOrderDO);
return resultDTO;
// 组装完成暂存不落库由调用方在全部页签校验通过后统一插入整体事务任一页签失败全部回滚
SheetImportResultDTO sheetResult = new SheetImportResultDTO();
sheetResult.setSheetName(sheetName);
sheetResult.setMaterialQuantity(productDetailList.size());
sheetResult.setQuantity(reservationInMaterialDetailDOList.stream()
.map(ReservationInMaterialDetailDO::getQuantity).filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
sheetResult.setStockInData(resultDTO);
PreparedStockInOrder prepared = new PreparedStockInOrder();
prepared.order = reservationStockInOrderDO;
prepared.result = sheetResult;
return prepared;
}
public Boolean insertApplication(ReservationStockInOrderDO stockInOrderDO) {
@@ -597,25 +933,6 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
/**
* 校验模板自带业务单号是否符合规则R + 年月日(yyMMdd) + 3位及以上序号
* 示例R260717001R260717002
* 格式不符或年月日部分非有效日期如R261332001时抛出提示
*/
private void validateTemplateOrderNo(String orderNo) {
// R + 6位年月日 + 至少3位数字序号
if (!orderNo.matches("R\\d{9,}")) {
throw new ServiceException("入库业务单号格式不正确:正确格式为R+年月日+3位序号,如R260717001");
}
try {
SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd");
fmt.setLenient(false);
fmt.parse(orderNo.substring(1, 7));
} catch (java.text.ParseException e) {
throw new ServiceException("入库业务单号格式不正确:正确格式为R+年月日+3位序号,如R260717001");
}
}
@Transactional(rollbackFor = Exception.class)
public Boolean insertDomain(ReservationStockInOrderDO stockInOrderDO) {
// 仓库信息校验保留原有行为仓库信息获取失败直接报错
@@ -624,12 +941,16 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
if (!"200".equals(String.valueOf(ajaxResult.get("code")))) {
throw new ServiceException("获取仓库信息失败");
}
// 入库单号调用方设置如导入时优先使用模板自带OrderNO且已校验不重复则沿用不再覆盖
// 未设置时按 R+年月日+当日累加 规则生成如R260717001R260717002跨reservation/reserve两表取号防撞
// 入库单号调用方设置时按 R+年月日+当日累加 规则生成如R260717001R260717002
// 跨reservation/reserve两表取号防撞导入时不再使用模板OrderNO作为入库单号模板单号只作业务单号
if (stockInOrderDO.getInOrderNumber() == null || stockInOrderDO.getInOrderNumber().trim().isEmpty()) {
stockInOrderDO.setInOrderNumber(reservationStockInOrderDomainService.generateReserveOrderNumber());
}
stockInOrderDO.setBusinessOrderNo(stockInOrderDO.getInOrderNumber());
// 业务单号未设置时默认=入库单号模板未填OrderNO的导入及常规场景
// 导入且模板带OrderNO时由调用方已设置此处沿用不覆盖
if (stockInOrderDO.getBusinessOrderNo() == null || stockInOrderDO.getBusinessOrderNo().trim().isEmpty()) {
stockInOrderDO.setBusinessOrderNo(stockInOrderDO.getInOrderNumber());
}
//统计总计划数
BigDecimal quantity = stockInOrderDO.getMaterialDetailList().stream().map(ReservationInMaterialDetailDO::getQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
stockInOrderDO.setQuantity(quantity);
@@ -0,0 +1,31 @@
package com.mhd.oms.domain.reservationStockInOrder.repository.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 多页签导入的单页签结果出库/入库导入通用
* 一个Excel文件中每个工作表独立解析为一张单据每个页签对应一条结果
*/
@Data
public class SheetImportResultDTO {
/** 来源工作表名 */
private String sheetName;
/** 生成的单据号(出库=出库业务单号,入库=入库业务单号;入库单号在落库阶段生成后回填) */
private String orderNumber;
/** 物料明细行数 */
private Integer materialQuantity;
/** 明细数量合计 */
private BigDecimal quantity;
/** 出库单解析数据(仅出库导入时有值) */
private DeliveryDataDTO deliveryData;
/** 入库单解析数据(仅入库导入时有值) */
private StockInDataDTO stockInData;
}
@@ -14,6 +14,10 @@ public class StockInHeader {
private Date stockInDate;
// 订单编号Excel第一行第4列
private String orderNo;
// 客户编号明细列头动态定位导入校验客户存在性时回填
private String customerCode;
// 客户名称按客户编号自动匹配库中名称后回填
private String customerName;
// Getter和Setter方法
public Date getStockInDate() {
@@ -31,4 +35,20 @@ public class StockInHeader {
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getCustomerCode() {
return customerCode;
}
public void setCustomerCode(String customerCode) {
this.customerCode = customerCode;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
}
@@ -9,10 +9,12 @@ import java.util.Date;
*/
@Data
public class StockInProductDetail {
// 产品编号Excel第1列
// 产品编号按列头动态定位
private String productCode;
// 产品名称Excel第2列
// 产品名称按列头动态定位可不填导入时按产品编号自动匹配库中物料名称
private String productName;
// 客户编号按列头动态定位可不填导入时按客户编号自动匹配库中客户名称
private String customerCode;
// 箱含量Excel第3列
private Integer boxCapacity;
// 件数Excel第4列
@@ -37,6 +39,14 @@ public class StockInProductDetail {
this.productName = productName;
}
public String getCustomerCode() {
return customerCode;
}
public void setCustomerCode(String customerCode) {
this.customerCode = customerCode;
}
public Integer getBoxCapacity() {
return boxCapacity;
}
@@ -580,6 +580,11 @@ public class ReservationStockInOrderDomainService {
InMaterialDetail inMaterialDetail = new InMaterialDetail();
BeanUtils.copyProperties(reservationInMaterialDetail, inMaterialDetail, "materialDetailId");
inMaterialDetail.setOmsInMaterialDetail(String.valueOf(reservationInMaterialDetail.getMaterialDetailId()));
// 过期日期为空且饮用日期有值时用饮用日期补齐过期日期食品业务饮用日期即效期
// 确保下发WMS后入库/收货/库存链路的过期日期能正常回显
if (inMaterialDetail.getExpiryDate() == null && inMaterialDetail.getDrinkDate() != null) {
inMaterialDetail.setExpiryDate(inMaterialDetail.getDrinkDate());
}
inMaterialDetailList.add(inMaterialDetail);
}
wmsServiceFeign.omsInOrderAddDetail(inMaterialDetailList); // 推明细会触发WMS自动审核
@@ -2,6 +2,7 @@ package com.mhd.oms.domain.reservationStockOutOrder.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
//import com.mhd.oms.domain.reservationReservationStockOutOrder.entity.ReservationReservationStockOutOrder;
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;
@@ -36,7 +37,12 @@ public interface ReservationStockOutOrderMapper extends BaseMapper<ReservationSt
*/
List<ReservationOutMaterialDetailPO> queryOutStockNoticePrintDetail(ReservationStockOutOrderDO stockOutOrderDO);
Long queryShipperId(@Param("userMemberCode") String userMemberCode,@Param("organizationId") Long organizationId);
/**
* 按客户编号+组织查询客户用户表返回客户ID/编号/名称
* 客户编号兼容两种编码USER.USER_MEMBER_CODE客户编码 USER_SHIPPER.CUSTOMER_NC_CODENC客户代码/khdm任一命中即可
* 用于出入库导入时校验客户是否存在并自动匹配客户名称结果为空表示库中无该客户
*/
UserPo queryCustomerByCode(@Param("userMemberCode") String userMemberCode, @Param("organizationId") Long organizationId);
int countByOrderNumberPrefix(@Param("prefix") String prefix);
@@ -514,6 +514,13 @@ public class ReservationStockOutOrderDomainService {
for (Long inOrderId : inOrderIds) {
ReservationStockOutOrder byId = stockOutOrderService.getById(inOrderId);
String inOrderNumber = byId.getOutOrderNumber();
// 幂等拦截已存在有效执行单时禁止重复下发重复下发会生成新执行单并原样复制明细uniqueId撞执行明细表唯一约束
long activeExecutionCount = executionStockOutOrderService.count(new QueryWrapper<ExecutionStockOutOrder>().lambda()
.eq(ExecutionStockOutOrder::getBusinessOutOrderNumber, inOrderNumber)
.eq(ExecutionStockOutOrder::getDelFlag, 1));
if (activeExecutionCount > 0) {
throw new ServiceException("出库单[" + inOrderNumber + "]已下发,请勿重复下发;如需重新下发请先取消下发");
}
List<String> executionInOrderNumbers = new ArrayList<>();
List<ReservationOutMaterialDetail> materialDetailList = outMaterialDetailService.list(new QueryWrapper<ReservationOutMaterialDetail>().lambda().eq(ReservationOutMaterialDetail::getOutOrderNumber, inOrderNumber));
@@ -689,6 +696,47 @@ public class ReservationStockOutOrderDomainService {
// 按照 warehouseId 分组
Map<Long, List<ReservationOutMaterialDetail>> materialDetailGroupByWarehouse = materialDetailList.stream()
.collect(Collectors.groupingBy(ReservationOutMaterialDetail::getWarehouseId));
//下发前统一校验各仓明细数量>0已关联库存WMS可用库存充足全部通过后再逐仓生成执行单下发
//本方法的事务回滚不了WMS已提交的数据若后面的仓在生成执行单后才失败前面的仓会在WMS残留单据
List<String> issueValidateErrors = new ArrayList<>();
materialDetailGroupByWarehouse.forEach((warehouseId, details) -> {
boolean groupBaseValid = true;
for (ReservationOutMaterialDetail detail : details) {
String materialDesc = detail.getMaterialCode() != null ? detail.getMaterialCode()
: (detail.getMaterialName() != null ? detail.getMaterialName() : String.valueOf(detail.getMaterialDetailId()));
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
issueValidateErrors.add("预约单[" + inOrderNumber + "]物料[" + materialDesc + "]数量为0,无法下发");
groupBaseValid = false;
} else if (detail.getMaterialInventoryId() == null || detail.getMaterialInventoryId() <= 0) {
issueValidateErrors.add("预约单[" + inOrderNumber + "]物料[" + materialDesc + "]未关联库存,无法下发");
groupBaseValid = false;
}
}
if (!groupBaseValid) {
return;
}
//WMS库存校验接口内部会聚合该仓全部明细的需求量后比对可用库存
List<OutMaterialDetail> checkList = new ArrayList<>();
for (ReservationOutMaterialDetail detail : details) {
OutMaterialDetail checkDetail = new OutMaterialDetail();
checkDetail.setMaterialInventoryId(detail.getMaterialInventoryId());
checkDetail.setQuantity(detail.getQuantity());
checkDetail.setMaterialCode(detail.getMaterialCode());
checkDetail.setMaterialName(detail.getMaterialName());
checkList.add(checkDetail);
}
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
String groupWarehouseName = reservationStockInOrderMapper.getWarehouseName(warehouseId);
issueValidateErrors.add("仓库[" + (groupWarehouseName != null ? groupWarehouseName : warehouseId) + "]库存校验失败:"
+ (checkResult == null ? "WMS服务不可用,已阻止下发" : checkResult.get("msg")));
}
});
if (!issueValidateErrors.isEmpty()) {
throw new ServiceException("下发校验未通过(共" + issueValidateErrors.size() + "项):" + String.join("", issueValidateErrors));
}
List<String> executionInOrderNumbers = new ArrayList<>();
materialDetailGroupByWarehouse.forEach((warehouseId, details) -> {
if (warehouseId == null) warehouseId = warehouseId1;
@@ -1213,7 +1261,9 @@ public class ReservationStockOutOrderDomainService {
ReservationDeliveryDetailsLink reservationDeliveryDetailsLink = list.get(0);
String orderNumber = reservationDeliveryDetailsLink.getOrderNumber();
Integer bag = reservationDeliveryDetailsLink.getBag();
ReservationStockOutOrder stockOutOrder = stockOutOrderService.getOne(new QueryWrapper<ReservationStockOutOrder>().lambda().eq(ReservationStockOutOrder::getOutOrderNumber, orderNumber), false);
ReservationStockOutOrder stockOutOrder = stockOutOrderService.getOne(new QueryWrapper<ReservationStockOutOrder>().lambda()
.eq(ReservationStockOutOrder::getOutOrderNumber, orderNumber)
.eq(ReservationStockOutOrder::getDelFlag,1), false);
if (stockOutOrder != null) {
ReservationStockInOrder reservationStockInOrder = new ReservationStockInOrder();
reservationStockInOrder.setPictureUrl(pictureUrl);
@@ -446,4 +446,10 @@ public class ReserveStockInOrderDO extends BaseVOEntity {
@ApiModelProperty("客户名称")
@Excel(name = "客户名称")
private String customerName;
@ApiModelProperty("出入库方向(inAndOutList查询条件): in-入库 out-出库 all/空-全部")
private String inOrOut;
@ApiModelProperty("出入库单号(inAndOutList查询条件,模糊匹配,出库out_order_number/入库in_order_number")
private String orderNumber;
}
@@ -443,4 +443,10 @@ public class ReserveStockInOrderDTO extends ReserveStockInOrderBaseDTO {
@ApiModelProperty("客户名称")
@Excel(name = "客户名称")
private String customerName;
@ApiModelProperty("出入库方向(inAndOutList查询条件): in-入库 out-出库 all/空-全部")
private String inOrOut;
@ApiModelProperty("出入库单号(inAndOutList查询条件,模糊匹配,出库out_order_number/入库in_order_number")
private String orderNumber;
}
@@ -0,0 +1,84 @@
package com.mhd.oms.interfaces.assembler.outEntrustOrder;
import cn.hutool.core.util.ObjectUtil;
import com.mhd.common.core.exception.digitalLogisticsException.DigitalLogisticsException;
import com.mhd.common.core.exception.digitalLogisticsException.UserError;
import com.mhd.common.core.utils.IgnoreNullUtil;
import com.mhd.common.core.utils.bean.BeanUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import com.mhd.oms.interfaces.dto.outEntrustOrder.OutEntrustOrderDTO;
import com.mhd.oms.interfaces.dto.outEntrustOrder.OutEntrustOrderDetailDTO;
import com.mhd.system.api.model.LoginUser;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 出仓委托单Assembler
*
* @author rcx
* @date 2026-09-02
*/
@Component
public class OutEntrustOrderAssembler {
/**
* 转换实体
*/
public OutEntrustOrderDO toDO(OutEntrustOrderDTO outEntrustOrderDTO) {
OutEntrustOrderDO outEntrustOrderDO = new OutEntrustOrderDO();
// 拷贝
BeanUtils.copyProperties(outEntrustOrderDTO, outEntrustOrderDO, IgnoreNullUtil.getNullPropertyNames(outEntrustOrderDTO));
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
String userName = loginUser.getUsername();
// 获取登录人id
Long userId = loginUser.getUserid();
if (outEntrustOrderDTO.getId() != null) {
if (userId != null) {
outEntrustOrderDO.setUpdateBy(userId);
} else {
outEntrustOrderDO.setUpdateBy(0L);
}
outEntrustOrderDO.setUpdateByName(userName);
outEntrustOrderDO.setUpdateTime(new Date());
} else {
if (userId != null) {
outEntrustOrderDO.setCreateBy(userId);
outEntrustOrderDO.setUpdateBy(userId);
} else {
outEntrustOrderDO.setCreateBy(0L);
outEntrustOrderDO.setUpdateBy(0L);
}
outEntrustOrderDO.setCreateByName(userName);
outEntrustOrderDO.setUpdateByName(userName);
outEntrustOrderDO.setCreateTime(new Date());
outEntrustOrderDO.setUpdateTime(new Date());
outEntrustOrderDO.setDelFlag(1);
}
// 明细转换
List<OutEntrustOrderDetailDTO> materialDetailList = outEntrustOrderDTO.getMaterialDetailList();
if (materialDetailList != null) {
outEntrustOrderDO.setMaterialDetailList(toDetailDOList(materialDetailList));
}
return outEntrustOrderDO;
}
/**
* 明细DTO转DO
*/
public List<OutEntrustOrderDetailDO> toDetailDOList(List<OutEntrustOrderDetailDTO> detailDTOList) {
return detailDTOList.stream().map(detailDTO -> {
OutEntrustOrderDetailDO detailDO = new OutEntrustOrderDetailDO();
BeanUtils.copyProperties(detailDTO, detailDO, IgnoreNullUtil.getNullPropertyNames(detailDTO));
return detailDO;
}).collect(Collectors.toList());
}
}
@@ -778,6 +778,16 @@ public class BusinessDocumentOrderDTO extends BaseVOEntity{
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date layDate;
@ApiModelProperty(value = "预计起运时间起")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date layDateStart;
@ApiModelProperty(value = "预计起运时间止")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date layDateEnd;
@ApiModelProperty("入库单号")
private String inOrderNumber;
@@ -0,0 +1,198 @@
package com.mhd.oms.interfaces.dto.outEntrustOrder;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.mhd.common.core.web.domain.BaseVOEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 出仓委托单对象 oms_out_entrust_order
* 表单字段对齐WMS出库管理新增(报关/委托信息块)
*
* @author rcx
* @date 2026-09-02
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class OutEntrustOrderDTO extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
private Long id;
@ApiModelProperty("编号集合(审核/下发/删除用)")
private List<Long> idList;
@ApiModelProperty(name = "组织ID集合")
private List<Long> organizationIdList;
@ApiModelProperty(name = "权限菜单ID")
private Long permissionMenuId;
@ApiModelProperty("委托单号")
private String entrustNumber;
@ApiModelProperty("出库仓库ID(必填)")
private Long warehouseId;
@ApiModelProperty("仓库编码")
private String warehouseCode;
@ApiModelProperty("仓库名称")
private String warehouseName;
@ApiModelProperty("客户ID(货主,必填)")
private Long shipperId;
@ApiModelProperty("客户编码")
private String shipperCode;
@ApiModelProperty("客户名称")
private String shipperName;
@ApiModelProperty("业务员ID(必填)")
private String salesmanId;
@ApiModelProperty("业务员名称")
private String salesmanName;
@ApiModelProperty("订单类型编码(字典outDocumentType)")
private String orderTypeCode;
@ApiModelProperty("订单类型名称")
private String orderTypeName;
@ApiModelProperty("结算币种")
private String settlementCurrency;
@ApiModelProperty("优先级别编码")
private String priorityLevelCode;
@ApiModelProperty("优先级别名称")
private String priorityLevelName;
@ApiModelProperty("是否直接出库: 1-是 2-否")
private Integer directWarehouse;
@ApiModelProperty("是否复核: 1-是 2-否")
private Integer review;
@ApiModelProperty("预计出货时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expectTime;
@ApiModelProperty("出仓日期")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date outboundDate;
@ApiModelProperty("orderDate")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date orderDate;
@ApiModelProperty("完成时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date completeTime;
@ApiModelProperty("To(收货人)")
@JsonProperty("to")
private String toReceiver;
@ApiModelProperty("联系人")
private String contactPerson;
@ApiModelProperty("车型及车牌")
private String vehicleModelPlate;
@ApiModelProperty("委托编号(客户委托单号)")
private String entrustNo;
@ApiModelProperty("Tel电话")
private String tel;
@ApiModelProperty("Fax传真")
private String fax;
@ApiModelProperty("司机签名")
private String driverSign;
@ApiModelProperty("承运商编码")
private String carrier;
@ApiModelProperty("承运商名称")
private String carrierName;
@ApiModelProperty("柜号")
private String containerNo;
@ApiModelProperty("")
private String containerNoName;
@ApiModelProperty("运输方式编码")
private String transportModeCode;
@ApiModelProperty("运输方式名称")
private String transportModeName;
@ApiModelProperty("监管方式编码")
private String supervisionModeCode;
@ApiModelProperty("监管方式名称")
private String supervisionModeName;
@ApiModelProperty("申报地关区")
private String declareCustoms;
@ApiModelProperty("进出境关别")
private String entryExitCustoms;
@ApiModelProperty("起运/运抵国(地区)")
private String departureArrivalCountry;
@ApiModelProperty("海关是否查验货物")
private String customsInspectionFlag;
@ApiModelProperty("是否需要报关(预留,页面隐藏)")
private String needDeclareFlag;
@ApiModelProperty("是否需要运输(预留,页面隐藏)")
private String needTransportFlag;
@ApiModelProperty("生产商")
private String manufacturer;
@ApiModelProperty("报关员姓名及联系方式")
private String customsDeclarerInfo;
@ApiModelProperty("送货地址")
private String deliveryAddr;
@ApiModelProperty("附件url")
private String annexUrl;
@ApiModelProperty("订单状态:1-待审核 2-已审核 3-已驳回 4-进行中 5-已入库")
private Integer orderStatus;
@ApiModelProperty("下发状态:1-未下发 2-已下发")
private Integer issueStatus;
@ApiModelProperty("审核备注(驳回原因,审核时传)")
private String auditRemark;
@ApiModelProperty("申报总数量(服务端按明细汇总,无需传入)")
private BigDecimal declareTotalQuantity;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("查询-创建开始时间(年月日yyyy-MM-dd)")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date beginCreateTime;
@ApiModelProperty("查询-创建结束时间(年月日yyyy-MM-dd,含当天)")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date endCreateTime;
@ApiModelProperty("查询-发票号(模糊匹配明细发票号)")
private String invoiceNo;
@ApiModelProperty("商品明细")
private List<OutEntrustOrderDetailDTO> materialDetailList;
}
@@ -0,0 +1,93 @@
package com.mhd.oms.interfaces.dto.outEntrustOrder;
import com.mhd.common.core.web.domain.BaseVOEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 出仓委托单明细对象 oms_out_entrust_order_detail
* 明细列对齐WMS出库管理新增(出仓委托单明细列)
*
* @author rcx
* @date 2026-09-02
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class OutEntrustOrderDetailDTO extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
private Long id;
@ApiModelProperty("委托单号(关联主表)")
private String entrustNumber;
@ApiModelProperty("物料基础信息ID")
private Long materialBaseInfoId;
@ApiModelProperty("商品编码")
private String materialCode;
@ApiModelProperty("商品名称(物料档案名,与commodityName至少其一)")
private String materialName;
@ApiModelProperty("商品条形码")
private String barCode;
@ApiModelProperty("商品名称(委托单品名,与materialName至少其一)")
private String commodityName;
@ApiModelProperty("商品编号")
private String commodityNo;
@ApiModelProperty("料号")
private String materialNo;
@ApiModelProperty("规格型号")
private String specificationModel;
@ApiModelProperty("SKU码")
private String skucode;
@ApiModelProperty("发票号")
private String invoiceNo;
@ApiModelProperty("申报单位")
private String declareUnit;
@ApiModelProperty("申报数量(必填,大于0)")
private BigDecimal declareQuantity;
@ApiModelProperty("单价")
private BigDecimal unitPrice;
@ApiModelProperty("单位")
private String unit;
@ApiModelProperty("")
private String liter;
@ApiModelProperty("尺寸")
private String size;
@ApiModelProperty("箱数")
private BigDecimal caseCount;
@ApiModelProperty("件数")
private BigDecimal pieceCount;
@ApiModelProperty("出库数量")
private BigDecimal outboundQuantity;
@ApiModelProperty("总净重(千克)")
private BigDecimal totalNetWeight;
@ApiModelProperty("总毛重(千克)")
private BigDecimal totalGrossWeight;
@ApiModelProperty("总体积(立方米)")
private BigDecimal totalVolume;
@ApiModelProperty("总面积")
private BigDecimal totalArea;
@ApiModelProperty("总金额")
private BigDecimal totalAmount;
@ApiModelProperty("币制")
private String currency;
@ApiModelProperty("批次")
private String batchRefNo;
@ApiModelProperty("单据参考号")
private String sheetRefNo;
@ApiModelProperty("原产国")
private String countryOfOrigin;
@ApiModelProperty("箱/托盘号")
private String boxPalletNo;
@ApiModelProperty("备注")
private String remark;
}
@@ -104,7 +104,8 @@ public class ReservationStockOutOrderApi extends BaseController {
{
// 根据出库单号查询出库单再按出库单ID查询详情保证与出库管理页面一致
ReservationStockOutOrder stockOutOrder = stockOutOrderService.getOne(new LambdaQueryWrapper<ReservationStockOutOrder>()
.eq(ReservationStockOutOrder::getOutOrderNumber, outOrderNumber));
.eq(ReservationStockOutOrder::getOutOrderNumber, outOrderNumber)
.eq(ReservationStockOutOrder::getDelFlag,1));
if (stockOutOrder == null) {
return AjaxResult.error("出库单不存在");
}
@@ -122,7 +123,8 @@ public class ReservationStockOutOrderApi extends BaseController {
return AjaxResult.error("orderNumber不能为空");
}
ReservationStockOutOrder stockOutOrder = stockOutOrderService.getOne(new LambdaQueryWrapper<ReservationStockOutOrder>()
.eq(ReservationStockOutOrder::getOutOrderNumber, orderNumber));
.eq(ReservationStockOutOrder::getOutOrderNumber, orderNumber)
.eq(ReservationStockOutOrder::getDelFlag,1));
if (stockOutOrder == null) {
return AjaxResult.error("出库单不存在");
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.reservationStockOutOrder.entity.ReservationStockOutOrder;
import com.mhd.oms.domain.reserveOutMaterialDetail.repository.po.ReserveOutMaterialDetailPO;
import com.mhd.oms.domain.reserveStockInOrder.repository.listener.ValidationException;
import com.mhd.oms.domain.reserveStockInOrder.repository.todo.ReserveStockInOrderDO;
@@ -149,7 +150,8 @@ public class ReserveStockOutOrderApi extends BaseController {
{
// 根据出库单号查询出库单再按出库单ID查询详情保证与出库管理页面一致
ReserveStockOutOrder stockOutOrder = stockOutOrderService.getOne(new LambdaQueryWrapper<ReserveStockOutOrder>()
.eq(ReserveStockOutOrder::getOutOrderNumber, outOrderNumber));
.eq(ReserveStockOutOrder::getOutOrderNumber, outOrderNumber)
.eq(ReserveStockOutOrder::getDelFlag,1));
if (stockOutOrder == null) {
return AjaxResult.error("出库单不存在");
}
@@ -167,7 +169,8 @@ public class ReserveStockOutOrderApi extends BaseController {
return AjaxResult.error("orderNumber不能为空");
}
ReserveStockOutOrder stockOutOrder = stockOutOrderService.getOne(new LambdaQueryWrapper<ReserveStockOutOrder>()
.eq(ReserveStockOutOrder::getOutOrderNumber, orderNumber));
.eq(ReserveStockOutOrder::getOutOrderNumber, orderNumber)
.eq(ReserveStockOutOrder::getDelFlag,1));
if (stockOutOrder == null) {
return AjaxResult.error("出库单不存在");
}
@@ -203,13 +203,15 @@ public class ExecutionStockInOrderApi extends BaseController {
* 上传并解析Excel文件
* @param file Excel文件
* @param sheetName 工作表名称出库导入/参考数据
* @param warehouseId 当前仓库id非空时校验导入物料是否属于该仓库并按该仓库过滤批次库存
* @return 解析后的出库数据
*/
@PostMapping("/parseExcel")
public ResponseEntity<Map<String, Object>> parseExcel(
@RequestParam("file") MultipartFile file) {
@RequestParam("file") MultipartFile file,
@RequestParam(value = "warehouseId", required = false) Long warehouseId) {
try {
DeliveryDataDTO deliveryDataDTO = reservationStockInOrderService.parseExcelFile(file, "");
DeliveryDataDTO deliveryDataDTO = reservationStockInOrderService.parseExcelFile(file, "", warehouseId);
Map<String, Object> result = new HashMap<>();
result.put("result", deliveryDataDTO);
result.put("code",200);
@@ -12,6 +12,7 @@ import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockO
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDTO;
import com.mhd.oms.domain.executionStockOutOrder.service.ExecutionStockOutOrderApplicationService;
import com.mhd.oms.domain.executionStockOutOrder.service.ExecutionStockOutOrderAssembler;
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
import com.mhd.system.api.domain.OutMaterialDetailTZPD;
import com.mhd.system.api.domain.StockOutOrderTZPD;
import io.swagger.annotations.ApiOperation;
@@ -102,7 +103,8 @@ public class ExecutionStockOutOrderApi extends BaseController {
{
// 根据出库单号查询出库单再按出库单ID查询详情保证与出库管理页面一致
ExecutionStockOutOrder stockOutOrder = stockOutOrderService.getOne(new LambdaQueryWrapper<ExecutionStockOutOrder>()
.eq(ExecutionStockOutOrder::getOutOrderNumber, outOrderNumber));
.eq(ExecutionStockOutOrder::getOutOrderNumber, outOrderNumber)
.eq(ExecutionStockOutOrder::getDelFlag,1));
if (stockOutOrder == null) {
return AjaxResult.error("出库单不存在");
}
@@ -120,7 +122,8 @@ public class ExecutionStockOutOrderApi extends BaseController {
return AjaxResult.error("orderNumber不能为空");
}
ExecutionStockOutOrder stockOutOrder = stockOutOrderService.getOne(new LambdaQueryWrapper<ExecutionStockOutOrder>()
.eq(ExecutionStockOutOrder::getOutOrderNumber, orderNumber));
.eq(ExecutionStockOutOrder::getOutOrderNumber, orderNumber)
.eq(ExecutionStockOutOrder::getDelFlag,1));
if (stockOutOrder == null) {
return AjaxResult.error("出库单不存在");
}
@@ -0,0 +1,112 @@
package com.mhd.oms.interfaces.facade.inboundRank;
import com.github.pagehelper.PageInfo;
import com.mhd.common.core.constant.HttpStatus;
import com.mhd.common.core.utils.PageUtils;
import com.mhd.common.core.utils.poi.ExcelUtil;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDTO;
import com.mhd.oms.domain.inboundRank.service.ExecutionInboundRankApplicationService;
import com.mhd.oms.domain.inboundRank.service.ExecutionInboundRankAssembler;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
/**
* 入库排名统计Api
*
* <p>按入仓日期区间筛选商品入库量统计排名OMS 库执行入库单状态/上架数量均不回传真实数据在 WMS
* 故数据源跨库取 WMS 入库表(NGWL_TEST_WMS.stock_in_order/in_material_detail OMS 出库排名跨库修法一致)
* 口径与 WMS /inboundRankApi 完全一致两边数字一致仅已入库(5) + 明细 level=1 主行 +
* 入仓日期(为空的历史单按建档时间兜底) + 件数=实际上架量(SHELVES_QUANTITY)
* 一行 = 一个物料的合计按物料编码合并区间内全部已入库仓单按件数降序排名件数为0的物料不返回
* 返回字段物料编码/货名/规格型号/单位/件数/毛重()/投保总额/体积(CBM)
* 仓单编号/客户编号/存货人/入仓日期为单据级字段仅作查询条件不返回
* 字段入仓日期/仓单编号/客户编号/存货人/货名/件数/毛重()/投保总额/体积(CBM)
* 与出库排名(/executionOutboundRankApi)配对前端用 tab 页区分</p>
*
* @author rcx
* @date 2026-08-31
*/
@RestController
@RequestMapping("/executionInboundRankApi")
@Slf4j
public class ExecutionInboundRankApi {
@Resource
private ExecutionInboundRankAssembler executionInboundRankAssembler;
@Autowired
private ExecutionInboundRankApplicationService executionInboundRankApplicationService;
/**
* 分页查询商品入库排名列表含合计行
*/
@ApiOperation("分页查询商品入库排名列表")
@GetMapping("/queryMaterialRankList")
public HashMap<String, Object> queryMaterialRankList(ExecutionInboundRankDTO executionInboundRankDTO) {
//转换实体
ExecutionInboundRankDO executionInboundRankDO = executionInboundRankAssembler.toDO(executionInboundRankDTO);
startPage();
List<ExecutionInboundMaterialRankPO> list = executionInboundRankApplicationService.queryMaterialRankList(executionInboundRankDO);
ExecutionInboundRankTotalPO total = executionInboundRankApplicationService.queryMaterialRankTotal(executionInboundRankDO);
return getDataTableWithTotal(list, total);
}
/**
* 导出商品入库排名Excel
*/
@ApiOperation("导出商品入库排名Excel")
@GetMapping("/exportMaterialRank")
public void exportMaterialRank(HttpServletResponse response, ExecutionInboundRankDTO executionInboundRankDTO) throws Exception {
//转换实体不分页导出全量
ExecutionInboundRankDO executionInboundRankDO = executionInboundRankAssembler.toDO(executionInboundRankDTO);
List<ExecutionInboundMaterialRankPO> list = executionInboundRankApplicationService.exportMaterialRank(executionInboundRankDO);
String fileName = URLEncoder.encode("入库排名统计", "UTF-8").replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
ExcelUtil<ExecutionInboundMaterialRankPO> util = new ExcelUtil<>(ExecutionInboundMaterialRankPO.class);
util.exportExcel(response, list, "入库排名统计");
}
/**
* 分页结果 + 合计行data/total/code/msg 之外追加
* totalPieceCount/totalGrossWeightTon/totalInsureAmount/totalVolume/totalOrderCount
*/
private HashMap<String, Object> getDataTableWithTotal(List<?> list, ExecutionInboundRankTotalPO 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("totalPieceCount", total.getTotalPieceCount());
result.put("totalGrossWeightTon", total.getTotalGrossWeightTon());
result.put("totalInsureAmount", total.getTotalInsureAmount());
result.put("totalVolume", total.getTotalVolume());
result.put("totalOrderCount", total.getTotalOrderCount());
}
return result;
}
/**
* 设置请求分页数据
*/
protected void startPage() {
PageUtils.startPage();
}
}
@@ -0,0 +1,111 @@
package com.mhd.oms.interfaces.facade.outEntrustOrder;
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.application.service.outEntrustOrder.OutEntrustOrderApplicationService;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.oms.interfaces.assembler.outEntrustOrder.OutEntrustOrderAssembler;
import com.mhd.oms.interfaces.dto.outEntrustOrder.OutEntrustOrderDTO;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
/**
* 出仓委托单Api
*
* @author rcx
* @date 2026-09-02
*/
@Slf4j
@RestController
@RequestMapping("/outEntrustOrderApi")
public class OutEntrustOrderApi extends BaseController {
@Autowired
private OutEntrustOrderApplicationService outEntrustOrderApplicationService;
@Resource
private OutEntrustOrderAssembler outEntrustOrderAssembler;
/**
* 分页查询出仓委托单列表(显示全部数据,不做默认状态过滤)
*/
@ApiOperation("查询出仓委托单列表")
@GetMapping("/list")
public TableDataInfo list(OutEntrustOrderDTO outEntrustOrderDTO) {
//转换实体
OutEntrustOrderDO outEntrustOrderDO = outEntrustOrderAssembler.toDO(outEntrustOrderDTO);
startPage();
List<OutEntrustOrderPO> list = outEntrustOrderApplicationService.queryList(outEntrustOrderDO);
return getDataTable(list);
}
/**
* 新增/修改出仓委托单(必填客户业务员仓库商品名称申报数量)
*/
@ApiOperation("编辑出仓委托单")
@PostMapping("/edit")
public AjaxResult edit(@RequestBody OutEntrustOrderDTO outEntrustOrderDTO) {
//转换实体
OutEntrustOrderDO outEntrustOrderDO = outEntrustOrderAssembler.toDO(outEntrustOrderDTO);
if (outEntrustOrderDTO.getId() != null) {
return toAjax(outEntrustOrderApplicationService.update(outEntrustOrderDO));
} else {
return toAjax(outEntrustOrderApplicationService.insert(outEntrustOrderDO));
}
}
/**
* 批量删除出仓委托单(仅待审核/已驳回且未下发)
*/
@ApiOperation("批量删除出仓委托单")
@DeleteMapping("/deleteByIds/{ids}")
public AjaxResult delete(@PathVariable Long[] ids) {
return toAjax(outEntrustOrderApplicationService.delete(ids));
}
/**
* 获取出仓委托单详情(含明细)
*/
@ApiOperation("获取出仓委托单")
@GetMapping(value = "/getInfo/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return AjaxResult.success(outEntrustOrderApplicationService.getInfo(id));
}
/**
* 批量审核待审核/已驳回 已审核(通过)或已驳回
* 请求体{"idList":[1,2],"orderStatus":3,"auditRemark":"备注"}orderStatus2-审核通过 3-驳回(auditRemark选填)
*/
@ApiOperation("审核出仓委托单")
@PostMapping("/audit")
public AjaxResult audit(@RequestBody OutEntrustOrderDTO outEntrustOrderDTO) {
return toAjax(outEntrustOrderApplicationService.audit(outEntrustOrderAssembler.toDO(outEntrustOrderDTO)));
}
/**
* 批量下发已审核的委托单下发到WMS(生成出库通知单)下发后订单状态变为进行中
*/
@ApiOperation("下发出仓委托单")
@PostMapping("/issueOrder")
public AjaxResult issueOrder(@RequestBody HashMap<String,List<Long>> body) {
List<Long> ids = body.get("ids");
return toAjax(outEntrustOrderApplicationService.issueOrder(ids));
}
/**
* 批量完成进行中 已入库(WMS出库完成后人工确认)
*/
@ApiOperation("委托单完成入库")
@PostMapping("/finish")
public AjaxResult finish(@RequestBody List<Long> ids) {
return toAjax(outEntrustOrderApplicationService.finish(ids));
}
}
@@ -0,0 +1,154 @@
package com.mhd.oms.interfaces.facade.outboundRank;
import com.github.pagehelper.PageInfo;
import com.mhd.common.core.constant.HttpStatus;
import com.mhd.common.core.utils.PageUtils;
import com.mhd.common.core.utils.poi.ExcelUtil;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundDetailRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDTO;
import com.mhd.oms.domain.outboundRank.service.ExecutionOutboundRankApplicationService;
import com.mhd.oms.domain.outboundRank.service.ExecutionOutboundRankAssembler;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
/**
* 出库排名统计Api
*
* <p>按日期区间筛选出库量统计排名按出库数量(实际复核出库量)降序
* 商品tab一行 = 一个货品货号/货名/件数/毛重()/体积(CBM)/退保总额占位跨出库单汇总忽略线路筛选参数
* 线路tab一行 = 线路×货品线路编码/线路/货号/货名/件数/毛重()/体积(CBM)/退保总额占位按线路+物料编码合并
* 仅统计带线路的单商品/线路筛选参数均生效
* 统计口径出库单创建时间 + 已出库及之后状态(9,12,13)明细仅取 level=1 主行
* 件数 = SUM(CHECK_QUANTITY) 复核确认的实际出库量与入库排名的实际上架量对称
* 数据源跨库取 WMS 库出库表口径与 WMS /outboundRankApi 完全一致两边数字一致
* 退保总额后端暂无对应数据来源恒为 null</p>
*
* @author rcx
* @date 2026-08-31
*/
@RestController
@RequestMapping("/executionOutboundRankApi")
@Slf4j
public class ExecutionOutboundRankApi {
@Resource
private ExecutionOutboundRankAssembler executionOutboundRankAssembler;
@Autowired
private ExecutionOutboundRankApplicationService executionOutboundRankApplicationService;
/**
* 分页查询商品出库排名列表含合计行商品tab忽略线路筛选参数
*/
@ApiOperation("分页查询商品出库排名列表")
@GetMapping("/queryMaterialRankList")
public HashMap<String, Object> queryMaterialRankList(ExecutionOutboundRankDTO executionOutboundRankDTO) {
//转换实体
ExecutionOutboundRankDO executionOutboundRankDO = executionOutboundRankAssembler.toDO(executionOutboundRankDTO);
//商品tab忽略线路筛选参数
executionOutboundRankDO.setLineCode(null);
executionOutboundRankDO.setLineName(null);
startPage();
List<ExecutionOutboundMaterialRankPO> list = executionOutboundRankApplicationService.queryMaterialRankList(executionOutboundRankDO);
ExecutionOutboundDetailRankTotalPO total = executionOutboundRankApplicationService.queryMaterialRankTotal(executionOutboundRankDO);
return getDataTableWithTotal(list, total);
}
/**
* 分页查询线路出库排名列表含合计行线路tab一行=线路×货品仅统计带线路的单
*/
@ApiOperation("分页查询线路出库排名列表")
@GetMapping("/queryLineRankList")
public HashMap<String, Object> queryLineRankList(ExecutionOutboundRankDTO executionOutboundRankDTO) {
//转换实体线路tab商品/线路筛选参数均生效按线路+物料编码合并
ExecutionOutboundRankDO executionOutboundRankDO = executionOutboundRankAssembler.toDO(executionOutboundRankDTO);
startPage();
List<ExecutionOutboundLineRankPO> list = executionOutboundRankApplicationService.queryLineRankList(executionOutboundRankDO);
ExecutionOutboundDetailRankTotalPO total = executionOutboundRankApplicationService.queryLineRankTotal(executionOutboundRankDO);
return getDataTableWithTotal(list, total);
}
/**
* 导出商品出库排名Excel商品tab忽略线路筛选参数
*/
@ApiOperation("导出商品出库排名Excel")
@GetMapping("/exportMaterialRank")
public void exportMaterialRank(HttpServletResponse response, ExecutionOutboundRankDTO executionOutboundRankDTO) throws Exception {
//转换实体不分页导出全量
ExecutionOutboundRankDO executionOutboundRankDO = executionOutboundRankAssembler.toDO(executionOutboundRankDTO);
//商品tab忽略线路筛选参数
executionOutboundRankDO.setLineCode(null);
executionOutboundRankDO.setLineName(null);
List<ExecutionOutboundMaterialRankPO> list = executionOutboundRankApplicationService.exportMaterialRankList(executionOutboundRankDO);
String fileName = URLEncoder.encode("商品出库排名", "UTF-8").replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
ExcelUtil<ExecutionOutboundMaterialRankPO> util = new ExcelUtil<>(ExecutionOutboundMaterialRankPO.class);
util.exportExcel(response, list, "商品出库排名");
}
/**
* 导出线路出库排名Excel线路tab一行=线路×货品仅统计带线路的单
*/
@ApiOperation("导出线路出库排名Excel")
@GetMapping("/exportLineRank")
public void exportLineRank(HttpServletResponse response, ExecutionOutboundRankDTO executionOutboundRankDTO) throws Exception {
//转换实体不分页导出全量线路tab商品/线路筛选参数均生效按线路+物料编码合并
ExecutionOutboundRankDO executionOutboundRankDO = executionOutboundRankAssembler.toDO(executionOutboundRankDTO);
List<ExecutionOutboundLineRankPO> list = executionOutboundRankApplicationService.exportLineRankList(executionOutboundRankDO);
String fileName = URLEncoder.encode("线路出库排名", "UTF-8").replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
ExcelUtil<ExecutionOutboundLineRankPO> util = new ExcelUtil<>(ExecutionOutboundLineRankPO.class);
util.exportExcel(response, list, "线路出库排名");
}
/**
* 分页结果 + 合计行data/total/code/msg 之外追加合计字段商品tab含 totalOrderCount线路tab含 totalOrderCount/totalLineCount
*/
private HashMap<String, Object> getDataTableWithTotal(List<?> list, ExecutionOutboundDetailRankTotalPO 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("totalPieceCount", total.getTotalPieceCount());
result.put("totalGrossWeightTon", total.getTotalGrossWeightTon());
result.put("totalVolume", total.getTotalVolume());
if (total.getTotalOrderCount() != null) {
result.put("totalOrderCount", total.getTotalOrderCount());
}
if (total.getTotalLineCount() != null) {
result.put("totalLineCount", total.getTotalLineCount());
}
}
return result;
}
/**
* 设置请求分页数据
*/
protected void startPage() {
PageUtils.startPage();
}
}
@@ -47,13 +47,18 @@ public class ReservationMaterialInventoryApi extends BaseController {
/**
* 分页查询物料库存列表
*/
@ApiOperation("查询物料库存列表")
@ApiOperation(value = "查询物料库存列表", notes = "导出复用本接口:传 isExport=true 标识导出请求(导出显示:商品饮用有效期expiryDate/物料分类materialClassifyName/进货日期purchaseDate,所有查询维度均返回)excludeZeroInventoryQuantity=true 排除库存为0的数据(所有维度生效)orderByExpiryDate=true 按到期日分组排序")
@GetMapping("/list")
public TableDataInfo list(ReservationMaterialInventoryDTO materialInventoryDTO,
@RequestParam(value = "customerName", required = false) String customerName,
@RequestParam(value = "expiryDateStart", required = false) String expiryDateStart,
@RequestParam(value = "expiryDateEnd", required = false) String expiryDateEnd,
@RequestParam(value = "orderByExpiryDate", required = false) Boolean orderByExpiryDate)
@RequestParam(value = "purchaseDateStart", required = false) String purchaseDateStart,
@RequestParam(value = "purchaseDateEnd", required = false) String purchaseDateEnd,
@RequestParam(value = "excludeZeroInventoryQuantity", required = false) Boolean excludeZeroInventoryQuantity,
@RequestParam(value = "orderByExpiryDate", required = false) Boolean orderByExpiryDate,
// 导出标识导出与查询共用本接口isExport=true 表示本次请求为导出导出专属排序等逻辑以此开关
@RequestParam(value = "isExport", required = false) Boolean isExport)
{
// 出库分配前端传的是客户名称 customerName这里通过名称查询货主ID使用精确匹配不模糊匹配
String shipperName = null; // 保存查询到的货主名称
@@ -81,17 +86,25 @@ public class ReservationMaterialInventoryApi extends BaseController {
ReservationMaterialInventoryDO materialInventoryDO = materialInventoryAssembler.toDO(materialInventoryDTO);
materialInventoryDO.setExpiryDateStr(materialInventoryDTO.getExpiryDate());
materialInventoryDO.setProductionDateStr(materialInventoryDTO.getProductionDate());
materialInventoryDO.setPurchaseDateStr(materialInventoryDTO.getPurchaseDate());
materialInventoryDO.setExpiryDateStart(expiryDateStart);
materialInventoryDO.setExpiryDateEnd(expiryDateEnd);
materialInventoryDO.setPurchaseDateStart(purchaseDateStart);
materialInventoryDO.setPurchaseDateEnd(purchaseDateEnd);
// 导出场景queryRule=wlExpiry + orderByExpiryDate=true SQL 按到期日倒序分组
// 日期近的在前空值最后0库存靠后同到期日按更新时间倒序
materialInventoryDO.setOrderByExpiryDate(orderByExpiryDate);
// 导出场景前端"库存为0是否导出"""时传 excludeZeroInventoryQuantity=true
// SQL HAVING 排除汇总库存为0的数据 WMS 同名参数语义一致
materialInventoryDO.setExcludeZeroInventoryQuantity(excludeZeroInventoryQuantity);
// 导出标识isExport=true 启用导出融合排序到期日倒序优先 同到期日按物料编码升序所有维度生效
materialInventoryDO.setIsExport(isExport);
startPage();
// 使用带批次属性的查询方法批次属性值从库存表中获取
List<ReservationMaterialInventoryPO> list = materialInventoryApplicationService.queryListWithBatchAttributes(materialInventoryDO);
// 按更新时间倒序排序update_time 为空的数据放最后
// 导出 orderByExpiryDate=true 时跳过该重排避免覆盖 SQL 到期日分组排序
if (!Boolean.TRUE.equals(orderByExpiryDate)) {
// 导出 orderByExpiryDate=true isExport=true 时跳过该重排避免覆盖 SQL 导出排序
if (!Boolean.TRUE.equals(orderByExpiryDate) && !Boolean.TRUE.equals(isExport)) {
list.sort((o1, o2) -> {
Date t1 = o1.getUpdateTime();
Date t2 = o2.getUpdateTime();
@@ -168,6 +181,20 @@ public class ReservationMaterialInventoryApi extends BaseController {
return AjaxResult.success(materialInventoryApplicationService.getInventoryByStorageLocationIdAndMaterialBaseInfoId(storageLocationId, materialBaseInfoId));
}
/**
* 入库业务单新增页面根据物料编号查询其库存批次过期日期+批次号去重取最近5条
* 供前端下拉回显用户也可点击右侧"日期"小图标手动选择日期
*/
@ApiOperation("根据物料编号查询库存批次最近5条过期日期+批次号")
@GetMapping(value = "/queryRecentExpiryDates/{materialCode}")
public AjaxResult queryRecentExpiryDates(@PathVariable("materialCode") String materialCode)
{
if (StringUtils.isBlank(materialCode)) {
return AjaxResult.error("物料编号不能为空");
}
return AjaxResult.success(materialInventoryApplicationService.selectRecentExpiryBatches(materialCode));
}
/**
@@ -9,6 +9,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStock
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.vo.DeliveryDataDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.SheetImportResultDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.StockInDataDTO;
import com.mhd.oms.domain.reservationStockInOrder.service.ReservationStockInOrderApplicationService;
import com.mhd.oms.domain.reservationStockInOrder.service.ReservationStockInOrderAssembler;
@@ -213,17 +214,25 @@ public class ReservationStockInOrderApi extends BaseController {
/**
* 上传并解析Excel文件默认读取"出库导入"工作表
* @param file Excel文件
* @param warehouseId 当前仓库id非空时校验导入物料是否属于该仓库并按该仓库过滤批次库存
* @return 解析后的出库数据
*/
@PostMapping("/parseExcel")
public ResponseEntity<Map<String, Object>> parseExcel(
@RequestParam("file") MultipartFile file) {
@RequestParam("file") MultipartFile file,
@RequestParam(value = "warehouseId", required = false) Long warehouseId) {
Map<String, Object> result = new HashMap<>();
try {
DeliveryDataDTO deliveryDataDTO = reservationStockInOrderService.parseExcelFile(file, "");
result.put("result", deliveryDataDTO);
// 多页签导入Excel中每个工作表独立生成一张出库单任一页签校验失败整体不落库
List<SheetImportResultDTO> results = reservationStockInOrderService.parseExcelFile(file, "", warehouseId);
// result 保留第一个页签的解析数据兼容老前端results 为全部页签结果
result.put("result", results.isEmpty() ? null : results.get(0).getDeliveryData());
result.put("results", results);
result.put("code", 200);
result.put("msg", "解析成功");
String orderNumbers = results.stream()
.map(SheetImportResultDTO::getOrderNumber)
.collect(java.util.stream.Collectors.joining(""));
result.put("msg", "成功导入" + results.size() + "张出库单:" + orderNumbers);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
result.put("code", 400);
@@ -237,18 +246,23 @@ public class ReservationStockInOrderApi extends BaseController {
}
/**
* 接口1解析Excel文件返回解析结果不保存
* 用途预览Excel解析数据
* 接口1解析并导入入库单Excel多页签不传sheetName时每个工作表独立生成一张入库单任一页签校验失败整体不落库
*/
@PostMapping("/parse-excel")
public ResponseEntity<Map<String, Object>> parseStockInExcel(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "sheetName", required = false) String sheetName) {
try {
StockInDataDTO resultDTO = reservationStockInOrderService.parseStockInExcel(file, sheetName);
List<SheetImportResultDTO> results = reservationStockInOrderService.parseStockInExcel(file, sheetName);
Map<String, Object> result = new HashMap<>();
result.put("result", resultDTO);
result.put("code",200);
// result 保留第一个页签的解析数据兼容老前端results 为全部页签结果
result.put("result", results.isEmpty() ? null : results.get(0).getStockInData());
result.put("results", results);
result.put("code", 200);
String orderNumbers = results.stream()
.map(SheetImportResultDTO::getOrderNumber)
.collect(java.util.stream.Collectors.joining(""));
result.put("msg", "成功导入" + results.size() + "张入库单:" + orderNumbers);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
// 参数错误如文件为空