diff --git a/oms_web/oms_vue/src/api/inventory/delivery.js b/oms_web/oms_vue/src/api/inventory/delivery.js index 0550cba4..c6339d2e 100644 --- a/oms_web/oms_vue/src/api/inventory/delivery.js +++ b/oms_web/oms_vue/src/api/inventory/delivery.js @@ -97,6 +97,15 @@ export function listProductSn(query) { }) } +// 查询仅维保订单发货可续用的SN(关联项目同产品编码的已发货SN) +export function maintenanceSn(query) { + return request({ + url: '/inventory/delivery/vue/maintenanceSn', + method: 'get', + params: query + }) +} + export function removeDelivery(id) { return request({ url: '/inventory/delivery/vue/' + id, diff --git a/oms_web/oms_vue/src/api/project/order.js b/oms_web/oms_vue/src/api/project/order.js index 039113ae..3f42f47d 100644 --- a/oms_web/oms_vue/src/api/project/order.js +++ b/oms_web/oms_vue/src/api/project/order.js @@ -86,6 +86,15 @@ export function updateFinanceStatus(id, status) { }) } +// 查询仅维保订单可关联的项目订单(仅正常订单且审批完成) +export function relatedCandidateList(query) { + return request({ + url: '/project/order/vue/relatedCandidateList', + method: 'get', + params: query + }) +} + // 查询产品配货备货明细 export function productMatchList(orderCode) { return request({ diff --git a/oms_web/oms_vue/src/views/inventory/outer/components/OuterForm.vue b/oms_web/oms_vue/src/views/inventory/outer/components/OuterForm.vue index cb5f947a..6a5b5c28 100644 --- a/oms_web/oms_vue/src/views/inventory/outer/components/OuterForm.vue +++ b/oms_web/oms_vue/src/views/inventory/outer/components/OuterForm.vue @@ -89,6 +89,8 @@ + + @@ -130,12 +132,13 @@ import { changeOuterStatus, getOuter, queryInfo } from '@/api/inventory/outer'; import { removeDelivery, updateDeliveryStatus, recallDelivery, recallApply, checkRecallByDelivery } from '@/api/inventory/delivery'; import GenerateDeliveryForm from './GenerateDeliveryForm.vue'; +import MaintenanceDeliveryForm from './MaintenanceDeliveryForm.vue'; import DeliveryDetail from '@/views/inventory/delivery/Detail.vue'; import OuterRebackDetail from '@/views/approve/all/components/OuterRebackDetail.vue'; export default { name: "OuterForm", - components: { GenerateDeliveryForm, DeliveryDetail, OuterRebackDetail }, + components: { GenerateDeliveryForm, MaintenanceDeliveryForm, DeliveryDetail, OuterRebackDetail }, props: { viewOnly: { type: Boolean, @@ -238,7 +241,12 @@ export default { ...this.form, notifierAddress: this.form.notifierAddress || row.notifierAddress || fallbackNotifierAddress || '' }; - this.$refs.deliveryForm.open(row, formData); + // 仅维保订单:SN直接续用关联项目已发货SN,走专用发货弹窗 + if (String(row.orderType || this.form.orderType) === '2') { + this.$refs.maintenanceDeliveryForm.open(row, formData); + } else { + this.$refs.deliveryForm.open(row, formData); + } }, // 取消发货 handleDeleteDelivery(deliveryId) { diff --git a/oms_web/oms_vue/src/views/project/order/OrderDetail.vue b/oms_web/oms_vue/src/views/project/order/OrderDetail.vue index 480dfd03..343a4673 100644 --- a/oms_web/oms_vue/src/views/project/order/OrderDetail.vue +++ b/oms_web/oms_vue/src/views/project/order/OrderDetail.vue @@ -13,12 +13,13 @@ @select-project="handleSelectProject" @select-user="openSelectPeople" @select-partner="openSelectPartner" + @select-related-order="openSelectRelatedOrder" />
- +
@@ -152,6 +153,7 @@ + @@ -172,6 +174,7 @@ import OrderInfo from '@/views/project/order/components/OrderInfo.vue'; import SelectProject from "@/views/project/info/SelectProject"; import SelectUser from "@/views/system/user/selectUser"; import SelectPartner from "@/views/system/partner/selectPartner"; +import RelatedOrderSelectDialog from "@/views/project/order/components/RelatedOrderSelectDialog.vue"; import {isEmpty} from "@/utils/validate"; export default { @@ -183,6 +186,7 @@ export default { SelectProject, SelectUser, SelectPartner, + RelatedOrderSelectDialog, }, props: { visible: { @@ -223,6 +227,7 @@ export default { selectProjectVisible: false, selectUserVisible: false, selectPartnerVisible: false, + selectRelatedOrderVisible: false, selectCommitTypeVisible: false, submitLoading: false, // 当前激活的Tab @@ -251,6 +256,13 @@ export default { notifierPhone: [{ required: true, message: "联系方式不能为空", trigger: "blur" }], paymentMethod: [{ required: true, message: "付款方式不能为空", trigger: "change" }], paymentRatio: [{ required: true, message: "付款比例不能为空", trigger: "blur" }], + relatedOrderId: [{ validator: (rule, value, callback) => { + if (this.form.orderType === '2' && !value) { + callback(new Error('仅维保订单必须选择关联项目')); + } else { + callback(); + } + }, trigger: 'change' }], } }; }, @@ -271,6 +283,17 @@ export default { const isRebackApproved = this.form.orderStatus === '2' && this.form.rebackApproveStatus === '2'; return !isRebackApproved && (this.form.orderStatus === '1' || this.form.orderStatus === '2'); }, + // 产品配置可编辑条件:原有口径是「已选所属项目且订单未审批/未在批」; + // 仅维保订单带出关联订单的软件配置后,即使还没选自己的所属项目也要能直接改(配置最终归属本项目) + productConfigDisabled() { + if (this.isOrderApprovedOrInReview) { + return true; + } + if (this.form.orderType === '2' && this.form.relatedOrderId) { + return false; + } + return !this.isProjectSelected; + }, uniqueVersions() { if (!this.approveLogList || this.approveLogList.length === 0) return []; const versions = [...new Set(this.approveLogList.map(log => log.extendField1))]; @@ -439,6 +462,7 @@ export default { dutyEmail: null, dutyPhone: null, partnerName: null, level: null, partnerUserName: null, partnerEmail: null, partnerPhone: null, notifierAddress: null, notifier: null, notifierEmail: null, notifierPhone: null, paymentMethod: null, paymentRatio: 0, paymentDescription: null, remark: null, + orderType: null, relatedOrderId: null, relatedOrderCode: null, relatedProjectName: null, softwareProjectProductInfoList: [], hardwareProjectProductInfoList: [], maintenanceProjectProductInfoList: [] }; this.orderBakFile = {}; @@ -523,7 +547,14 @@ export default { this.form.businessPerson = projectData.contactPerson; this.form.businessEmail = projectData.contactEmail; this.form.businessPhone = projectData.contactPhone; - this.form.softwareProjectProductInfoList = projectData.softwareProjectProductInfoList || []; + const incomingSoftware = projectData.softwareProjectProductInfoList || []; + // 仅维保订单已带出关联订单的软件配置时,随后选自己的项目不能把它清空(新项目本就还没有产品行) + const keepRelatedSoftware = !incomingSoftware.length + && this.form.orderType === '2' && !!this.form.relatedOrderId + && (this.form.softwareProjectProductInfoList || []).length > 0; + if (!keepRelatedSoftware) { + this.form.softwareProjectProductInfoList = incomingSoftware; + } this.form.hardwareProjectProductInfoList = projectData.hardwareProjectProductInfoList || []; this.form.maintenanceProjectProductInfoList = projectData.maintenanceProjectProductInfoList || []; this.form.industryType = projectData.industryType; @@ -531,6 +562,53 @@ export default { this.selectProjectVisible = false; this.form.shipmentAmount = this.totalConfigAmount; }, + openSelectRelatedOrder() { + this.selectRelatedOrderVisible = true; + }, + // 选择关联项目:记录关联订单,并把关联订单的软件配置带入当前订单 + // 复制出来的产品行必须清空 id/projectId,否则保存时会按关联项目的范围物理删除或更新产品行 + handleRelatedOrderSelected(row) { + this.$set(this.form, 'relatedOrderId', row.id); + this.$set(this.form, 'relatedOrderCode', row.orderCode); + this.$set(this.form, 'relatedProjectName', row.projectName); + this.selectRelatedOrderVisible = false; + // 同一项目的产品配置本就共用,无需复制 + if (row.projectId && this.form.projectId && row.projectId === this.form.projectId) { + this.$modal.msgSuccess('已关联项目:' + row.projectName); + this.validateRelatedOrderField(); + return; + } + getOrder(row.id).then(response => { + const related = (response.data && response.data.projectOrderInfo) || {}; + const softwareList = (related.softwareProjectProductInfoList || []).map(item => { + return Object.assign({}, item, { id: null, projectId: null }); + }); + if (softwareList.length === 0) { + this.$modal.msgWarning('关联项目未配置软件产品,请手动维护产品配置信息'); + this.validateRelatedOrderField(); + return; + } + const overwrite = () => { + this.form.softwareProjectProductInfoList = softwareList; + this.form.shipmentAmount = this.totalConfigAmount; + this.$modal.msgSuccess('已带出关联项目的软件配置'); + this.validateRelatedOrderField(); + }; + if ((this.form.softwareProjectProductInfoList || []).length > 0) { + this.$modal.confirm('将使用关联项目的软件配置覆盖当前订单的软件产品,是否继续?').then(overwrite).catch(() => {}); + } else { + overwrite(); + } + }); + }, + // 关联项目为只读输入框,赋值后需手动触发校验 + validateRelatedOrderField() { + this.$nextTick(() => { + if (this.$refs.form) { + this.$refs.form.validateField('relatedOrderId', () => {}); + } + }); + }, openSelectPeople() { this.selectUserVisible = true; }, diff --git a/oms_web/oms_vue/src/views/project/order/components/OrderInfo.vue b/oms_web/oms_vue/src/views/project/order/components/OrderInfo.vue index dd5f0b96..c80b0dcc 100644 --- a/oms_web/oms_vue/src/views/project/order/components/OrderInfo.vue +++ b/oms_web/oms_vue/src/views/project/order/components/OrderInfo.vue @@ -68,11 +68,30 @@ - + + + + + + + + + @@ -290,6 +309,17 @@ export default { } }, dicts:['delivery_order_type'], + computed: { + // 关联项目回显文本:关联订单编码 / 关联项目名称 + relatedOrderText() { + const code = this.localOrderData.relatedOrderCode; + const name = this.localOrderData.relatedProjectName; + if (!code && !name) { + return ''; + } + return [code, name].filter(item => !!item).join(' / '); + } + }, data() { return { localOrderData: {}, @@ -331,6 +361,25 @@ export default { }, methods: { getDicts, + // 切换订单类型:非仅维保时清空关联项目 + handleOrderTypeChange(value) { + if (value !== '2') { + this.clearRelatedOrder(); + } + }, + // 打开关联项目选择弹窗,只读态不响应 + openRelatedOrderDialog() { + if (this.isReadonly) { + return; + } + this.$emit('select-related-order'); + }, + // 清空关联项目:关联订单id/编码/项目名称一并清除,产品配置保持不动 + clearRelatedOrder() { + this.$set(this.localOrderData, 'relatedOrderId', null); + this.$set(this.localOrderData, 'relatedOrderCode', null); + this.$set(this.localOrderData, 'relatedProjectName', null); + }, handleBgChange(value, isInitialLoad = false) { if (!isInitialLoad) { this.localOrderData.industryType = null; diff --git a/oms_web/oms_vue/src/views/project/order/components/OrderInfoDisplay.vue b/oms_web/oms_vue/src/views/project/order/components/OrderInfoDisplay.vue index 961d7193..5968b541 100644 --- a/oms_web/oms_vue/src/views/project/order/components/OrderInfoDisplay.vue +++ b/oms_web/oms_vue/src/views/project/order/components/OrderInfoDisplay.vue @@ -106,6 +106,10 @@ 付款比例 {{ displayValue(localOrderData.paymentRatio, true) }} + + 关联项目 + {{ displayValue(relatedText) }} + 付款条件 {{ displayValue(localOrderData.paymentDescription) }} @@ -180,6 +184,16 @@ export default { paymentMethodOptions: [], }; }, + computed: { + // 仅维保订单展示的关联项目:订单编号 / 项目名称 + relatedText() { + if (!this.localOrderData.relatedOrderCode) { return ''; } + if (this.localOrderData.relatedProjectName) { + return this.localOrderData.relatedOrderCode + ' / ' + this.localOrderData.relatedProjectName; + } + return this.localOrderData.relatedOrderCode; + }, + }, watch: { orderData: { handler(newValue) { diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueDeliveryController.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueDeliveryController.java index 0da88326..e0d99696 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueDeliveryController.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueDeliveryController.java @@ -75,6 +75,15 @@ public class VueDeliveryController extends BaseController { return AjaxResult.success(inventoryDelivery); } + /** + * 查询仅维保订单发货可续用的SN(关联项目下同产品编码的已发货SN) + */ + @RequiresPermissions("inventory:delivery:add") + @GetMapping("/maintenanceSn") + public AjaxResult maintenanceSn(@RequestParam String orderCode, @RequestParam String productCode) { + return AjaxResult.success(inventoryDeliveryService.getMaintenanceSnList(orderCode, productCode)); + } + /** * 新增发货记录 */ diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueProjectOrderInfoController.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueProjectOrderInfoController.java index 39283441..8aa5ead3 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueProjectOrderInfoController.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/vue/VueProjectOrderInfoController.java @@ -169,6 +169,19 @@ public class VueProjectOrderInfoController extends BaseController { return AjaxResult.success(fileName); } + /** + * 仅维保订单「关联项目」候选订单:只返回已审批完成的正常订单,可排除自身 + */ + @RequiresPermissions("project:order:list") + @GetMapping("/relatedCandidateList") + public TableDataInfo relatedCandidateList(@RequestParam(required = false) String projectCode, + @RequestParam(required = false) String projectName, + @RequestParam(required = false) Long excludeOrderId) { + startPage(); + List list = projectOrderInfoService.selectRelatedCandidateList(projectCode, projectName, excludeOrderId); + return getDataTable(list); + } + /** * 配备货产品信息 */ diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java index e2b18254..a9e52bc1 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java @@ -73,6 +73,21 @@ public class ProjectOrderInfo extends BaseEntity { private String customerUserName; private String orderType; + /** + * 关联项目订单id:仅维保订单(orderType=2)必填,指向被关联的正常订单 + */ + private Long relatedOrderId; + + /** + * 关联项目订单编码(只读,联表带出) + */ + private String relatedOrderCode; + + /** + * 关联项目名称(只读,联表带出) + */ + private String relatedProjectName; + /** * 出货金额 */ @@ -317,6 +332,25 @@ public class ProjectOrderInfo extends BaseEntity { */ private Integer operationVersion; + /** + * 订单类型 1:正常订单 2:仅维保 + */ + @Getter + public enum OrderTypeEnum { + NORMAL("1", "正常订单"), + MAINTENANCE("2", "仅维保"), + + ; + + private final String value; + private final String code; + + OrderTypeEnum(String code, String value) { + this.code = code; + this.value = value; + } + + } @Getter public enum OrderStatus { WAIT_COMMIT("0", "待提交"), diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/InventoryDeliveryMapper.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/InventoryDeliveryMapper.java index a3563f5b..12309f25 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/InventoryDeliveryMapper.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/InventoryDeliveryMapper.java @@ -7,6 +7,7 @@ import com.ruoyi.sip.domain.InventoryDelivery; import com.ruoyi.sip.dto.ApiDataQueryDto; import com.ruoyi.sip.vo.DeliveryApproveVo; import com.ruoyi.sip.vo.DeliveryInfoVo; +import com.ruoyi.sip.vo.MaintenanceSnVo; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; @@ -71,6 +72,15 @@ public interface InventoryDeliveryMapper List listSn(ApiDataQueryDto dto); + /** + * 查询指定订单指定产品编码下已发货的SN(仅维保订单续用) + * + * @param orderCode 关联订单编码 + * @param productCode 产品编码 + * @return 已发货SN列表 + */ + List listMaintenanceSn(@Param("orderCode") String orderCode, @Param("productCode") String productCode); + List selectDeliveryApproveList(@Param("id") Long id); /** diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/ProjectOrderInfoMapper.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/ProjectOrderInfoMapper.java index 020e1bdb..2f652b97 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/ProjectOrderInfoMapper.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/mapper/ProjectOrderInfoMapper.java @@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper; import java.math.BigDecimal; import java.util.List; +import java.util.Map; import com.ruoyi.sip.domain.InventoryDelivery; import com.ruoyi.sip.domain.ProjectOrderInfo; @@ -38,6 +39,14 @@ public interface ProjectOrderInfoMapper * @return 订单管理集合 */ public List selectProjectOrderInfoList(ProjectOrderInfo projectOrderInfo); + + /** + * 仅维保订单「关联项目」候选订单:已审批完成的正常订单 + * + * @param params projectCode 项目编码 / projectName 项目名称 / excludeOrderId 需排除的订单id + * @return 候选订单集合 + */ + public List selectRelatedCandidateList(Map params); public ProjectOrderInfo selectProjectOrderInfoByCode(String orderCode); /** @@ -64,6 +73,14 @@ public interface ProjectOrderInfoMapper */ public int clearRebackApproveStatus(Long id); + /** + * 清空订单的关联项目(仅维保订单改回正常订单时使用) + * + * @param id 订单管理主键 + * @return 结果 + */ + public int clearRelatedOrderId(Long id); + /** * 删除订单管理 * diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IInventoryDeliveryService.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IInventoryDeliveryService.java index 5ba630ce..426bb8d6 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IInventoryDeliveryService.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IInventoryDeliveryService.java @@ -6,6 +6,7 @@ import com.ruoyi.sip.domain.InventoryDelivery; import com.ruoyi.sip.dto.ApiDataQueryDto; import com.ruoyi.sip.dto.inventory.InventoryDeliveryDetailExcelDto; import com.ruoyi.sip.vo.DeliveryInfoVo; +import com.ruoyi.sip.vo.MaintenanceDeliveryVo; /** * 产品库存Service接口 @@ -110,4 +111,13 @@ public interface IInventoryDeliveryService void generateServiceBillsWhenAllDelivered(String orderCode); + /** + * 查询仅维保订单发货可续用的SN(关联项目下同产品编码的已发货SN) + * + * @param orderCode 仅维保订单编码 + * @param productCode 产品编码 + * @return 关联项目信息与可续用的SN列表 + */ + MaintenanceDeliveryVo getMaintenanceSnList(String orderCode, String productCode); + } diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java index 833abddc..1435c494 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java @@ -54,6 +54,16 @@ public interface IProjectOrderInfoService * @return 结果 */ public int updateProjectOrderInfo(ProjectOrderInfo projectOrderInfo); + + /** + * 仅维保订单「关联项目」候选订单列表(已审批完成的正常订单) + * + * @param projectCode 项目编码,模糊匹配,可为空 + * @param projectName 项目名称,模糊匹配,可为空 + * @param excludeOrderId 需要排除的订单id(编辑时排除自身),可为空 + * @return 候选订单集合 + */ + List selectRelatedCandidateList(String projectCode, String projectName, Long excludeOrderId); public int updateProjectOrderInfoByCode(ProjectOrderInfo projectOrderInfo); /** diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/InventoryDeliveryServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/InventoryDeliveryServiceImpl.java index 81468970..ac064321 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/InventoryDeliveryServiceImpl.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/InventoryDeliveryServiceImpl.java @@ -27,6 +27,8 @@ import com.ruoyi.sip.mapper.OmsPurchaseOrderMapMapper; import com.ruoyi.sip.service.*; import com.ruoyi.sip.vo.DeliveryApproveVo; import com.ruoyi.sip.vo.DeliveryInfoVo; +import com.ruoyi.sip.vo.MaintenanceDeliveryVo; +import com.ruoyi.sip.vo.MaintenanceSnVo; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.flowable.engine.RuntimeService; @@ -145,6 +147,8 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, throw new ServiceException("发货清单为空,保存失败"); } + //仅维保订单:发货SN直接沿用关联项目已发货的SN,不校验采购绑定、不回写SN库存信息 + boolean maintenanceOnly = false; if (StringUtils.isNotEmpty(inventoryDelivery.getOrderCode())) { ProjectOrderInfo orderInfo = projectOrderInfoService.selectProjectOrderInfoByOrderCode(inventoryDelivery.getOrderCode()); if (orderInfo == null) { @@ -152,8 +156,11 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, } //版本号取订单的version_code inventoryDelivery.setVersionCode(orderInfo.getVersionCode()); + maintenanceOnly = ProjectOrderInfo.OrderTypeEnum.MAINTENANCE.getCode().equals(orderInfo.getOrderType()); - if (StringUtils.isNotEmpty(inventoryDelivery.getPurchaseNo())) { + if (maintenanceOnly) { + validateMaintenanceDeliverySn(inventoryDelivery.getOrderCode(), inventoryDelivery.getProductCode(), productSnList); + } else if (StringUtils.isNotEmpty(inventoryDelivery.getPurchaseNo())) { int newSnCount = CollUtil.isEmpty(productSnList) ? productSnDataList.size() : productSnList.size(); OmsPurchaseOrder purchaseOrder = omsPurchaseOrderService.selectOmsPurchaseOrderByNo(inventoryDelivery.getPurchaseNo()); if (purchaseOrder == null) { @@ -276,7 +283,10 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, return info; }).collect(Collectors.toList()); - inventoryInfoService.saveBatch(inventoryInfoList); + //仅维保发货只写发货单与发货明细,SN的库存状态、出库单号、订单归属保持关联项目原值 + if (!maintenanceOnly) { + inventoryInfoService.saveBatch(inventoryInfoList); + } } else { Long warehouseId = productSnDataList.get(0).getWarehouseId(); if (warehouseId!=null && !warehouseId.equals(inventoryDelivery.getWarehouseId())){ @@ -615,9 +625,19 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, List productInfos = productInfoService.selectProductInfoList(productInfo); productInfo.setType(ProductInfo.ProductTypeEnum.SERVICE.getType()); productInfos.addAll(productInfoService.selectProductInfoList(productInfo)); - // 设置服务等级和服务结束时间 - for (DeliveryInfoVo deliveryInfoVo : resultList) { - updateDeliveryInfoVo(deliveryInfoVo, maintenanceTypeMap, productInfos); + // 设置服务等级和服务结束时间:同一SN+同一产品编码按订单排序(审批时间→创建时间→订单id升序), + // 后一单(仅维保)的维保起始日取前一单最后一段维保截止日,实现维保日期续算 + Map> snGroupMap = resultList.stream().collect(Collectors.groupingBy( + item -> item.getSerialNumber() + "#" + item.getProductCode(), LinkedHashMap::new, Collectors.toList())); + for (List sameSnList : snGroupMap.values()) { + sameSnList.sort(maintenanceSnComparator()); + Date carryStartTime = null; + for (DeliveryInfoVo deliveryInfoVo : sameSnList) { + Date lastServiceEndTime = updateDeliveryInfoVo(deliveryInfoVo, maintenanceTypeMap, productInfos, carryStartTime); + if (lastServiceEndTime != null) { + carryStartTime = lastServiceEndTime; + } + } } return resultList; } @@ -1473,10 +1493,16 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, /** * 更新 DeliveryInfoVo 的服务等级和服务结束时间 */ - private void updateDeliveryInfoVo(DeliveryInfoVo deliveryInfoVo, Map> maintenanceTypeMap, List productInfos) { + /** + * 计算发货明细的维保等级与维保起止时间 + * + * @param carryStartTime 续算起始日(同一SN上一单最后一段维保截止日),为空时按发货日期起算 + * @return 本单最后一段维保截止日,无维保段时返回 null,供下一单续算 + */ + private Date updateDeliveryInfoVo(DeliveryInfoVo deliveryInfoVo, Map> maintenanceTypeMap, List productInfos, Date carryStartTime) { List productInfoList = maintenanceTypeMap.get(deliveryInfoVo.getProjectId()); List serviceInfoList = new ArrayList<>(); - Date startTime = deliveryInfoVo.getServiceStartTime(); + Date startTime = carryStartTime != null ? carryStartTime : deliveryInfoVo.getServiceStartTime(); if (CollUtil.isNotEmpty(productInfoList) && ProductInfo.ProductTypeEnum.SOFTWARE.getType().equals(deliveryInfoVo.getProductType())) { for (ProjectProductInfo productInfo : productInfoList) { if (ProductInfo.ProductTypeLevel2ServiceEnum.SOFTWARE_QA.getType().equals(productInfo.getLevel2Type()) && StringUtils.isNotEmpty(productInfo.getValue())) { @@ -1511,6 +1537,10 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, } } deliveryInfoVo.setServiceInfo(serviceInfoList); + if (CollUtil.isEmpty(serviceInfoList)) { + return null; + } + return serviceInfoList.get(serviceInfoList.size() - 1).getServiceEndTime(); } private Date updateStartTimeAndAddList(DeliveryInfoVo deliveryInfoVo, ProjectProductInfo productInfo, Date startTime, List serviceInfoList) { @@ -1525,4 +1555,57 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, serviceInfoList.add(serviceInfo); return startTime; } + + /** + * 同一SN+产品编码的续算排序:审批时间(为空回退创建时间)升序,完全相同按订单id升序 + */ + private Comparator maintenanceSnComparator() { + Comparator dateComparator = Comparator.nullsLast(Comparator.naturalOrder()); + return Comparator.comparing((DeliveryInfoVo vo) -> vo.getApproveTime() != null ? vo.getApproveTime() : vo.getCreateTime(), dateComparator) + .thenComparing(DeliveryInfoVo::getOrderId, Comparator.nullsLast(Comparator.naturalOrder())); + } + + /** + * 查询仅维保订单发货可续用的SN(关联项目下同产品编码的已发货SN) + */ + @Override + public MaintenanceDeliveryVo getMaintenanceSnList(String orderCode, String productCode) { + if (StringUtils.isEmpty(orderCode) || StringUtils.isEmpty(productCode)) { + throw new ServiceException("订单编码和产品编码不能为空"); + } + ProjectOrderInfo orderInfo = projectOrderInfoService.selectProjectOrderInfoByOrderCode(orderCode); + if (orderInfo == null) { + throw new ServiceException("订单信息不存在"); + } + if (!ProjectOrderInfo.OrderTypeEnum.MAINTENANCE.getCode().equals(orderInfo.getOrderType())) { + throw new ServiceException("当前订单不是仅维保订单,无需续用关联项目SN"); + } + if (orderInfo.getRelatedOrderId() == null) { + throw new ServiceException("当前仅维保订单未配置关联项目,无法发货"); + } + ProjectOrderInfo relatedOrder = projectOrderInfoService.selectProjectOrderInfoById(orderInfo.getRelatedOrderId()); + if (relatedOrder == null) { + throw new ServiceException("关联项目订单不存在"); + } + List snList = inventoryDeliveryMapper.listMaintenanceSn(relatedOrder.getOrderCode(), productCode); + MaintenanceDeliveryVo vo = new MaintenanceDeliveryVo(); + vo.setRelatedOrderId(relatedOrder.getId()); + vo.setRelatedOrderCode(relatedOrder.getOrderCode()); + vo.setRelatedProjectName(relatedOrder.getProjectName()); + vo.setSnList(snList == null ? new ArrayList<>() : snList); + vo.setAvailableNum(CollUtil.isEmpty(snList) ? 0 : snList.size()); + return vo; + } + + /** + * 校验仅维保订单发货的SN必须来自关联项目同产品编码的已发货SN + */ + private void validateMaintenanceDeliverySn(String orderCode, String productCode, List productSnList) { + MaintenanceDeliveryVo maintenanceVo = getMaintenanceSnList(orderCode, productCode); + List availableSnList = maintenanceVo.getSnList().stream() + .map(MaintenanceSnVo::getProductSn).collect(Collectors.toList()); + if (CollUtil.isEmpty(productSnList) || !availableSnList.containsAll(productSnList)) { + throw new ServiceException("仅维保订单只能发关联项目已发货的SN,请重新选择发货清单"); + } + } } diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java index b2fed239..82c15b85 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java @@ -329,6 +329,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To if (projectOrderInfo.getProjectId() == null) { throw new ServiceException("请选择项目"); } + validateRelatedOrder(projectOrderInfo, projectOrderInfo.getOrderType()); List projectOrderInfos = this.selectProjectOrderInfoByProjectId(Collections.singletonList(projectOrderInfo.getProjectId())); if (CollUtil.isNotEmpty(projectOrderInfos)) { throw new ServiceException("该项目存在订单流转,无法添加"); @@ -448,6 +449,15 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To } projectInfoMapper.updateProjectInfo(projectInfo); + // 前端未回传订单类型时以库中已有类型为准,避免仅维保校验被绕过 + String effectiveOrderType = StringUtils.isNotEmpty(projectOrderInfo.getOrderType()) + ? projectOrderInfo.getOrderType() : existProjectOrderInfo.getOrderType(); + validateRelatedOrder(projectOrderInfo, effectiveOrderType); + if (!ProjectOrderInfo.OrderTypeEnum.MAINTENANCE.getCode().equals(effectiveOrderType) + && existProjectOrderInfo.getRelatedOrderId() != null) { + //订单类型由仅维保改回正常:动态update不会写入null,需显式清空历史关联 + projectOrderInfoMapper.clearRelatedOrderId(projectOrderInfo.getId()); + } //不更新订单编号 projectOrderInfo.setOrderCode(null); projectOrderInfo.setUpdateBy(ShiroUtils.getUserId().toString()); @@ -529,6 +539,46 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To return projectOrderInfoMapper.updateProjectOrderInfoByCode(projectOrderInfo); } + @Override + public List selectRelatedCandidateList(String projectCode, String projectName, Long excludeOrderId) { + Map params = new HashMap<>(); + params.put("projectCode", projectCode); + params.put("projectName", projectName); + params.put("excludeOrderId", excludeOrderId); + return projectOrderInfoMapper.selectRelatedCandidateList(params); + } + + /** + * 校验「关联项目」:仅维保订单必须关联一个已审批完成的正常订单,且不能是订单自身; + * 非仅维保订单不保留关联项目。 + * + * @param projectOrderInfo 待保存订单 + * @param orderType 生效的订单类型(编辑时前端可能不回传) + */ + private void validateRelatedOrder(ProjectOrderInfo projectOrderInfo, String orderType) { + if (!ProjectOrderInfo.OrderTypeEnum.MAINTENANCE.getCode().equals(orderType)) { + projectOrderInfo.setRelatedOrderId(null); + return; + } + Long relatedOrderId = projectOrderInfo.getRelatedOrderId(); + if (relatedOrderId == null) { + throw new ServiceException("仅维保订单必须选择关联项目"); + } + if (relatedOrderId.equals(projectOrderInfo.getId())) { + throw new ServiceException("关联项目不能选择当前订单本身"); + } + ProjectOrderInfo related = projectOrderInfoMapper.selectProjectOrderInfoById(relatedOrderId); + if (related == null) { + throw new ServiceException("关联项目订单不存在"); + } + if (!ProjectOrderInfo.OrderTypeEnum.NORMAL.getCode().equals(related.getOrderType())) { + throw new ServiceException("仅维保订单只能关联正常订单"); + } + if (!ProjectOrderInfo.OrderStatus.APPROVE_COMPLETE.getCode().equals(related.getOrderStatus())) { + throw new ServiceException("关联项目订单不处于审批完成状态,无法关联"); + } + } + private String getFlowBusinessKey(String code, String processType) { if (StringUtils.isEmpty(code)) { throw new ServiceException("流程BusinessKey为空"); diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/vo/DeliveryInfoVo.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/vo/DeliveryInfoVo.java index 22e41ffb..5d0dab03 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/vo/DeliveryInfoVo.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/vo/DeliveryInfoVo.java @@ -38,6 +38,17 @@ public class DeliveryInfoVo { private Date serviceStartTime; private List serviceInfo; + /** + * 订单审批完成时间:仅维保续算时用于订单排序,审批时间为空回退 createTime + */ + @JsonIgnore + private Date approveTime; + /** + * 订单创建时间 + */ + @JsonIgnore + private Date createTime; + private Date lastUpdateTime; @JsonIgnore private Long projectId; diff --git a/ruoyi-sip/src/main/resources/mapper/inventory/InventoryDeliveryMapper.xml b/ruoyi-sip/src/main/resources/mapper/inventory/InventoryDeliveryMapper.xml index f280f007..d0f3d0c6 100644 --- a/ruoyi-sip/src/main/resources/mapper/inventory/InventoryDeliveryMapper.xml +++ b/ruoyi-sip/src/main/resources/mapper/inventory/InventoryDeliveryMapper.xml @@ -85,51 +85,101 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where t3.order_code= #{orderCode} and t1.delivery_status = #{status} + + insert into oms_inventory_delivery diff --git a/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml b/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml index 3cc0beb9..f0c106ca 100644 --- a/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml +++ b/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml @@ -50,6 +50,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + @@ -57,7 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" shipment_amount, actual_purchase_amount, order_end_time, delivery_time, company_delivery, notifier,finance_status, notifier_email, notifier_phone, duty, duty_email, duty_phone, order_channel, partner_code, supplier,notifier_address, remark, order_status, create_by, create_time, update_by, update_time,version_code,process_type,process_template,discount_fold,order_stocking_status, - delivery_status,sign_status,outer_status,approve_time,payment_method,payment_ratio,payment_description,order_type,operation_version,reback_approve_status + delivery_status,sign_status,outer_status,approve_time,payment_method,payment_ratio,payment_description,order_type,operation_version,reback_approve_status,related_order_id from project_order_info t1 @@ -67,7 +70,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" t1.notifier_email, t1.notifier_phone, t1.duty, t1.duty_email, t1.duty_phone, t1.order_channel, t1.partner_code, t1.supplier, t1.remark, t1.order_status, t1.create_by, t1.create_time, t1.update_by, t1.update_time,t1.partner_user_name,t1.partner_email ,t1.partner_phone,t1.version_code,t1.process_type,t1.process_template,t1.discount_fold,t1.notifier_address,t1.finance_status, - t1.delivery_status,t1.sign_status,t1.outer_status,t1.approve_time,t1.payment_method,t1.payment_ratio,t1.payment_description,t1.operation_version,t1.order_stocking_status,t1.reback_approve_status + t1.delivery_status,t1.sign_status,t1.outer_status,t1.approve_time,t1.payment_method,t1.payment_ratio,t1.payment_description,t1.operation_version,t1.order_stocking_status,t1.reback_approve_status,t1.related_order_id ,t2.project_code,t2.project_name,t2.project_desc,t2.software_info,t2.hardware_info,t2.terminal_peripheral,t2.server_configuration ,t2.management_version,t2.desktop_vm_os_version,t2.vm_spec_quantity,t2.key_problem,t2.joint_trial_result ,t2.province,t2.customer_name,t2.customer_code,t2.industry_type,t2.bg_property,t2.agent_code,t2.estimated_order_time @@ -76,7 +79,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ,t4.agent_name ,t5.user_name as duty_name ,t6.charge_status,t6.biz_charge_date,t6.finance_charge_date + ,rel_order.order_code related_order_code,rel_project.project_name related_project_name from project_order_info t1 + left join project_order_info rel_order on t1.related_order_id = rel_order.id + left join project_info rel_project on rel_order.project_id = rel_project.id left join project_info t2 on t1.project_id = t2.id left join partner_info t3 on t1.partner_code=t3.partner_code @@ -85,6 +91,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" left join oms_finance_charge t6 on t1.order_code=t6.order_code + + +