diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpProjectInfoServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpProjectInfoServiceImpl.java index cc783cb1..4ea1e037 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpProjectInfoServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpProjectInfoServiceImpl.java @@ -25,7 +25,6 @@ import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.oa.crm.domain.CrmCustomerInfo; import org.dromara.oa.erp.constant.ProjectCategoryConstant; import org.dromara.oa.erp.domain.ErpContractInfo; -import org.dromara.oa.erp.domain.ErpFinInvoiceDetail; import org.dromara.oa.erp.domain.ErpProjectContracts; import org.dromara.oa.erp.domain.ErpProjectInfo; import org.dromara.oa.erp.domain.bo.ErpProjectContractsBo; @@ -458,44 +457,40 @@ public class ErpProjectInfoServiceImpl implements IErpProjectInfoService { */ private RemoteWmsShippingDraft buildShippingDraft(ErpProjectInfo projectInfo) { RemoteWmsShippingDraft draft = new RemoteWmsShippingDraft(); + // 项目维度字段直接回填到发货草稿主表,保证后续仓储审批能无损追溯到来源项目 draft.setProjectId(projectInfo.getProjectId()); draft.setProjectCode(projectInfo.getProjectCode()); draft.setProjectName(projectInfo.getProjectName()); + // 当前场景固定是“项目驱动生成发货草稿”,因此绑定类型固定为项目绑定 draft.setBindType("1"); + // 自动生成的草稿先按常规发货模式初始化,后续由业务人员按实际场景再调整 draft.setShippingMode("1"); + // 发货类型不是简单照搬项目分类,而是根据备件属性做业务映射,避免前端重复判断 draft.setShippingType(resolveShippingType(projectInfo)); draft.setManagerId(projectInfo.getManagerId()); draft.setContractUserId(projectInfo.getContractUserId()); draft.setRemark("系统根据项目完成自动创建,请补充后发起审批"); - - Long contractId = resolveShippingContractId(projectInfo); - if (contractId == null) { - return draft; - } - - ErpContractInfoVo contractInfo = erpContractInfoService.queryById(contractId); - if (contractInfo == null) { - return draft; - } - draft.setContractId(contractInfo.getContractId()); - draft.setContractCode(contractInfo.getContractCode()); - draft.setContractName(contractInfo.getContractName()); - draft.setOrderContractCode(contractInfo.getOrderContractCode()); - draft.setCustomerId(contractInfo.getOneCustomerId()); - draft.setCustomerName(contractInfo.getOneCustomerName()); - draft.setShippingAddress(contractInfo.getDetailedAddress()); - // 自动草稿只做“带得出来就先带”,联系人优先取商务联系人,没有时再退化到技术联系人,避免无意义空白单 - draft.setReceiverName(firstNonBlank(contractInfo.getOneBusinessContact(), contractInfo.getOneTechnicalContact(), contractInfo.getOneRepresent())); - draft.setReceiverPhone(firstNonBlank(contractInfo.getOneBusinessContactPhone(), contractInfo.getOneTechnicalContactPhone())); - draft.setDirections(StringUtils.defaultIfBlank(contractInfo.getMaterialRemark(), contractInfo.getRemark())); - draft.setDetails(buildShippingDraftDetails(contractInfo.getContractMaterialList())); - - //显示填充租户、创建人、创建时间 + // 显式补齐租户、创建人、创建部门等审计字段,确保远程创建草稿时上下文完整 draft.setTenantId(projectInfo.getTenantId()); draft.setCreateBy(projectInfo.getCreateBy()); + // 草稿创建时间以当前生成时间为准,表示“发货草稿建立时刻”,而不是项目创建时间 draft.setCreateTime(new Date()); draft.setCreateDept(projectInfo.getDeptId()); + // 项目可能同时关联主合同和多条项目关联合同,这里统一解析出“可用于发货”的合同集合 + List contractInfos = resolveShippingContracts(projectInfo); + if (CollUtil.isEmpty(contractInfos)) { + // 没有关联合同时仍返回主表草稿,允许后续人工补充,避免因为基础数据不完整而阻断流程 + return draft; + } + + // 主表默认仍只展示一份合同,但为了联调和后续待办展示完整性, + // 这里采用“主合同优先、首个可用合同兜底、其他合同补空值”的策略,尽可能把客户与收货信息带全 + fillDraftHeaderByContracts(draft, projectInfo, contractInfos); + // 项目和合同是多对多关系,发货草稿不能只带一份合同的物料, + // 这里需要把项目下所有关联合同的物料统一汇总后再落到发货明细里 + draft.setDetails(buildShippingDraftDetails(contractInfos)); + return draft; } @@ -506,60 +501,258 @@ public class ErpProjectInfoServiceImpl implements IErpProjectInfoService { * @return 发货类型 */ private String resolveShippingType(ErpProjectInfo projectInfo) { + // 备件项目默认走备件发货类型,便于 WMS 侧在单据初始状态就套用对应业务规则 if ("1".equals(projectInfo.getSpareFlag())) { return "2"; } + // 非备件项目先不强行指定类型,交由后续人工确认,避免这里做过度推断 return null; } + + /** - * 解析发货默认合同 + * 解析项目下所有用于发货的合同 * * @param projectInfo 项目 - * @return 合同ID + * @return 合同列表 */ - private Long resolveShippingContractId(ErpProjectInfo projectInfo) { + private List resolveShippingContracts(ErpProjectInfo projectInfo) { + // 用有序 Set 去重,既避免重复合同,又保留“主合同优先、关联合同随后”的稳定顺序 + Set contractIds = new LinkedHashSet<>(); if (projectInfo.getContractId() != null) { - return projectInfo.getContractId(); + // 项目主合同优先放入,后续若又出现在关联表中会自动去重,不影响主合同优先级 + contractIds.add(projectInfo.getContractId()); } + // 关联表里维护的是项目补充关联的合同,按排序字段读取,保证草稿展示和用户维护顺序一致 List projectContracts = projectContractsMapper.selectList(Wrappers.lambdaQuery() .eq(ErpProjectContracts::getProjectId, projectInfo.getProjectId()) .eq(ErpProjectContracts::getDelFlag, "0") .orderByAsc(ErpProjectContracts::getSortOrder) .orderByAsc(ErpProjectContracts::getProjectContractsId)); - if (CollUtil.isEmpty(projectContracts)) { + if (CollUtil.isNotEmpty(projectContracts)) { + projectContracts.stream() + .map(ErpProjectContracts::getContractId) + // 过滤空合同 ID,避免脏数据导致远程查询报错 + .filter(Objects::nonNull) + .forEach(contractIds::add); + } + if (CollUtil.isEmpty(contractIds)) { + // 没有任何可用合同则返回空集合,由上层决定是否仅创建空草稿头 + return Collections.emptyList(); + } + List contractInfos = new ArrayList<>(contractIds.size()); + for (Long contractId : contractIds) { + // 合同详情统一通过服务层获取,复用已有封装,避免这里直接拼 join 破坏服务边界 + ErpContractInfoVo contractInfo = erpContractInfoService.queryById(contractId); + if (contractInfo != null) { + contractInfos.add(contractInfo); + } + // 查询不到的合同直接跳过,保证自动建草稿过程尽量向前推进,不因单条脏数据全量失败 + } + return contractInfos; + } + + /** + * 解析发货主表默认展示合同 + * + * @param projectInfo 项目 + * @param contractInfos 合同列表 + * @return 主展示合同 + */ + private ErpContractInfoVo resolvePrimaryShippingContract(ErpProjectInfo projectInfo, List contractInfos) { + if (CollUtil.isEmpty(contractInfos)) { return null; } - return projectContracts.get(0).getContractId(); + if (projectInfo.getContractId() != null) { + for (ErpContractInfoVo contractInfo : contractInfos) { + // 如果项目主合同仍在可用合同集合内,优先作为主表展示合同,保证与项目主数据一致 + if (Objects.equals(projectInfo.getContractId(), contractInfo.getContractId())) { + return contractInfo; + } + } + } + if (contractInfos.size() == 1) { + // 没有显式主合同但只有一份合同时,直接使用该合同,减少后续人工补录成本 + return contractInfos.get(0); + } + // 多合同且无法明确主合同场景下,不自动拍板,避免主表误展示错误客户或地址信息 + return null; + } + + /** + * 用合同集合尽可能补齐发货草稿表头 + * + * @param draft 发货草稿 + * @param projectInfo 项目 + * @param contractInfos 合同集合 + */ + private void fillDraftHeaderByContracts(RemoteWmsShippingDraft draft, ErpProjectInfo projectInfo, List contractInfos) { + if (CollUtil.isEmpty(contractInfos)) { + return; + } + ErpContractInfoVo primaryContract = resolvePrimaryShippingContract(projectInfo, contractInfos); + if (primaryContract == null) { + // 调试与代办展示场景更关注“先有值可看”,因此无显式主合同时退化到首个可用合同 + primaryContract = contractInfos.get(0); + } + fillDraftHeaderByContract(draft, primaryContract); + + for (ErpContractInfoVo contractInfo : contractInfos) { + // 多合同项目常见“某份合同有客户、另一份合同有联系人/交货地点”的情况,这里只补空值不覆盖已选主合同 + supplementDraftHeaderByContract(draft, contractInfo); + } + } + + /** + * 用主合同回填发货主表默认展示信息 + * + * @param draft 发货草稿 + * @param contractInfo 主合同 + */ + private void fillDraftHeaderByContract(RemoteWmsShippingDraft draft, ErpContractInfoVo contractInfo) { + if (contractInfo == null) { + return; + } + // 表头合同信息只回填“默认展示值”,后续若业务选择其他合同,草稿仍可再编辑 + draft.setContractId(contractInfo.getContractId()); + draft.setContractCode(contractInfo.getContractCode()); + draft.setContractName(contractInfo.getContractName()); + draft.setOrderContractCode(contractInfo.getOrderContractCode()); + // 客户、收货地址等默认取自主合同,保证新建草稿时用户先看到最可能正确的一套信息 + draft.setCustomerId(resolveShippingCustomerId(contractInfo)); + draft.setCustomerName(resolveShippingCustomerName(contractInfo)); + draft.setShippingAddress(resolveShippingAddress(contractInfo)); + // 主表联系人只做默认带出,优先用甲方商务/技术联系人,缺失时再退化到授权代表与乙方联系人,减少草稿空值 + draft.setReceiverName(resolveReceiverName(contractInfo)); + // 联系电话保持与联系人取值策略一致,优先补齐客户侧电话,没有时再退化其他联系人电话 + draft.setReceiverPhone(resolveReceiverPhone(contractInfo)); + // 发货说明优先采用物料备注,因为它更贴近履约交付要求;缺失时再退回合同备注 + draft.setDirections(resolveDirections(contractInfo)); + } + + /** + * 用其他合同补齐主展示合同缺失的字段 + * + * @param draft 发货草稿 + * @param contractInfo 候选合同 + */ + private void supplementDraftHeaderByContract(RemoteWmsShippingDraft draft, ErpContractInfoVo contractInfo) { + if (contractInfo == null) { + return; + } + if (draft.getContractId() == null) { + draft.setContractId(contractInfo.getContractId()); + } + if (StringUtils.isBlank(draft.getContractCode())) { + draft.setContractCode(contractInfo.getContractCode()); + } + if (StringUtils.isBlank(draft.getContractName())) { + draft.setContractName(contractInfo.getContractName()); + } + if (StringUtils.isBlank(draft.getOrderContractCode())) { + draft.setOrderContractCode(contractInfo.getOrderContractCode()); + } + if (draft.getCustomerId() == null) { + draft.setCustomerId(resolveShippingCustomerId(contractInfo)); + } + if (StringUtils.isBlank(draft.getCustomerName())) { + draft.setCustomerName(resolveShippingCustomerName(contractInfo)); + } + if (StringUtils.isBlank(draft.getShippingAddress())) { + draft.setShippingAddress(resolveShippingAddress(contractInfo)); + } + if (StringUtils.isBlank(draft.getReceiverName())) { + draft.setReceiverName(resolveReceiverName(contractInfo)); + } + if (StringUtils.isBlank(draft.getReceiverPhone())) { + draft.setReceiverPhone(resolveReceiverPhone(contractInfo)); + } + if (StringUtils.isBlank(draft.getDirections())) { + draft.setDirections(resolveDirections(contractInfo)); + } + } + + private Long resolveShippingCustomerId(ErpContractInfoVo contractInfo) { + return contractInfo.getOneCustomerId() != null ? contractInfo.getOneCustomerId() : contractInfo.getTwoCustomerId(); + } + + private String resolveShippingCustomerName(ErpContractInfoVo contractInfo) { + return firstNonBlank(contractInfo.getOneCustomerName(), contractInfo.getTwoCustomerName()); + } + + private String resolveShippingAddress(ErpContractInfoVo contractInfo) { + // 发货单的收货地址更应优先采用合同约定交货地点,办公地只作为兜底,避免地址空值或业务语义不符 + return firstNonBlank(contractInfo.getDeliveryLocation(), contractInfo.getDetailedAddress()); + } + + private String resolveReceiverName(ErpContractInfoVo contractInfo) { + return firstNonBlank( + contractInfo.getOneBusinessContact(), + contractInfo.getOneTechnicalContact(), + contractInfo.getOneRepresent(), + contractInfo.getTwoBusinessContact(), + contractInfo.getTwoTechnicalContact(), + contractInfo.getTwoRepresent() + ); + } + + private String resolveReceiverPhone(ErpContractInfoVo contractInfo) { + return firstNonBlank( + contractInfo.getOneBusinessContactPhone(), + contractInfo.getOneTechnicalContactPhone(), + contractInfo.getTwoBusinessContactPhone(), + contractInfo.getTwoTechnicalContactPhone() + ); + } + + private String resolveDirections(ErpContractInfoVo contractInfo) { + return firstNonBlank(contractInfo.getMaterialRemark(), contractInfo.getRemark()); } /** * 合同物料转发货快照 * - * @param materials 合同物料 + * @param contractInfos 合同列表 * @return 发货快照明细 */ - private List buildShippingDraftDetails(List materials) { - if (CollUtil.isEmpty(materials)) { + private List buildShippingDraftDetails(List contractInfos) { + if (CollUtil.isEmpty(contractInfos)) { return new ArrayList<>(); } - List details = new ArrayList<>(materials.size()); - for (ErpContractMaterialVo material : materials) { - RemoteWmsShippingDraftItem item = new RemoteWmsShippingDraftItem(); - // 这里复制的是“合同当前版本”的展示快照,发货草稿生成后即与合同解耦,后续合同调整不回写历史草稿 - item.setSourceDetailId(material.getContractMaterialId()); - item.setErpMaterialId(material.getMaterialId()); - item.setMaterielId(material.getMaterialId()); - item.setMaterialCode(material.getMaterialCode()); - item.setMaterialName(firstNonBlank(material.getSaleMaterialName(), material.getMaterialName(), material.getProductName())); - item.setMaterielSpecification(material.getSpecificationDescription()); - item.setShippingStockAmount(material.getAmount()); - item.setUnitId(material.getUnitId()); - item.setUnitName(material.getUnitName()); - item.setUnitPrice(material.getIncludingPrice()); - item.setTotalPrice(material.getSubtotal()); - item.setRemark(material.getRemark()); - details.add(item); + List details = new ArrayList<>(); + // 合同物料可能因主合同与关联合同重复引用同一条快照,这里按合同物料快照 ID 去重 + Set materialSnapshotIds = new HashSet<>(); + for (ErpContractInfoVo contractInfo : contractInfos) { + List materials = contractInfo.getContractMaterialList(); + if (CollUtil.isEmpty(materials)) { + // 单个合同没有物料时直接跳过,不影响其他合同的明细生成 + continue; + } + for (ErpContractMaterialVo material : materials) { + // 已处理过的合同物料快照不再重复落明细,避免自动生成草稿时出现重复发货项 + if (material.getContractMaterialId() != null && !materialSnapshotIds.add(material.getContractMaterialId())) { + continue; + } + RemoteWmsShippingDraftItem item = new RemoteWmsShippingDraftItem(); + // 这里复制的是“合同当前版本”的展示快照,发货草稿生成后即与合同解耦,后续合同调整不回写历史草稿 + item.setSourceDetailId(material.getContractMaterialId()); + // ERP 物料 ID 与 WMS 物料 ID 当前共用同一来源值,先完整回填,降低后续映射成本 + item.setErpMaterialId(material.getMaterialId()); + item.setMaterielId(material.getMaterialId()); + // 名称优先级以销售名称为先,兼顾客户视角展示;缺失时再退回基础物料名和产品名 + item.setMaterialCode(material.getMaterialCode()); + item.setMaterialName(firstNonBlank(material.getSaleMaterialName(), material.getMaterialName(), material.getProductName())); + item.setMaterielSpecification(material.getSpecificationDescription()); + // 发货数量先取合同约定数量,自动草稿阶段不在这里扣减历史已发量,避免引入复杂结转逻辑 + item.setShippingStockAmount(material.getAmount()); + item.setUnitId(material.getUnitId()); + item.setUnitName(material.getUnitName()); + item.setUnitPrice(material.getIncludingPrice()); + item.setTotalPrice(material.getSubtotal()); + item.setRemark(material.getRemark()); + details.add(item); + } } return details; } @@ -569,10 +762,12 @@ public class ErpProjectInfoServiceImpl implements IErpProjectInfoService { return null; } for (String value : values) { + // 统一在一个工具方法里做“按优先级取第一个非空白值”,避免各处重复写判空分支 if (StringUtils.isNotBlank(value)) { return value; } } + // 所有候选值都为空白时返回 null,让调用方明确感知“没有可用默认值” return null; } diff --git a/ruoyi-modules/ruoyi-wms/src/main/java/org/dromara/wms/service/impl/WmsShippingBillServiceImpl.java b/ruoyi-modules/ruoyi-wms/src/main/java/org/dromara/wms/service/impl/WmsShippingBillServiceImpl.java index 5343d3cd..759cb580 100644 --- a/ruoyi-modules/ruoyi-wms/src/main/java/org/dromara/wms/service/impl/WmsShippingBillServiceImpl.java +++ b/ruoyi-modules/ruoyi-wms/src/main/java/org/dromara/wms/service/impl/WmsShippingBillServiceImpl.java @@ -207,6 +207,7 @@ public class WmsShippingBillServiceImpl implements IWmsShippingBillService { @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(WmsShippingBillBo bo) { WmsShippingBill update = MapstructUtils.convert(bo, WmsShippingBill.class); + fillArrivalConfirmAuditFields(update); validEntityBeforeSave(update); boolean flag = baseMapper.updateById(update) > 0; @@ -245,6 +246,22 @@ public class WmsShippingBillServiceImpl implements IWmsShippingBillService { return flag; } + private void fillArrivalConfirmAuditFields(WmsShippingBill entity) { + boolean hasArrivalConfirmData = StringUtils.isNotBlank(entity.getIsAllReceiving()) + || StringUtils.isNotBlank(entity.getArrivalReceiptOssId()); + if (!hasArrivalConfirmData) { + return; + } + if (entity.getArrivalConfirmTime() == null) { + // 到货确认时间必须以后端落库时间为准,避免前端漏传后页面与台账都看不到确认时间 + entity.setArrivalConfirmTime(new Date()); + } + if (entity.getArrivalConfirmBy() == null && LoginHelper.getUserId() != null) { + // 确认人属于审计字段,必须依赖当前登录态补齐,不能信任前端传值 + entity.setArrivalConfirmBy(LoginHelper.getUserId()); + } + } + /** * 根据项目快照创建发货草稿 *