1
22 小时以前 a422cb5b73799050827251f835ebc53d5757a96b
lsh#
4个文件已修改
1个文件已添加
729 ■■■■ 已修改文件
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/SyncOrderController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncOrderParams.java 285 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncOrdersItem.java 272 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/validator/SyncOrderValidator.java 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/SyncOrderController.java
@@ -3,6 +3,7 @@
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.controller.erp.params.*;
import com.vincent.rsf.server.api.entity.validator.SyncOrderValidator;
import com.vincent.rsf.server.api.service.ReceiveMsgService;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.utils.ExcelUtil;
@@ -164,8 +165,17 @@
        if (Objects.isNull(orders) || orders.isEmpty()) {
            return R.error("参数不能为空!!");
        }
//        return receiveMsgService.syncOrderUpdate(orders);
        try {
            // 业务验证
            new SyncOrderValidator().validateBatchOrders(orders);
            // 处理业务
        return receiveMsgService.syncCheckOrder(orders, getLoginUserId());
        } catch (IllegalArgumentException e) {
            return R.error(e.getMessage());
        } catch (Exception e) {
            return R.error(e.getMessage());
        }
    }
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncOrderParams.java
@@ -1,14 +1,16 @@
package com.vincent.rsf.server.api.controller.erp.params;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.vincent.rsf.framework.common.Cools;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@@ -20,127 +22,274 @@
    private static final long serialVersionUID = 1L;
    /*
    * 业务类型,待ERP补充,以下为示例:
    * 入库:收料通知单(PUR_ReceiveBill)、
    * 采购入库单(STK_InStock)、
    * 退料申请单(PUR_MRAPP)、
    * 采购退料单(PUR_MRB)、
    * 退货通知单(SAL_RETURNNOTICE)、
    * 销售退货单(SAL_RETURNSTOCK)、
    * 生产退料单(PRD_ReturnMtrl)、
    * 生产入库单(PRD_INSTOCK)/
    * 生产汇报单(PRD_MORPT)、
    * 其他入库单(STK_MISCELLANEOUS)
    * 出库:发货通知单(SAL_DELIVERYNOTICE)、
    * 销售出库单(SAL_OUTSTOCK)、
    * 出库申请单(STK_OutStockApply)、
    * 生产领料单(PRD_PickMtrl)、
    * 生产补料单(PRD_FeedMtrl)、
    * 其他出库单(STK_MisDelivery)调拨:
    * 调拨申请单(STK_TRANSFERAPPLY)、
    * 直接调拨单(STK_TransferDirect)
    * */
    /**
     * 业务类型枚举
     */
    public interface BusinessType {
        // 入库单据
        String PUR_RECEIVE_BILL = "PUR_ReceiveBill";        // 收料通知单
        String STK_IN_STOCK = "STK_InStock";                // 采购入库单
        String PUR_MRAPP = "PUR_MRAPP";                     // 退料申请单
        String PUR_MRB = "PUR_MRB";                         // 采购退料单
        String SAL_RETURNNOTICE = "SAL_RETURNNOTICE";      // 退货通知单
        String SAL_RETURNSTOCK = "SAL_RETURNSTOCK";        // 销售退货单
        String PRD_RETURN_MTRL = "PRD_ReturnMtrl";         // 生产退料单 //接驳
        String PRD_INSTOCK = "PRD_INSTOCK";                // 生产入库单 //接驳
        String PRD_MORPT = "PRD_MORPT";                    // 生产汇报单
        String STK_MISCELLANEOUS = "STK_MISCELLANEOUS";    // 其他入库单
        // 出库单据
        String SAL_DELIVERYNOTICE = "SAL_DELIVERYNOTICE";  // 发货通知单
        String SAL_OUTSTOCK = "SAL_OUTSTOCK";              // 销售出库单
        String STK_OUTSTOCK_APPLY = "STK_OutStockApply";   // 出库申请单
        String PRD_PICK_MTRL = "PRD_PickMtrl";             // 生产领料单 //接驳
        String PRD_FEED_MTRL = "PRD_FeedMtrl";             // 生产补料单 //接驳
        String STK_MIS_DELIVERY = "STK_MisDelivery";       // 其他出库单
        // 调拨单据
        String STK_TRANSFER_APPLY = "STK_TRANSFERAPPLY";   // 调拨申请单
        String STK_TRANSFER_DIRECT = "STK_TransferDirect"; // 直接调拨单
    }
    @NotNull(message = "业务类型不能为null")
    @NotEmpty(message = "业务类型不能为空")
    @ApiModelProperty("业务类型")
    /**
     * 单据类型枚举
     */
    public interface OrderType {
        String OUT_STOCK = "1";    // 出库单
        String IN_STOCK = "2";     // 入库单
        String TRANSFER = "3";     // 调拨单
    }
    @NotBlank(message = "业务类型不能为空")
    @Pattern(regexp = "^(PUR_ReceiveBill|STK_InStock|PUR_MRAPP|PUR_MRB|SAL_RETURNNOTICE|SAL_RETURNSTOCK|"
            + "PRD_ReturnMtrl|PRD_INSTOCK|PRD_MORPT|STK_MISCELLANEOUS|SAL_DELIVERYNOTICE|SAL_OUTSTOCK|"
            + "STK_OutStockApply|PRD_PickMtrl|PRD_FeedMtrl|STK_MisDelivery|STK_TRANSFERAPPLY|STK_TransferDirect)$",
            message = "业务类型格式不正确")
    @ApiModelProperty(value = "业务类型", required = true, example = "STK_InStock")
    private String wkType;
    /*
    * 订单类型,1 出库单;2 入库单;3 调拨单;
    * */
    @NotNull(message = "单据类型不能为null")
    @NotEmpty(message = "单据类型不能为空")
    @ApiModelProperty("单据类型")
    @NotBlank(message = "单据类型不能为空")
    @Pattern(regexp = "^[123]$", message = "单据类型只能是1(出库单)、2(入库单)或3(调拨单)")
    @ApiModelProperty(value = "单据类型: 1-出库单, 2-入库单, 3-调拨单", required = true, example = "2")
    private String type;
    @NotNull(message = "单号不能为null")
    @NotEmpty(message = "单号不能为空")
    @ApiModelProperty("单号")
    @NotBlank(message = "单号不能为空")
    @Size(max = 50, message = "单号长度不能超过50个字符")
    @ApiModelProperty(value = "单号", required = true, example = "PO202401010001")
    private String orderNo;
    @NotNull(message = "单据内码不能为null")
    @NotEmpty(message = "单据内码不能为空")
    @ApiModelProperty("单据内码,唯一标识")
    @NotNull(message = "单据内码不能为空")
    @Positive(message = "单据内码必须是正整数")
    @ApiModelProperty(value = "单据内码,唯一标识", required = true, example = "100001")
    private Long orderInternalCode;
    @NotNull(message = "订单ID不能为null")
    @NotEmpty(message = "订单ID不能为空")
    @ApiModelProperty("订单ID")
//    @NotNull(message = "订单ID不能为空")
//    @Positive(message = "订单ID必须是正整数")
//    @ApiModelProperty(value = "订单ID", required = true, example = "200001")
    private Long orderId;
    @NotNull(message = "数量不能为null")
    @NotEmpty(message = "数量不能为空")
    @ApiModelProperty("数量")
//    @NotNull(message = "数量不能为空")
//    @DecimalMin(value = "0.0", inclusive = false, message = "数量必须大于0")
//    @ApiModelProperty(value = "数量", required = true, example = "100.5")
    private Double anfme;
    @ApiModelProperty("客户编码")
    @ApiModelProperty(value = "客户编码", example = "CUST001")
    private String customerId;
    @ApiModelProperty("客户名称")
    @ApiModelProperty(value = "客户名称", example = "XX科技有限公司")
    private String customerName;
    @ApiModelProperty("供应商编码")
    @ApiModelProperty(value = "供应商编码", example = "SUP001")
    private String supplierId;
    @ApiModelProperty("供应商名称")
    @ApiModelProperty(value = "供应商名称", example = "XX供应商")
    private String supplierName;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty(value = "到达时间", example = "2024-01-01 10:00:00")
    private Date arrTime;
    @ApiModelProperty("创建日期,时间戳,精确到秒")
    @NotNull(message = "创建日期不能为空")
    @Positive(message = "创建日期必须是正整数")
    @ApiModelProperty(value = "创建日期,时间戳,精确到秒", required = true, example = "1704067200")
    private Long createTime;
    @ApiModelProperty("业务日期,对账使用,时间戳,精确到秒")
    @NotNull(message = "业务日期不能为空")
    @Positive(message = "业务日期必须是正整数")
    @ApiModelProperty(value = "业务日期,对账使用,时间戳,精确到秒", required = true, example = "1704067200")
    private Long businessTime;
    @ApiModelProperty("单据明细信息")
    @Valid
    @NotNull(message = "单据明细信息不能为空")
    @Size(min = 1, message = "至少需要一个明细项")
    @ApiModelProperty(value = "单据明细信息", required = true)
    private List<SyncOrdersItem> orderItems;
    @ApiModelProperty("收料/发货组织")
    @ApiModelProperty(value = "收料/发货组织", example = "ORG001")
    private String stockOrgId;
    @ApiModelProperty("收料/发货组织名称")
    @ApiModelProperty(value = "收料/发货组织名称", example = "发货部")
    private String stockOrgName;
    @ApiModelProperty("采购组织")
    @ApiModelProperty(value = "采购组织", example = "PUR001")
    private String purchaseOrgId;
    @ApiModelProperty("采购组织名称")
    @ApiModelProperty(value = "采购组织名称", example = "采购部")
    private String purchaseOrgName;
    @ApiModelProperty("采购员")
    @ApiModelProperty(value = "采购员", example = "USER001")
    private String purchaseUserId;
    @ApiModelProperty("采购员名称")
    @ApiModelProperty(value = "采购员名称", example = "张三")
    private String purchaseUserName;
    @ApiModelProperty("生产组织")
    @ApiModelProperty(value = "生产组织", example = "PRD001")
    private String prdOrgId;
    @ApiModelProperty("生产组织名称")
    @ApiModelProperty(value = "生产组织名称", example = "生产部")
    private String prdOrgName;
    @ApiModelProperty("销售组织")
    @ApiModelProperty(value = "销售组织", example = "SALE001")
    private String saleOrgId;
    @ApiModelProperty("销售组织名称")
    @ApiModelProperty(value = "销售组织名称", example = "销售部")
    private String saleOrgName;
    @ApiModelProperty("销售员")
    @ApiModelProperty(value = "销售员", example = "USER002")
    private String saleUserId;
    @ApiModelProperty("销售员名称")
    @ApiModelProperty(value = "销售员名称", example = "李四")
    private String saleUserName;
    @ApiModelProperty("库存方向")
    @ApiModelProperty(value = "库存方向", example = "IN")
    private String stockDirect;
    @ApiModelProperty("出入库接驳站点,出库时将物料出库后运输至该站点,入库时从该站点将物料运回库中")
    @ApiModelProperty(value = "出入库接驳站点,出库时将物料出库后运输至该站点,入库时从该站点将物料运回库中", example = "STATION001")
    private String stationId;
    /**
     * 获取实际到达时间
     * 优先使用 arrTime,如果为空则从 createTime 转换
     */
    @JsonIgnore
    public Date getActualArrTime() {
        if (this.arrTime != null) {
            return this.arrTime;
        }
        if (this.createTime != null) {
            return new Date(this.createTime * 1000L);
        }
        return null;
    }
    /**
     * 获取实际业务时间
     */
    @JsonIgnore
    public Date getActualBusinessTime() {
        if (this.businessTime != null) {
            return new Date(this.businessTime * 1000L);
        }
        return null;
    }
    /**
     * 获取实际创建时间
     */
    @JsonIgnore
    public Date getActualCreateTime() {
        if (this.createTime != null) {
            return new Date(this.createTime * 1000L);
        }
        return null;
    }
    /**
     * 是否入库单
     */
    @JsonIgnore
    public boolean isInStockOrder() {
        return OrderType.IN_STOCK.equals(this.type);
    }
    /**
     * 是否出库单
     */
    @JsonIgnore
    public boolean isOutStockOrder() {
        return OrderType.OUT_STOCK.equals(this.type);
    }
    /**
     * 是否调拨单
     */
    @JsonIgnore
    public boolean isTransferOrder() {
        return OrderType.TRANSFER.equals(this.type);
    }
    /**
     * 业务验证
     */
    public void validateBusiness() {
//        if (isOutStockOrder() && Cools.isEmpty(customerId)) {
//            throw new IllegalArgumentException("出库单必须指定客户");
//        }
//
//        if (isInStockOrder() && Cools.isEmpty(supplierId)) {
//            throw new IllegalArgumentException("入库单必须指定供应商");
//        }
//        if (Cools.isEmpty(stationId) && !isTransferOrder()) {
//            throw new IllegalArgumentException("必须指定接驳站点");
//        }
        // 验证明细总数与主单数量是否一致
//        if (orderItems != null && anfme != null) {
//            double itemsTotal = orderItems.stream()
//                    .mapToDouble(item -> item.getAnfme() != null ? item.getAnfme() : 0.0)
//                    .sum();
//
//            if (Math.abs(itemsTotal - anfme) > 0.001) {
//                throw new IllegalArgumentException(String.format(
//                        "主单数量(%.3f)与明细总数(%.3f)不一致", anfme, itemsTotal));
//            }
//        }
    }
    /**
     * 计算明细总数量
     */
    @JsonIgnore
    public Double calculateItemsTotal() {
        if (orderItems == null || orderItems.isEmpty()) {
            return 0.0;
        }
        return orderItems.stream()
                .mapToDouble(item -> item.getAnfme() != null ? item.getAnfme() : 0.0)
                .sum();
    }
    /**
     * 获取单据摘要
     */
    @JsonIgnore
    public String getSummary() {
        return String.format("单据[%s] 类型[%s] 业务类型[%s] 单据内码[%s]",
                orderNo, type, wkType, orderInternalCode);
    }
    /**
     * 是否有效单据
     */
    @JsonIgnore
    public boolean isValid() {
        return !Cools.isEmpty(orderNo) &&
                orderInternalCode != null && !Cools.isEmpty(orderInternalCode) &&
                type != null && !Cools.isEmpty(type) &&
                wkType != null && !Cools.isEmpty(wkType) &&
                createTime != null && !Cools.isEmpty(createTime) &&
                businessTime != null && !Cools.isEmpty(businessTime) &&
                orderItems != null && !orderItems.isEmpty();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncOrdersItem.java
@@ -1,129 +1,327 @@
package com.vincent.rsf.server.api.controller.erp.params;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.vincent.rsf.framework.common.Cools;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.experimental.Accessors;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@Accessors(chain = true)
@ApiModel(value = "SyncOrdersParams", description = "同步盘点参数")
@ApiModel(value = "SyncOrdersItem", description = "单据明细参数")
public class SyncOrdersItem implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("盘点单明细ID")
    /**
     * 货主类型枚举
     */
    public interface OwnerType {
        String SUPPLIER = "supplier";   // 供应商
        String CUSTOMER = "customer";   // 客户
        String COMPANY = "company";     // 公司
        String THIRD_PARTY = "third_party"; // 第三方
    }
    @ApiModelProperty(value = "盘点单明细ID", example = "1001")
    private Long id;
    @ApiModelProperty("物料标识")
    @ApiModelProperty(value = "物料标识", example = "2001")
    private Long matnrId;
    @ApiModelProperty("物料编码")
    @NotBlank(message = "物料编码不能为空")
    @Size(max = 50, message = "物料编码长度不能超过50个字符")
    @ApiModelProperty(value = "物料编码", required = true, example = "MAT001")
    private String matnr;
    @ApiModelProperty("物料名称")
    @ApiModelProperty(value = "物料名称", example = "原材料A")
    private String maktx;
    @ApiModelProperty("客单号")
    @ApiModelProperty(value = "客单号", example = "CUST20240101001")
    private String platOrderCode;
    @ApiModelProperty("平台标识(行号)")
    @NotBlank(message = "平台标识(行号)不能为空")
    @ApiModelProperty(value = "平台标识(行号)", required = true, example = "10")
    private String platItemId;
    @ApiModelProperty("工单号")
    @ApiModelProperty(value = "工单号", example = "WO20240101001")
    private String platWorkCode;
    @ApiModelProperty("项目号")
    @ApiModelProperty(value = "项目号", example = "PROJ001")
    private String projectCode;
    @ApiModelProperty("字段索引")
    @ApiModelProperty(value = "字段索引", example = "ext_001")
    private String fieldsIndex;
    @ApiModelProperty("规格")
    @ApiModelProperty(value = "规格", example = "10 * 20 * 30")
    private String spec;
    @ApiModelProperty("型号")
    @ApiModelProperty(value = "型号", example = "MODEL-A")
    private String model;
    @ApiModelProperty("数量")
    @NotNull(message = "数量不能为空")
    @DecimalMin(value = "0.0", inclusive = false, message = "数量必须大于0")
    @Digits(integer = 10, fraction = 3, message = "数量整数位不超过10位,小数位不超过3位")
    @ApiModelProperty(value = "数量", required = true, example = "100.5")
    private Double anfme;
    @ApiModelProperty("库存单位")
    @ApiModelProperty(value = "库存单位", example = "个")
    private String unit;
    @ApiModelProperty("库存批次")
    @NotBlank(message = "库存批次不能为空")
    @Size(max = 50, message = "库存批次长度不能超过50个字符")
    @ApiModelProperty(value = "库存批次", required = true, example = "BATCH20240101")
    private String batch;
    @ApiModelProperty("已收数量")
    @DecimalMin(value = "0.0", message = "已收数量不能为负数")
    @ApiModelProperty(value = "已收数量", example = "50.0")
    private Double qty;
    @ApiModelProperty("条形码")
    @ApiModelProperty(value = "条形码", example = "6936983800013")
    private String barcode;
    @ApiModelProperty("现金票号")
    @ApiModelProperty(value = "现金票号", example = "CASH001")
    private String crushNo;
    @ApiModelProperty("计划跟踪号")
    @NotBlank(message = "计划跟踪号不能为空")
    @Size(max = 50, message = "计划跟踪号长度不能超过50个字符")
    @ApiModelProperty(value = "计划跟踪号", required = true, example = "PLAN20240101")
    private String planNo;
    @ApiModelProperty("行内码,唯一标识")
    @NotBlank(message = "行内码不能为空")
    @Size(max = 50, message = "行内码长度不能超过50个字符")
    @ApiModelProperty(value = "行内码,唯一标识", required = true, example = "LINE001")
    private String lineId;
    @ApiModelProperty("物料编码")
    @ApiModelProperty(value = "物料编码", example = "MAT001")
    private String matNr;
    @ApiModelProperty("物料名称")
    @ApiModelProperty(value = "物料名称", example = "原材料A")
    private String makTx;
    @ApiModelProperty("规格")
    @ApiModelProperty(value = "规格", example = "10 * 20 * 30")
    private String specs;
    @ApiModelProperty("基本单位")
    @ApiModelProperty(value = "基本单位", example = "PCS")
    private String baseUnitId;
    @ApiModelProperty("托盘码,半成品/成品入库ERP需要传,非则ERP不需要传")
    @ApiModelProperty(value = "托盘码,半成品/成品入库ERP需要传,非则ERP不需要传", example = "PALLET001")
    private String palletId;
    @ApiModelProperty("计价单位")
    @ApiModelProperty(value = "计价单位", example = "BOX")
    private String priceUnitId;
    @ApiModelProperty("建议目标仓库")
    @ApiModelProperty(value = "建议目标仓库", example = "WH001")
    private String targetWareHouseId;
    @ApiModelProperty("调出仓")
    @ApiModelProperty(value = "调出仓", example = "WH002")
    private String sourceWareHouseId;
    @ApiModelProperty("入库类型")
    @ApiModelProperty(value = "入库类型", example = "purchase")
    private String inStockType;
    @ApiModelProperty("货主类型")
    @NotBlank(message = "货主类型不能为空")
    @Pattern(regexp = "^(supplier|customer|company|third_party)$",
            message = "货主类型必须是supplier、customer、company或third_party")
    @ApiModelProperty(value = "货主类型", required = true, example = "company")
    private String ownerTypeId;
    @ApiModelProperty("货主")
    @NotBlank(message = "货主不能为空")
    @Size(max = 50, message = "货主长度不能超过50个字符")
    @ApiModelProperty(value = "货主", required = true, example = "OWNER001")
    private String ownerId;
    @ApiModelProperty("货主名称")
    @NotBlank(message = "货主名称不能为空")
    @Size(max = 100, message = "货主名称长度不能超过100个字符")
    @ApiModelProperty(value = "货主名称", required = true, example = "XX有限公司")
    private String ownerName;
    @ApiModelProperty("保管者类型")
    @ApiModelProperty(value = "保管者类型", example = "warehouse")
    private String keeperTypeId;
    @ApiModelProperty("保管者")
    @ApiModelProperty(value = "保管者", example = "KEEPER001")
    private String keeperId;
    @ApiModelProperty("保管者名称")
    @ApiModelProperty(value = "保管者名称", example = "仓库管理部")
    private String keeperName;
    /**
     * 获取实际的物料编码
     * 优先使用 matnr,如果为空则使用 matNr
     */
    @JsonIgnore
    public String getActualMatnr() {
        return !Cools.isEmpty(matnr) ? matnr : matNr;
    }
    /**
     * 获取实际的物料名称
     * 优先使用 maktx,如果为空则使用 makTx
     */
    @JsonIgnore
    public String getActualMaktx() {
        return !Cools.isEmpty(maktx) ? maktx : makTx;
    }
    /**
     * 获取实际的规格
     * 优先使用 spec,如果为空则使用 specs
     */
    @JsonIgnore
    public String getActualSpec() {
        return !Cools.isEmpty(spec) ? spec : specs;
    }
    /**
     * 设置物料编码(保持两个字段同步)
     */
    public void setMatnr(String matnr) {
        this.matnr = matnr;
        this.matNr = matnr;
    }
    public void setMatNr(String matNr) {
        this.matNr = matNr;
        this.matnr = matNr;
    }
    /**
     * 设置物料名称(保持两个字段同步)
     */
    public void setMaktx(String maktx) {
        this.maktx = maktx;
        this.makTx = maktx;
    }
    public void setMakTx(String makTx) {
        this.makTx = makTx;
        this.maktx = makTx;
    }
    /**
     * 设置规格(保持两个字段同步)
     */
    public void setSpec(String spec) {
        this.spec = spec;
        this.specs = spec;
    }
    public void setSpecs(String specs) {
        this.specs = specs;
        this.spec = specs;
    }
    /**
     * 转换为BigDecimal(精度计算使用)
     */
    @JsonIgnore
    public BigDecimal getAnfmeAsBigDecimal() {
        return anfme != null ? BigDecimal.valueOf(anfme) : BigDecimal.ZERO;
    }
    @JsonIgnore
    public BigDecimal getQtyAsBigDecimal() {
        return qty != null ? BigDecimal.valueOf(qty) : BigDecimal.ZERO;
    }
    /**
     * 获取剩余数量
     */
    @JsonIgnore
    public Double getRemainingQty() {
        if (anfme == null) {
            return 0.0;
        }
        double currentQty = qty != null ? qty : 0.0;
        double remaining = anfme - currentQty;
        return Math.max(0, remaining);
    }
    /**
     * 是否已完成
     */
    @JsonIgnore
    public boolean isCompleted() {
        if (anfme == null) {
            return false;
        }
        double currentQty = qty != null ? qty : 0.0;
        return currentQty >= anfme;
    }
    /**
     * 获取完成百分比
     */
    @JsonIgnore
    public int getCompletionPercentage() {
        if (anfme == null || anfme == 0) {
            return 0;
        }
        double currentQty = qty != null ? qty : 0.0;
        int percentage = (int) ((currentQty / anfme) * 100);
        return Math.min(Math.max(percentage, 0), 100);
    }
    /**
     * 业务验证
     */
    public void validateBusiness(String orderType) {
        if ("purchase".equals(inStockType) && Cools.isEmpty(palletId)) {
            throw new IllegalArgumentException("采购入库明细必须提供托盘码");
        }
        if ("production".equals(inStockType) && Cools.isEmpty(palletId)) {
            throw new IllegalArgumentException("生产入库明细必须提供托盘码");
        }
        if (!Cools.isEmpty(targetWareHouseId) && !Cools.isEmpty(sourceWareHouseId)) {
            if (targetWareHouseId.equals(sourceWareHouseId)) {
                throw new IllegalArgumentException("目标仓库和调出仓库不能相同");
            }
        }
        if (qty != null && qty > anfme) {
            throw new IllegalArgumentException("已收数量不能大于计划数量");
        }
    }
    /**
     * 是否需要托盘码
     */
    @JsonIgnore
    public boolean requiresPallet() {
        return "purchase".equals(inStockType) ||
                "production".equals(inStockType) ||
                "return".equals(inStockType);
    }
    /**
     * 是否调拨明细
     */
    @JsonIgnore
    public boolean isTransferItem() {
        return !Cools.isEmpty(sourceWareHouseId) && !Cools.isEmpty(targetWareHouseId);
    }
    /**
     * 获取物料唯一标识
     */
    @JsonIgnore
    public String getMaterialKey() {
        return String.format("%s_%s_%s", getActualMatnr(), batch, getActualSpec());
    }
    /**
     * 获取明细摘要
     */
    @JsonIgnore
    public String getSummary() {
        return String.format("物料[%s] 批次[%s] 数量[%s/%s]",
                getActualMatnr(), batch, qty, anfme);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/validator/SyncOrderValidator.java
New file
@@ -0,0 +1,152 @@
package com.vincent.rsf.server.api.entity.validator;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.server.api.controller.erp.params.SyncOrderParams;
import com.vincent.rsf.server.api.controller.erp.params.SyncOrdersItem;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@Slf4j
public class SyncOrderValidator {
    /**
     * 验证同步订单参数
     */
    public void validateSyncOrder(SyncOrderParams order) {
        if (order == null) {
            throw new IllegalArgumentException("订单参数不能为空");
        }
        // 基本验证
        if (!order.isValid()) {
            throw new IllegalArgumentException("订单参数不完整");
        }
        // 业务验证
        order.validateBusiness();
        // 验证明细
        if (order.getOrderItems() == null || order.getOrderItems().isEmpty()) {
            throw new IllegalArgumentException("订单明细不能为空");
        }
        // 检查重复行号
        Set<String> lineIds = new HashSet<>();
        for (int i = 0; i < order.getOrderItems().size(); i++) {
            SyncOrdersItem item = order.getOrderItems().get(i);
            String prefix = "第" + (i + 1) + "个明细: ";
            if (item == null) {
                throw new IllegalArgumentException(prefix + "明细为空");
            }
            // 验证行内码
            if (Cools.isEmpty(item.getLineId())) {
                throw new IllegalArgumentException(prefix + "行内码不能为空");
            }
            // 检查重复
            if (!lineIds.add(item.getLineId())) {
                throw new IllegalArgumentException(prefix + "行内码重复: " + item.getLineId());
            }
            // 业务验证
            try {
                item.validateBusiness(order.getType());
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException(prefix + e.getMessage());
            }
        }
//        // 验证数量一致性
//        double itemsTotal = order.calculateItemsTotal();
//        if (Math.abs(itemsTotal - order.getAnfme()) > 0.001) {
//            log.warn("订单[{}] 主单数量({})与明细总数({})不一致",
//                    order.getOrderNo(), order.getAnfme(), itemsTotal);
//        }
    }
    /**
     * 验证批次订单
     */
    public void validateBatchOrders(List<SyncOrderParams> orders) {
        if (orders == null || orders.isEmpty()) {
            throw new IllegalArgumentException("订单列表不能为空");
        }
        if (orders.size() > 100) {
            throw new IllegalArgumentException("批量同步单次最多支持100个订单");
        }
        // 检查重复订单号
        Set<String> orderNos = new HashSet<>();
        Set<Long> orderInternalCodes = new HashSet<>();
        for (int i = 0; i < orders.size(); i++) {
            SyncOrderParams order = orders.get(i);
            String prefix = "第" + (i + 1) + "个订单: ";
            if (order == null) {
                throw new IllegalArgumentException(prefix + "订单为空");
            }
            // 验证订单号
            if (Cools.isEmpty(order.getOrderNo())) {
                throw new IllegalArgumentException(prefix + "订单号不能为空");
            }
            if (!orderNos.add(order.getOrderNo())) {
                throw new IllegalArgumentException(prefix + "订单号重复: " + order.getOrderNo());
            }
            // 验证单据内码
            if (order.getOrderInternalCode() == null) {
                throw new IllegalArgumentException(prefix + "单据内码不能为空");
            }
            if (!orderInternalCodes.add(order.getOrderInternalCode())) {
                throw new IllegalArgumentException(prefix + "单据内码重复: " + order.getOrderInternalCode());
            }
            // 验证单个订单
            try {
                validateSyncOrder(order);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException(prefix + e.getMessage());
            }
        }
    }
    /**
     * 过滤有效订单
     */
    public List<SyncOrderParams> filterValidOrders(List<SyncOrderParams> orders) {
        if (orders == null) {
            return Collections.emptyList();
        }
        return orders.stream()
                .filter(order -> order != null && order.isValid())
                .collect(Collectors.toList());
    }
    /**
     * 计算总数量
     */
//    public double calculateTotalQuantity(List<SyncOrderParams> orders) {
//        if (orders == null || orders.isEmpty()) {
//            return 0.0;
//        }
//
//        return orders.stream()
//                .mapToDouble(order -> order.getAnfme() != null ? order.getAnfme() : 0.0)
//                .sum();
//    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java
@@ -14,6 +14,7 @@
import com.vincent.rsf.server.api.controller.erp.params.dto.CheckDiffDto;
import com.vincent.rsf.server.api.controller.erp.params.dto.TransferInfoDto;
import com.vincent.rsf.server.api.controller.erp.params.dto.WkOrderDto;
import com.vincent.rsf.server.api.utils.TimeConverterUtils;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.manager.controller.dto.LocStockDto;
@@ -448,13 +449,14 @@
                        .setWkType(one.getValue())
                        .setAnfme(syncOrder.getAnfme())
                        .setPoCode(syncOrder.getOrderNo())
                        .setWorkQty(0.0)
                        .setQty(0.0)
                        .setWorkQty(0.0)//执行数量
                        .setQty(0.0)//完成数量
                        .setPoId(syncOrder.getOrderInternalCode())
                        .setCode(ruleCode)
                        .setArrTime(syncOrder.getArrTime())
                        .setId(null)
                        .setCreateTime(new Date())
                        .setCreateTime(new TimeConverterUtils().timestampToDate(syncOrder.getCreateTime()))
                        .setBusinessTime(new TimeConverterUtils().timestampToDate(syncOrder.getBusinessTime()))
                        .setUpdateTime(new Date())
                        .setCreateBy(loginUserId)
                        .setUpdateBy(loginUserId);