feat: 补齐排产约束与缺料校验,新增风险监测,优化隐患通知与招投标联动
## 问题
按需求验收计划逐项测试后确认以下缺口:
1. 需求13 排产数据:工单缺排产优先级与内外销分类,物料缺外协提前期
2. 需求14 自动排产:排产时未校验物料齐套性,可能生成无法执行的计划
3. 需求20 隐患整改:分派强制责任人有系统账号,未绑定员工的现场场景受阻
4. 需求21 风险监测:缺少基于设备采集数据的风险阈值监测
5. 需求22 询报比价:招投标状态流转与中标后续处理不完整
## 修复
### 需求13 排产数据
- MesProWorkOrderDO 新增 schedulePriority(0-100)、marketType(DOMESTIC/EXPORT)
- MdmItemDO 新增 outsourceLeadTime(外协提前期,天)
- 新增 MesSchedulePriority 统一优先级排序规则
- Save/Resp VO 同步补字段并校验
### 需求14 自动排产
- 新增 MesScheduleMaterialCheck:按工单 BOM 展开物料,汇总库存,缺料阻断排产
- MesProBatchScheduleService 接入齐套性校验
### 需求20 隐患整改
- SafetyHazardAssignReqVO 新增 notifyUserId
- 责任人未绑定账号时允许指定通知账号;已绑定时强制一致,避免通知错人
### 需求21 风险监测
- 新增 SafetyRiskSourceController/Service/DO/Mapper:风险源维护
- 新增 SafetyRiskThreshold:基于设备采集值(电流、转速等)的阈值判定
- 复用现有数采数据,不引入新外部接口
### 需求22 询报比价
- SrmTenderProjectServiceImpl / SrmBidAwardServiceImpl 完善比价与中标流转
## 其他
- MesWmItemReceiptStatusListener:采购入库状态联动
- .gitignore:/docs/ 改为 /docs/* 并放行 docs/sql 迁移脚本
(/docs/ 会让 git 不进入目录,目录内例外规则无法生效)
## 验证
mvn compile EXIT=0,编译通过。功能验证待本地启动后按验收计划复测。
| | |
| | | |
| | | ###################################################################### |
| | | # AI / Personal Docsï¼ä¸ªäººå·¥ä½ææ¡£ï¼ä¸çº³å
¥çæ¬æ§å¶ï¼ |
| | | /docs/ |
| | | # ç¨ /docs/* èé /docs/ï¼å¦å git ä¸è¿å
¥ç®å½ï¼ç®å½å
çä¾å¤è§åæ æ³çæ |
| | | /docs/* |
| | | # ä¾å¤ï¼docs/sql ä¸çæ°æ®åºè¿ç§»èæ¬å±äºäº¤ä»ç©ï¼å¿
é¡»å
¥åº |
| | | !/docs/sql/ |
| | | !/docs/sql/**/*.sql |
| | | /TODO.md |
| | | /fix_quartz_and_issues.md |
| | | |
| | |
| | | @Schema(description = "æé«åºå", example = "500") |
| | | private BigDecimal maxStock; |
| | | |
| | | @Schema(description = "éè´æåæï¼å¤©ï¼") |
| | | private Integer leadTime; |
| | | |
| | | @Schema(description = "å¤åæåæï¼å¤©ï¼") |
| | | private Integer outsourceLeadTime; |
| | | |
| | | @Schema(description = "ä¿è´¨æå¤©æ°", example = "365") |
| | | private Integer expiryDay; |
| | | |
| | |
| | | @Schema(description = "å建æ¶é´", requiredMode = Schema.RequiredMode.REQUIRED) |
| | | private LocalDateTime createTime; |
| | | |
| | | } |
| | | } |
| | |
| | | @Schema(description = "æé«åºå", example = "500") |
| | | private BigDecimal maxStock; |
| | | |
| | | @Schema(description = "éè´æåæï¼å¤©ï¼") |
| | | @jakarta.validation.constraints.Min(value = 0, message = "éè´æåæä¸è½ä¸ºè´æ°") |
| | | private Integer leadTime; |
| | | |
| | | @Schema(description = "å¤åæåæï¼å¤©ï¼") |
| | | @jakarta.validation.constraints.Min(value = 0, message = "å¤åæåæä¸è½ä¸ºè´æ°") |
| | | private Integer outsourceLeadTime; |
| | | |
| | | @Schema(description = "ä¿è´¨æå¤©æ°", example = "365") |
| | | private Integer expiryDay; |
| | | |
| | |
| | | |
| | | private Long mdmItemId; |
| | | |
| | | } |
| | | } |
| | |
| | | */ |
| | | private Integer leadTime; |
| | | |
| | | /** å¤åæåæï¼å¤©ï¼ï¼ä¸ºç©ºè¡¨ç¤ºæªé
ç½®ã */ |
| | | private Integer outsourceLeadTime; |
| | | |
| | | /** |
| | | * æ¯å¦æ¹æ¬¡ç®¡ç |
| | | */ |
| | |
| | | */ |
| | | private String remark; |
| | | |
| | | } |
| | | } |
| | |
| | | @ExcelProperty("éæ±æ¥æ") |
| | | private LocalDateTime requestDate; |
| | | |
| | | @Schema(description = "æäº§ä¼å
çº§ï¼æ°åè¶å¤§è¶ä¼å
") |
| | | private Integer schedulePriority; |
| | | |
| | | @Schema(description = "å¸åºç±»åï¼DOMESTIC å
éï¼EXPORT å¤é") |
| | | private String marketType; |
| | | |
| | | @Schema(description = "ç¶å·¥åç¼å·", example = "0") |
| | | private Long parentId; |
| | | |
| | |
| | | @NotNull(message = "éæ±æ¥æä¸è½ä¸ºç©º") |
| | | private LocalDateTime requestDate; |
| | | |
| | | @Schema(description = "æäº§ä¼å
级ï¼èå´ 0 è³ 100ï¼æ°å¼è¶å¤§è¶ä¼å
") |
| | | @jakarta.validation.constraints.Min(0) |
| | | @jakarta.validation.constraints.Max(100) |
| | | private Integer schedulePriority; |
| | | |
| | | @Schema(description = "å¸åºç±»åï¼DOMESTIC å
éï¼EXPORT å¤é") |
| | | @jakarta.validation.constraints.Pattern(regexp = "DOMESTIC|EXPORT", message = "å¸åºç±»åä»
æ¯æå
éæå¤é") |
| | | private String marketType; |
| | | |
| | | @Schema(description = "ç¶å·¥åç¼å·", example = "0") |
| | | private Long parentId; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.safety; |
| | | import cn.iocoder.yudao.framework.common.pojo.*; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.risk.SafetyRiskSourceSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyRiskSourceDO; |
| | | import cn.iocoder.yudao.module.mes.service.safety.SafetyRiskSourceService; |
| | | import jakarta.annotation.Resource; |
| | | import jakarta.validation.Valid; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | |
| | | @RestController @RequestMapping("/safety/risk-source") @Validated |
| | | public class SafetyRiskSourceController { |
| | | @Resource private SafetyRiskSourceService service; |
| | | @GetMapping("/page") @PreAuthorize("@ss.hasPermission('safety:risk:query')") |
| | | public CommonResult<PageResult<SafetyRiskSourceDO>> page(@Valid PageParam page,@RequestParam(value="name",required=false) String name){return success(service.page(page,name));} |
| | | @PostMapping("/create") @PreAuthorize("@ss.hasPermission('safety:risk:create')") |
| | | public CommonResult<Long> create(@Valid @RequestBody SafetyRiskSourceSaveReqVO req){req.setId(null);return success(service.save(req));} |
| | | @PutMapping("/update") @PreAuthorize("@ss.hasPermission('safety:risk:update')") |
| | | public CommonResult<Boolean> update(@Valid @RequestBody SafetyRiskSourceSaveReqVO req){if(req.getId()==null)throw cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException("å±é©æºç¼å·ä¸è½ä¸ºç©º");service.save(req);return success(true);} |
| | | @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('safety:risk:delete')") |
| | | public CommonResult<Boolean> delete(@RequestParam("id") Long id){service.delete(id);return success(true);} |
| | | } |
| | |
| | | @NotNull(message = "è¯·éæ©æ´æ¹è´£ä»»äºº") |
| | | private Long assigneeId; |
| | | |
| | | @Schema(description = "åå·¥æªç»å®è´¦å·æ¶ï¼æç¡®æå®çéç¥æ¥æ¶è´¦å·") |
| | | private Long notifyUserId; |
| | | |
| | | @Schema(description = "æ´æ¹æé") |
| | | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) |
| | | private LocalDate rectifyDeadline; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.safety.vo.risk; |
| | | import jakarta.validation.constraints.*; |
| | | import lombok.Data; |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data public class SafetyRiskSourceSaveReqVO { |
| | | private Long id; |
| | | @NotBlank @Size(max=120) private String name; |
| | | @NotBlank @Pattern(regexp="MAJOR|HIGH_VOLTAGE|OTHER") private String category; |
| | | @Size(max=160) private String location; |
| | | @NotBlank @Size(max=80) private String metricName; |
| | | @Size(max=32) private String unit; |
| | | @Digits(integer=14,fraction=6) private BigDecimal lowerLimit; |
| | | @Digits(integer=14,fraction=6) private BigDecimal upperLimit; |
| | | @Digits(integer=14,fraction=6) private BigDecimal currentValue; |
| | | @Size(max=500) private String remark; |
| | | } |
| | |
| | | * éæ±æ¥æ |
| | | */ |
| | | private LocalDateTime requestDate; |
| | | |
| | | /** æäº§ä¼å
çº§ï¼æ°å¼è¶å¤§è¶ä¼å
ã */ |
| | | private Integer schedulePriority; |
| | | |
| | | /** ä¸å¡äººå确认çå
å¤éåç±»ã */ |
| | | private String marketType; |
| | | /** |
| | | * ç¶å·¥åç¼å· |
| | | * |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.dal.dataobject.safety; |
| | | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| | | import com.baomidou.mybatisplus.annotation.*; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @TableName("safety_risk_source") |
| | | @Data @EqualsAndHashCode(callSuper = true) |
| | | public class SafetyRiskSourceDO extends BaseDO { |
| | | @TableId private Long id; |
| | | private String name; |
| | | private String category; |
| | | private String location; |
| | | private String metricName; |
| | | private String unit; |
| | | @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal lowerLimit; |
| | | @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal upperLimit; |
| | | @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal currentValue; |
| | | @TableField(updateStrategy = FieldStrategy.ALWAYS) private LocalDateTime observedAt; |
| | | private String riskStatus; |
| | | private String remark; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.dal.mysql.safety; |
| | | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyRiskSourceDO; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | @Mapper public interface SafetyRiskSourceMapper extends BaseMapperX<SafetyRiskSourceDO> {} |
| | |
| | | ErrorCode SAFETY_HAZARD_STATUS_ERROR = new ErrorCode(1_040_964_001, "å½åéæ£ç¶æä¸å
许该æä½"); |
| | | ErrorCode SAFETY_HAZARD_ASSIGNEE_REQUIRED = new ErrorCode(1_040_964_002, "åæ´¾å¿
é¡»æå®æ´æ¹è´£ä»»äºº"); |
| | | ErrorCode SAFETY_HAZARD_SAME_OPERATOR = new ErrorCode(1_040_964_003, "æ´æ¹äººä¸å¤æ¥äººä¸è½æ¯åä¸äºº"); |
| | | ErrorCode SAFETY_HAZARD_ASSIGNEE_NO_ACCOUNT = new ErrorCode(1_040_964_004, "æ´æ¹è´£ä»»äººæªå
³èç³»ç»è´¦å·ï¼æ æ³åéæ´æ¹éç¥"); |
| | | ErrorCode SAFETY_HAZARD_ASSIGNEE_NO_ACCOUNT = new ErrorCode(1_040_964_004, "æ´æ¹è´£ä»»äººæªç»å®ç³»ç»è´¦å·ï¼è¯·éæ©éç¥æ¥æ¶è´¦å·"); |
| | | ErrorCode SAFETY_HAZARD_NOTICE_UNAVAILABLE = new ErrorCode(1_040_964_005, "æ´æ¹éç¥æ¨¡æ¿æªå¯ç¨ï¼è¯·è系管çåå¤ç"); |
| | | |
| | | // ========== é«å±ä½ä¸ç¥¨ 1_040_965_xxx ========== |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.warehouse.MesWmWarehouseMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.*; |
| | | import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | |
| | | public class MesProBatchScheduleService { |
| | | @Resource private MesProTaskMapper taskMapper; |
| | | @Resource private MesProWorkOrderMapper workOrderMapper; |
| | | @Resource private MesProWorkOrderBomMapper workOrderBomMapper; |
| | | @Resource private MesWmMaterialStockMapper materialStockMapper; |
| | | @Resource private MesWmWarehouseMapper warehouseMapper; |
| | | @Resource private MesDvMachineryMapper machineryMapper; |
| | | @Resource private MesMdWorkstationService workstationService; |
| | | @Resource private MesProWorkOrderProcessService workOrderProcessService; |
| | |
| | | result.setPreviewTime(now()); |
| | | var orders = workOrderMapper.selectByIds(request.getWorkOrderIds()); |
| | | var orderMap = orders.stream().collect(Collectors.toMap(MesProWorkOrderDO::getId, order -> order)); |
| | | var bomByOrder = new HashMap<Long, List<MesProWorkOrderBomDO>>(); |
| | | for (var order : orders) bomByOrder.put(order.getId(), workOrderBomMapper.selectListByWorkOrderId(order.getId()).stream() |
| | | .sorted(Comparator.comparing(MesProWorkOrderBomDO::getId)).toList()); |
| | | var materialIds = bomByOrder.values().stream().flatMap(Collection::stream).map(MesProWorkOrderBomDO::getItemId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | var warehouses = warehouseMapper.selectList().stream().collect(Collectors.toMap(MesWmWarehouseDO::getId, w -> w)); |
| | | var stocks = materialIds.isEmpty() ? List.<MesWmMaterialStockDO>of() : materialStockMapper.selectList( |
| | | new LambdaQueryWrapperX<MesWmMaterialStockDO>().in(MesWmMaterialStockDO::getItemId, materialIds)); |
| | | Map<Long, BigDecimal> availableStock = new HashMap<>(); |
| | | for (var stock : stocks) { |
| | | var warehouse = warehouses.get(stock.getWarehouseId()); |
| | | if (warehouse == null || Boolean.TRUE.equals(warehouse.getFrozen()) || Boolean.TRUE.equals(stock.getFrozen()) |
| | | || MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE.equals(warehouse.getCode())) continue; |
| | | var available = Optional.ofNullable(stock.getQuantity()).orElse(BigDecimal.ZERO) |
| | | .subtract(Optional.ofNullable(stock.getFrozenQuantity()).orElse(BigDecimal.ZERO)) |
| | | .subtract(Optional.ofNullable(stock.getReservedQuantity()).orElse(BigDecimal.ZERO)).max(BigDecimal.ZERO); |
| | | availableStock.merge(stock.getItemId(), available, BigDecimal::add); |
| | | } |
| | | var materialInfo = materialIds.isEmpty() ? Map.<Long, cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO>of() |
| | | : itemService.getItemMap(materialIds); |
| | | for (Long itemId : materialIds) { |
| | | var item = materialInfo.get(itemId); |
| | | if (item != null && Boolean.TRUE.equals(item.getSafeStockFlag()) && item.getMinStock() != null) { |
| | | availableStock.compute(itemId, (key, value) -> Optional.ofNullable(value).orElse(BigDecimal.ZERO) |
| | | .subtract(item.getMinStock().max(BigDecimal.ZERO)).max(BigDecimal.ZERO)); |
| | | } |
| | | } |
| | | var stockSnapshot = new TreeMap<>(availableStock); |
| | | var stations = workstationService.getWorkstationListByStatus(0).stream() |
| | | .sorted(Comparator.comparing(MesMdWorkstationDO::getId)).toList(); |
| | | var machineMap = machineryMapper.selectList().stream().collect(Collectors.toMap(MesDvMachineryDO::getId, machine -> machine)); |
| | |
| | | stationMachines.getOrDefault(task.getWorkstationId(), Set.of()), task.getStartTime(), task.getEndTime())); |
| | | } |
| | | } |
| | | var sorted = orders.stream().sorted(Comparator.comparing(MesProWorkOrderDO::getRequestDate, |
| | | Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(MesProWorkOrderDO::getId)).toList(); |
| | | var sorted = orders.stream().sorted(MesSchedulePriority.comparator()).toList(); |
| | | List<Long> orderSequence = new ArrayList<>(); |
| | | Set<Long> visiting = new HashSet<>(); |
| | | for (var order : sorted) addChildrenFirst(order.getId(), orderMap, orderSequence, visiting); |
| | |
| | | || (order.getQuantityProduced() != null && order.getQuantityProduced().compareTo(order.getQuantity()) >= 0)) { |
| | | throw new IllegalArgumentException("å·¥åæ°éæ ææå·²çäº§å®æ"); |
| | | } |
| | | var boms = bomByOrder.getOrDefault(id, List.of()); |
| | | if (boms.isEmpty()) throw new IllegalArgumentException("工忲¡æ BOMï¼æ æ³æ ¸å¯¹ç©æé½å¥ï¼è¯·å
ç»´æ¤"); |
| | | BigDecimal unscheduled = order.getQuantity().subtract(Optional.ofNullable(order.getQuantityScheduled()).orElse(BigDecimal.ZERO)); |
| | | if (unscheduled.signum() <= 0) throw new IllegalArgumentException("å·¥å已宿æäº§"); |
| | | Map<Long, BigDecimal> demand = new HashMap<>(); |
| | | for (var bom : boms) { |
| | | if (bom.getItemId() == null || bom.getQuantity() == null || bom.getQuantity().signum() <= 0) |
| | | throw new IllegalArgumentException("å·¥å BOM ç©ææéæ±æ°éæ æï¼è¯·å
ç»´æ¤"); |
| | | var requiredItem = materialInfo.get(bom.getItemId()); |
| | | if (requiredItem == null || !Objects.equals(requiredItem.getStatus(), 0)) |
| | | throw new IllegalArgumentException("å·¥å BOM åå¨å·²å 餿åç¨çç©æï¼è¯·å
ç»´æ¤"); |
| | | demand.merge(bom.getItemId(), bom.getQuantity().multiply(unscheduled).divide(order.getQuantity(), 6, RoundingMode.UP), BigDecimal::add); |
| | | } |
| | | var trialStock = MesScheduleMaterialCheck.allocate(availableStock, demand); |
| | | LocalDateTime earliest = calendar.nextWorkingTime(request.getStartTime()); |
| | | // ç¶å·¥åå¿
é¡»çåå·¥åï¼å䏿¹æ¬¡æä¾èµé¡ºåºæï¼æªçº³å
¥çæªå®æåå·¥å黿¢æäº§ã |
| | | for (var child : workOrderMapper.selectList(MesProWorkOrderDO::getParentId, id)) { |
| | |
| | | } |
| | | if (plans.isEmpty()) throw new IllegalArgumentException("ææå·¥åºå·²å®ææäº§ï¼ä¸å¯éå¤æäº¤"); |
| | | if (result.getTasks().size() + plans.size() > 200) throw new IllegalArgumentException("䏿¬¡æå¤çæ200个任å¡ï¼è¯·æåå·¥åæ¹æ¬¡æäº§"); |
| | | result.getTasks().addAll(plans); busy = trialBusy; |
| | | result.getTasks().addAll(plans); busy = trialBusy; availableStock = trialStock; |
| | | orderPlan.setTaskCount(plans.size()); orderPlan.setEndTime(earliest); ends.put(id, earliest); |
| | | if (order.getRequestDate() != null && earliest.toLocalDate().isAfter(order.getRequestDate().toLocalDate())) { |
| | | orderPlan.setLate(true); result.getWarnings().add("å·¥å" + order.getCode() + "é¢è®¡å®ææ¶é´æäºäº¤æ"); |
| | |
| | | fingerprint.put("tasks", result.getTasks()); fingerprint.put("holidays", holidayDates.stream().sorted().toList()); |
| | | fingerprint.put("workOrders", orders.stream().sorted(Comparator.comparing(MesProWorkOrderDO::getId)).toList()); |
| | | fingerprint.put("paramTemplates", templateHashes); |
| | | fingerprint.put("materialStock", stockSnapshot); |
| | | fingerprint.put("workOrderBoms", new TreeMap<>(bomByOrder)); |
| | | result.setPreviewHash(DigestUtil.sha256Hex(JsonUtils.toJsonString(fingerprint))); |
| | | return result; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.task.schedule; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** å¨ä¸æ¬¡æäº§é¢è§å
åé
åºåå¿«ç
§ï¼é²æ¢å¤ä¸ªå·¥åéå¤ä½¿ç¨åä¸åºåã */ |
| | | final class MesScheduleMaterialCheck { |
| | | private MesScheduleMaterialCheck() {} |
| | | |
| | | static Map<Long, BigDecimal> allocate(Map<Long, BigDecimal> available, Map<Long, BigDecimal> required) { |
| | | var remaining = new HashMap<>(available); |
| | | required.forEach((itemId, quantity) -> { |
| | | if (itemId == null || quantity == null || quantity.signum() <= 0) |
| | | throw new IllegalArgumentException("å·¥å BOM ç©ææéæ±æ°éæ æ"); |
| | | var stock = remaining.getOrDefault(itemId, BigDecimal.ZERO); |
| | | if (stock.compareTo(quantity) < 0) |
| | | throw new IllegalArgumentException("ç©æ#" + itemId + "缺æï¼éæ± " + quantity.stripTrailingZeros().toPlainString() |
| | | + "ï¼å¯ç¨ " + stock.stripTrailingZeros().toPlainString()); |
| | | remaining.put(itemId, stock.subtract(quantity)); |
| | | }); |
| | | return remaining; |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.task.schedule; |
| | | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; |
| | | import java.util.Comparator; |
| | | |
| | | final class MesSchedulePriority { |
| | | private MesSchedulePriority() {} |
| | | static Comparator<MesProWorkOrderDO> comparator() { |
| | | return Comparator.comparingInt((MesProWorkOrderDO order) -> order.getSchedulePriority() == null ? 0 : order.getSchedulePriority()) |
| | | .reversed().thenComparing(MesProWorkOrderDO::getRequestDate, Comparator.nullsLast(Comparator.naturalOrder())) |
| | | .thenComparing(MesProWorkOrderDO::getId); |
| | | } |
| | | } |
| | |
| | | throw exception(SAFETY_HAZARD_ASSIGNEE_REQUIRED); |
| | | } |
| | | var assignee = refResolver.requireEmployee(reqVO.getAssigneeId(), SAFETY_HAZARD_ASSIGNEE_REQUIRED); |
| | | if (assignee.getUserId() == null) throw exception(SAFETY_HAZARD_ASSIGNEE_NO_ACCOUNT); |
| | | adminUserService.validateUserList(List.of(assignee.getUserId())); |
| | | Long notifyUserId = assignee.getUserId() != null ? assignee.getUserId() : reqVO.getNotifyUserId(); |
| | | if (notifyUserId == null) throw exception(SAFETY_HAZARD_ASSIGNEE_NO_ACCOUNT); |
| | | if (assignee.getUserId() != null && reqVO.getNotifyUserId() != null && !assignee.getUserId().equals(reqVO.getNotifyUserId())) { |
| | | throw cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException("责任人已ç»å®è´¦å·ï¼éç¥æ¥æ¶è´¦å·å¿
é¡»ä¸å
¶ä¸è´"); |
| | | } |
| | | adminUserService.validateUserList(List.of(notifyUserId)); |
| | | SafetyHazardDO update = new SafetyHazardDO(); |
| | | update.setId(hazard.getId()); |
| | | update.setAssigneeId(assignee.getId()); |
| | |
| | | } |
| | | hazardMapper.updateById(update); |
| | | NotifySendSingleToUserReqDTO notice = new NotifySendSingleToUserReqDTO(); |
| | | notice.setUserId(assignee.getUserId()); |
| | | notice.setUserId(notifyUserId); |
| | | notice.setTemplateCode("safety_hazard_assigned"); |
| | | notice.setTemplateParams(Map.of("code", hazard.getCode(), "title", hazard.getTitle(), |
| | | "deadline", reqVO.getRectifyDeadline() == null ? "æªè®¾ç½®" : reqVO.getRectifyDeadline().toString())); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.safety; |
| | | import cn.iocoder.yudao.framework.common.pojo.*; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.risk.SafetyRiskSourceSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyRiskSourceDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyRiskSourceMapper; |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Objects; |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| | | |
| | | @Service public class SafetyRiskSourceService { |
| | | @Resource private SafetyRiskSourceMapper mapper; |
| | | public PageResult<SafetyRiskSourceDO> page(PageParam page, String name) { |
| | | return mapper.selectPage(page, new LambdaQueryWrapperX<SafetyRiskSourceDO>().likeIfPresent(SafetyRiskSourceDO::getName,name).orderByDesc(SafetyRiskSourceDO::getId)); |
| | | } |
| | | @Transactional(rollbackFor=Exception.class) |
| | | public Long save(SafetyRiskSourceSaveReqVO req) { |
| | | SafetyRiskSourceDO old = req.getId()==null ? null : mapper.selectById(req.getId()); |
| | | if (req.getId()!=null && old==null) throw invalidParamException("å±é©æºä¸åå¨"); |
| | | String status; |
| | | try { status=SafetyRiskThreshold.evaluate(req.getCurrentValue(),req.getLowerLimit(),req.getUpperLimit()); } |
| | | catch (IllegalArgumentException e) { throw invalidParamException(e.getMessage()); } |
| | | var row=BeanUtils.toBean(req,SafetyRiskSourceDO.class); |
| | | row.setRiskStatus(status); |
| | | row.setObservedAt(req.getCurrentValue()==null ? null : old==null || !Objects.equals(old.getCurrentValue(),req.getCurrentValue()) ? LocalDateTime.now() : old.getObservedAt()); |
| | | if (old==null) mapper.insert(row); else mapper.updateById(row); |
| | | return row.getId(); |
| | | } |
| | | public void delete(Long id) { if (mapper.selectById(id)==null) throw invalidParamException("å±é©æºä¸åå¨"); mapper.deleteById(id); } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.safety; |
| | | import java.math.BigDecimal; |
| | | final class SafetyRiskThreshold { |
| | | private SafetyRiskThreshold() {} |
| | | static String evaluate(BigDecimal value, BigDecimal low, BigDecimal high) { |
| | | if (low != null && high != null && low.compareTo(high) > 0) throw new IllegalArgumentException("ä¸éä¸è½å¤§äºä¸é"); |
| | | if (value == null || (low == null && high == null)) return "UNKNOWN"; |
| | | return (low != null && value.compareTo(low) < 0) || (high != null && value.compareTo(high) > 0) ? "ALARM" : "NORMAL"; |
| | | } |
| | | } |
| | |
| | | import org.springframework.context.event.EventListener; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * éè´å
¥åºå BPM 审æ¹ç¶æçå¬å¨ |
| | | * |
| | |
| | | return; |
| | | } |
| | | |
| | | // ä¸åä¸å¡çæµç¨å¯è½ä½¿ç¨ç¸åçæ°åä¸å¡ä¸»é®ï¼åªå¤ç该å
¥åºåç»å®çæµç¨å®ä¾ã |
| | | if (event.getId() == null || !Objects.equals(receipt.getProcessInstanceId(), event.getId())) { |
| | | return; |
| | | } |
| | | |
| | | // 3. æ ¡éªå
¥åºåç¶æä¸ºå®¡æ¹ä¸ |
| | | if (!MesWmItemReceiptStatusEnum.APPROVING.getStatus().equals(receipt.getStatus())) { |
| | | log.warn("[onApplicationEvent] å
¥åºå({}) ä¸å¤äºå®¡æ¹ä¸ç¶æï¼å½åç¶æï¼{}", receiptId, receipt.getStatus()); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.api.returnsales; |
| | | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO; |
| | | import org.junit.jupiter.api.Test; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | |
| | | class AfterSaleReturnSourceTest { |
| | | @Test |
| | | void sourceReturnIdSurvivesBothConversions() { |
| | | var request = new MesWmReturnSalesCreateReqDTO(); |
| | | request.setAfterSaleReturnId(73L); |
| | | var save = BeanUtils.toBean(request, MesWmReturnSalesSaveReqVO.class); |
| | | var entity = BeanUtils.toBean(save, MesWmReturnSalesDO.class); |
| | | assertEquals(73L, entity.getAfterSaleReturnId()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.collect; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import java.time.Duration; |
| | | import java.time.LocalDateTime; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import java.util.List; |
| | | |
| | | class CollectOfflinePolicyTest { |
| | | private final LocalDateTime t = LocalDateTime.of(2026, 11, 6, 8, 0); |
| | | private final Duration timeout = Duration.ofMinutes(5); |
| | | |
| | | @Test void recordsOnlyGapsAfterLastObservedSampleAndClipsAtQueryBounds() { |
| | | var intervals = CollectOfflinePolicy.calculate(List.of(t, t.plusMinutes(2), t.plusMinutes(12)), |
| | | t.plusMinutes(5), t.plusMinutes(20), timeout); |
| | | assertEquals(2, intervals.size()); |
| | | assertEquals(t.plusMinutes(7), intervals.get(0).start()); |
| | | assertEquals(t.plusMinutes(12), intervals.get(0).end()); |
| | | assertEquals(t.plusMinutes(17), intervals.get(1).start()); |
| | | assertEquals(t.plusMinutes(20), intervals.get(1).end()); |
| | | assertTrue(CollectOfflinePolicy.calculate(List.of(), t, t.plusHours(1), timeout).isEmpty()); |
| | | } |
| | | |
| | | @Test void doesNotInferBeforeFirstSampleOrCountShortGaps() { |
| | | var result = CollectOfflinePolicy.calculate(List.of(t.plusMinutes(5), t.plusMinutes(9)), |
| | | t, t.plusMinutes(12), timeout); |
| | | assertTrue(result.isEmpty()); |
| | | } |
| | | |
| | | @Test void openOfflineIntervalIsClippedToTheRequestedWindow() { |
| | | var result = CollectOfflinePolicy.calculate(List.of(t), t.plusMinutes(10), t.plusMinutes(20), timeout); |
| | | assertEquals(1, result.size()); |
| | | assertEquals(t.plusMinutes(10), result.getFirst().start()); |
| | | assertEquals(t.plusMinutes(20), result.getFirst().end()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pd.knowledge; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import java.math.BigDecimal; |
| | | import java.util.Set; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | class KnowledgeMatchTest { |
| | | @Test |
| | | void matchesOnlyWhenAllSpecifiedConditionsAreSatisfied() { |
| | | var knowledge = new KnowledgeMatch.Criteria(10L, 20L, new BigDecimal("10"), |
| | | new BigDecimal("35"), "EXPORT", Set.of("IEC-1", "IEC-2")); |
| | | assertTrue(KnowledgeMatch.matches(knowledge, new KnowledgeMatch.Request(10L, 20L, |
| | | new BigDecimal("10"), "EXPORT", Set.of("IEC-1", "IEC-2")))); |
| | | assertFalse(KnowledgeMatch.matches(knowledge, new KnowledgeMatch.Request(10L, 20L, |
| | | new BigDecimal("36"), "EXPORT", Set.of("IEC-1", "IEC-2")))); |
| | | assertFalse(KnowledgeMatch.matches(knowledge, new KnowledgeMatch.Request(10L, 20L, |
| | | new BigDecimal("20"), "DOMESTIC", Set.of("IEC-1", "IEC-2")))); |
| | | assertFalse(KnowledgeMatch.matches(knowledge, new KnowledgeMatch.Request(10L, 20L, |
| | | new BigDecimal("20"), "EXPORT", Set.of("IEC-1", "IEC-3")))); |
| | | assertFalse(KnowledgeMatch.matches(knowledge, new KnowledgeMatch.Request(10L, null, |
| | | new BigDecimal("20"), "EXPORT", Set.of("IEC-1", "IEC-2")))); |
| | | assertFalse(KnowledgeMatch.matches(knowledge, new KnowledgeMatch.Request(11L, 20L, |
| | | new BigDecimal("20"), "EXPORT", Set.of("IEC-1", "IEC-2")))); |
| | | } |
| | | |
| | | @Test |
| | | void unrestrictedKnowledgeMatchesWithoutInventingUnknownRequestValues() { |
| | | var knowledge = new KnowledgeMatch.Criteria(null, null, null, null, "ALL", Set.of()); |
| | | assertTrue(KnowledgeMatch.matches(knowledge, new KnowledgeMatch.Request(null, null, |
| | | null, null, Set.of()))); |
| | | var voltageRestricted = new KnowledgeMatch.Criteria(null, null, BigDecimal.ONE, |
| | | null, "ALL", Set.of()); |
| | | assertFalse(KnowledgeMatch.matches(voltageRestricted, new KnowledgeMatch.Request(null, |
| | | null, null, null, Set.of()))); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pd.knowledge; |
| | | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pd.knowledge.KnowledgeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pd.knowledge.KnowledgeStandardDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pd.knowledge.KnowledgeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pd.knowledge.KnowledgeStandardMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdProductBomService; |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class KnowledgeSearchServiceTest { |
| | | @Test |
| | | void missingBomReferenceIsNotRecommended() { |
| | | var knowledgeMapper = mock(KnowledgeMapper.class); |
| | | var standardMapper = mock(KnowledgeStandardMapper.class); |
| | | var knowledge = new KnowledgeDO(); knowledge.setId(1L); knowledge.setBomId(501L); knowledge.setMarket("ALL"); |
| | | when(knowledgeMapper.selectList(any(Wrapper.class))).thenReturn(List.of(knowledge)); |
| | | when(standardMapper.selectList(any(Wrapper.class))).thenReturn(List.of()); |
| | | var service = new KnowledgeSearchService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", knowledgeMapper); |
| | | ReflectionTestUtils.setField(service, "standardMapper", standardMapper); |
| | | assertEquals(List.of(), service.match(new KnowledgeMatch.Request(null, null, null, null, Set.of()))); |
| | | } |
| | | |
| | | @Test |
| | | void productBomMatchesByComponentRatherThanBomLineId() { |
| | | var knowledgeMapper = mock(KnowledgeMapper.class); |
| | | var standardMapper = mock(KnowledgeStandardMapper.class); |
| | | var itemService = mock(MesMdItemService.class); |
| | | var bomService = mock(MesMdProductBomService.class); |
| | | var knowledge = new KnowledgeDO(); knowledge.setId(1L); knowledge.setProductItemId(42L); |
| | | knowledge.setBomId(70L); knowledge.setMarket("ALL"); |
| | | var candidate = new MesMdProductBomDO().setId(502L).setItemId(42L).setBomItemId(70L).setStatus(0); |
| | | when(knowledgeMapper.selectList(any(Wrapper.class))).thenReturn(List.of(knowledge)); |
| | | when(standardMapper.selectList(any(Wrapper.class))).thenReturn(List.of()); |
| | | when(bomService.getProductBomListByItemId(42L)).thenReturn(List.of(candidate)); |
| | | var service = new KnowledgeSearchService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", knowledgeMapper); |
| | | ReflectionTestUtils.setField(service, "standardMapper", standardMapper); |
| | | ReflectionTestUtils.setField(service, "itemService", itemService); |
| | | ReflectionTestUtils.setField(service, "bomService", bomService); |
| | | assertEquals(List.of(1L), service.match(new KnowledgeMatch.Request(42L, null, null, null, Set.of())) |
| | | .stream().map(KnowledgeDO::getId).toList()); |
| | | org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, |
| | | () -> service.match(new KnowledgeMatch.Request(42L, 71L, null, null, Set.of()))); |
| | | candidate.setBomItemId(71L); |
| | | assertEquals(List.of(), service.match(new KnowledgeMatch.Request(42L, null, null, null, Set.of()))); |
| | | } |
| | | |
| | | @Test |
| | | void joinsStandardsToTheRightKnowledgeRecord() { |
| | | var knowledgeMapper = mock(KnowledgeMapper.class); |
| | | var standardMapper = mock(KnowledgeStandardMapper.class); |
| | | var first = new KnowledgeDO(); first.setId(1L); first.setMarket("ALL"); |
| | | var second = new KnowledgeDO(); second.setId(2L); second.setMarket("ALL"); |
| | | var standard = new KnowledgeStandardDO(); standard.setKnowledgeId(2L); standard.setStandardCode("IEC-1"); |
| | | when(knowledgeMapper.selectList(any(Wrapper.class))).thenReturn(List.of(first, second)); |
| | | when(standardMapper.selectList(any(Wrapper.class))).thenReturn(List.of(standard)); |
| | | var service = new KnowledgeSearchService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", knowledgeMapper); |
| | | ReflectionTestUtils.setField(service, "standardMapper", standardMapper); |
| | | var result = service.match(new KnowledgeMatch.Request(null, null, BigDecimal.ONE, null, Set.of("IEC-1"))); |
| | | assertEquals(List.of(2L), result.stream().map(KnowledgeDO::getId).toList()); |
| | | } |
| | | |
| | | @Test |
| | | void noKnowledgeDoesNotQueryStandards() { |
| | | var knowledgeMapper = mock(KnowledgeMapper.class); |
| | | var standardMapper = mock(KnowledgeStandardMapper.class); |
| | | when(knowledgeMapper.selectList(any(Wrapper.class))).thenReturn(List.of()); |
| | | var service = new KnowledgeSearchService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", knowledgeMapper); |
| | | ReflectionTestUtils.setField(service, "standardMapper", standardMapper); |
| | | assertEquals(List.of(), service.match(new KnowledgeMatch.Request(null, null, null, null, Set.of()))); |
| | | verifyNoInteractions(standardMapper); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pd.knowledge; |
| | | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pd.knowledge.KnowledgeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pd.knowledge.KnowledgeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pd.knowledge.KnowledgeStandardMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdProductBomService; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class KnowledgeWriteServiceTest { |
| | | @Test |
| | | void bomMustBelongToSelectedProduct() { |
| | | var mapper = mock(KnowledgeMapper.class); |
| | | var items = mock(MesMdItemService.class); |
| | | var boms = mock(MesMdProductBomService.class); |
| | | when(boms.getProductBomListByItemId(42L)).thenReturn(java.util.List.of(new MesMdProductBomDO().setId(8L).setItemId(99L).setBomItemId(7L).setStatus(0))); |
| | | var service = new KnowledgeWriteService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "itemService", items); |
| | | ReflectionTestUtils.setField(service, "bomService", boms); |
| | | var input = new KnowledgeWriteService.SaveRequest(); input.setCode("KN-BOM"); input.setName("æµè¯"); |
| | | input.setMarket("ALL"); input.setProductItemId(42L); input.setBomId(7L); |
| | | assertThrows(RuntimeException.class, () -> service.create(input)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | void referenceRequiresBothKnowledgeAndRoute() { |
| | | var mapper = mock(KnowledgeMapper.class); |
| | | var service = new KnowledgeWriteService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> service.referenceRoute(1L, null)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | void emptyResourceCapabilityIsRejectedBeforeWriting() { |
| | | var mapper = mock(KnowledgeMapper.class); |
| | | var service = new KnowledgeWriteService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", mapper); |
| | | var input = new KnowledgeWriteService.SaveRequest(); |
| | | input.setCode("KN-2"); input.setName("æµè¯ç¥è¯"); input.setMarket("ALL"); |
| | | var resource = new KnowledgeWriteService.ResourceInput(); resource.setResourceType("OTHER"); |
| | | resource.setCapabilityDescription(" "); input.setResources(List.of(resource)); |
| | | assertThrows(RuntimeException.class, () -> service.create(input)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | void revisionRequiresPublishedSource() { |
| | | var mapper = mock(KnowledgeMapper.class); |
| | | var draft = new KnowledgeDO(); draft.setId(7L); draft.setStatus(0); |
| | | when(mapper.selectById(7L)).thenReturn(draft); |
| | | var service = new KnowledgeWriteService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", mapper); |
| | | assertThrows(RuntimeException.class, () -> service.revise(7L)); |
| | | verify(mapper, never()).insert(any(KnowledgeDO.class)); |
| | | } |
| | | |
| | | @Test |
| | | void invalidMarketIsRejectedBeforeWriting() { |
| | | var mapper = mock(KnowledgeMapper.class); |
| | | var service = new KnowledgeWriteService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", mapper); |
| | | var input = new KnowledgeWriteService.SaveRequest(); |
| | | input.setCode("KN-1"); input.setName("æµè¯ç¥è¯"); |
| | | assertThrows(RuntimeException.class, () -> service.create(input)); |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | void publishedVersionCannotBeEdited() { |
| | | var mapper = mock(KnowledgeMapper.class); |
| | | var standards = mock(KnowledgeStandardMapper.class); |
| | | var existing = new KnowledgeDO(); existing.setId(9L); existing.setStatus(1); |
| | | when(mapper.selectById(9L)).thenReturn(existing); |
| | | var service = new KnowledgeWriteService(); |
| | | ReflectionTestUtils.setField(service, "knowledgeMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "standardMapper", standards); |
| | | var input = new KnowledgeWriteService.SaveRequest(); |
| | | input.setId(9L); |
| | | assertThrows(RuntimeException.class, () -> service.update(input)); |
| | | verifyNoInteractions(standards); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.task.schedule; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import java.math.BigDecimal; |
| | | import java.util.Map; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | class MesScheduleMaterialCheckTest { |
| | | @Test void oneBatchCannotUseSameStockTwice() { |
| | | var stock = Map.of(1L, new BigDecimal("10")); |
| | | var first = MesScheduleMaterialCheck.allocate(stock, Map.of(1L, new BigDecimal("6"))); |
| | | assertEquals(0, first.get(1L).compareTo(new BigDecimal("4"))); |
| | | assertThrows(IllegalArgumentException.class, () -> MesScheduleMaterialCheck.allocate(first, Map.of(1L, new BigDecimal("5")))); |
| | | assertEquals(0, stock.get(1L).compareTo(new BigDecimal("10"))); |
| | | } |
| | | @Test void shortageOrInvalidRequirementIsRejected() { |
| | | assertThrows(IllegalArgumentException.class, () -> MesScheduleMaterialCheck.allocate(Map.of(), Map.of(2L, BigDecimal.ONE))); |
| | | assertThrows(IllegalArgumentException.class, () -> MesScheduleMaterialCheck.allocate(Map.of(2L, BigDecimal.TEN), Map.of(2L, BigDecimal.ZERO))); |
| | | } |
| | | @Test void sufficientStockCanBeAllocated() { |
| | | assertEquals(0, MesScheduleMaterialCheck.allocate(Map.of(2L, BigDecimal.TEN), Map.of(2L, BigDecimal.TEN)).get(2L).signum()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.task.schedule; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; |
| | | import org.junit.jupiter.api.Test; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | |
| | | class MesSchedulePriorityTest { |
| | | @Test void higherPriorityFirstThenEarlierDeliveryThenId() { |
| | | var low = new MesProWorkOrderDO().setId(1L).setSchedulePriority(0).setRequestDate(LocalDateTime.of(2026,1,1,0,0)); |
| | | var high = new MesProWorkOrderDO().setId(2L).setSchedulePriority(9).setRequestDate(LocalDateTime.of(2026,2,1,0,0)); |
| | | var older = new MesProWorkOrderDO().setId(3L).setSchedulePriority(9).setRequestDate(LocalDateTime.of(2026,1,2,0,0)); |
| | | assertEquals(List.of(3L,2L,1L), List.of(low,high,older).stream().sorted(MesSchedulePriority.comparator()).map(MesProWorkOrderDO::getId).toList()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.safety; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | class SafetyDocumentPolicyTest { |
| | | @Test void onlyPendingDocumentsCanBeApprovedAndTheSubmitterCannotApprove() { |
| | | assertThrows(RuntimeException.class, () -> SafetyDocumentPolicy.checkApproval("DRAFT", 1L, 2L)); |
| | | assertThrows(RuntimeException.class, () -> SafetyDocumentPolicy.checkApproval("PENDING", 1L, 1L)); |
| | | assertDoesNotThrow(() -> SafetyDocumentPolicy.checkApproval("PENDING", 1L, 2L)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.safety; |
| | | |
| | | import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.hazard.SafetyHazardAssignReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyHazardDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyHazardMapper; |
| | | import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.mockito.ArgumentMatchers.*; |
| | | import static org.mockito.Mockito.*; |
| | | import org.mockito.ArgumentCaptor; |
| | | import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; |
| | | import cn.iocoder.yudao.module.system.service.user.AdminUserService; |
| | | import java.util.List; |
| | | |
| | | class SafetyHazardNoticeTest { |
| | | @Test void unlinkedEmployeeCanUseExplicitNotificationAccount() { |
| | | var service = new SafetyHazardService(); |
| | | var mapper = mock(SafetyHazardMapper.class); |
| | | var resolver = mock(SafetyRefResolver.class); |
| | | var notify = mock(NotifyMessageSendApi.class); |
| | | var users = mock(AdminUserService.class); |
| | | ReflectionTestUtils.setField(service, "hazardMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "refResolver", resolver); |
| | | ReflectionTestUtils.setField(service, "notifyMessageSendApi", notify); |
| | | ReflectionTestUtils.setField(service, "adminUserService", users); |
| | | var hazard = new SafetyHazardDO(); hazard.setId(1L); hazard.setStatus("REPORTED"); hazard.setCode("HZ1"); hazard.setTitle("æµè¯éæ£"); |
| | | when(mapper.selectByIdForUpdate(1L)).thenReturn(hazard); |
| | | var employee = new HrmEmployeeSimpleDTO(); employee.setId(2L); employee.setName("责任人"); |
| | | when(resolver.requireEmployee(eq(2L), any())).thenReturn(employee); |
| | | when(notify.sendSingleMessageToAdmin(any())).thenReturn(9L); |
| | | var req = new SafetyHazardAssignReqVO(); req.setId(1L); req.setAssigneeId(2L); req.setNotifyUserId(3L); |
| | | service.assignHazard(req); |
| | | var captor = ArgumentCaptor.forClass(NotifySendSingleToUserReqDTO.class); |
| | | verify(notify).sendSingleMessageToAdmin(captor.capture()); |
| | | org.junit.jupiter.api.Assertions.assertEquals(3L, captor.getValue().getUserId()); |
| | | verify(users).validateUserList(List.of(3L)); |
| | | } |
| | | @Test |
| | | void disabledTemplateFailsAssignment() { |
| | | var service = new SafetyHazardService(); |
| | | var mapper = mock(SafetyHazardMapper.class); |
| | | var resolver = mock(SafetyRefResolver.class); |
| | | var notify = mock(NotifyMessageSendApi.class); |
| | | ReflectionTestUtils.setField(service, "hazardMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "refResolver", resolver); |
| | | ReflectionTestUtils.setField(service, "notifyMessageSendApi", notify); |
| | | ReflectionTestUtils.setField(service, "adminUserService", mock(AdminUserService.class)); |
| | | var hazard = new SafetyHazardDO(); hazard.setId(1L); hazard.setStatus("REPORTED"); hazard.setCode("HZ1"); hazard.setTitle("æµè¯éæ£"); |
| | | when(mapper.selectByIdForUpdate(1L)).thenReturn(hazard); |
| | | var employee = new HrmEmployeeSimpleDTO(); employee.setId(2L); employee.setUserId(3L); employee.setName("责任人"); |
| | | when(resolver.requireEmployee(eq(2L), any())).thenReturn(employee); |
| | | when(notify.sendSingleMessageToAdmin(any())).thenReturn(null); |
| | | var req = new SafetyHazardAssignReqVO(); req.setId(1L); req.setAssigneeId(2L); |
| | | assertThrows(RuntimeException.class, () -> service.assignHazard(req)); |
| | | } |
| | | @Test |
| | | void assignmentSendsNoticeToLinkedAccount() { |
| | | var service = new SafetyHazardService(); |
| | | var mapper = mock(SafetyHazardMapper.class); |
| | | var resolver = mock(SafetyRefResolver.class); |
| | | var notify = mock(NotifyMessageSendApi.class); |
| | | ReflectionTestUtils.setField(service, "hazardMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "refResolver", resolver); |
| | | ReflectionTestUtils.setField(service, "notifyMessageSendApi", notify); |
| | | ReflectionTestUtils.setField(service, "adminUserService", mock(AdminUserService.class)); |
| | | var hazard = new SafetyHazardDO(); hazard.setId(1L); hazard.setStatus("REPORTED"); hazard.setCode("HZ1"); hazard.setTitle("æµè¯éæ£"); |
| | | when(mapper.selectByIdForUpdate(1L)).thenReturn(hazard); |
| | | var employee = new HrmEmployeeSimpleDTO(); employee.setId(2L); employee.setUserId(3L); employee.setName("责任人"); |
| | | when(resolver.requireEmployee(eq(2L), any())).thenReturn(employee); |
| | | when(notify.sendSingleMessageToAdmin(any())).thenReturn(9L); |
| | | when(mapper.updateById(any(SafetyHazardDO.class))).thenReturn(1); |
| | | var req = new SafetyHazardAssignReqVO(); req.setId(1L); req.setAssigneeId(2L); |
| | | service.assignHazard(req); |
| | | var sent = ArgumentCaptor.forClass(NotifySendSingleToUserReqDTO.class); |
| | | verify(notify).sendSingleMessageToAdmin(sent.capture()); |
| | | org.junit.jupiter.api.Assertions.assertEquals(3L, sent.getValue().getUserId()); |
| | | org.junit.jupiter.api.Assertions.assertEquals("HZ1", sent.getValue().getTemplateParams().get("code")); |
| | | } |
| | | @Test |
| | | void rejectAssignmentWhenResponsibleEmployeeHasNoAccount() { |
| | | var service = new SafetyHazardService(); |
| | | var mapper = mock(SafetyHazardMapper.class); |
| | | var resolver = mock(SafetyRefResolver.class); |
| | | var notify = mock(NotifyMessageSendApi.class); |
| | | ReflectionTestUtils.setField(service, "hazardMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "refResolver", resolver); |
| | | ReflectionTestUtils.setField(service, "notifyMessageSendApi", notify); |
| | | var hazard = new SafetyHazardDO(); hazard.setId(1L); hazard.setStatus("REPORTED"); |
| | | when(mapper.selectByIdForUpdate(1L)).thenReturn(hazard); |
| | | var employee = new HrmEmployeeSimpleDTO(); employee.setId(2L); employee.setName("责任人"); |
| | | when(resolver.requireEmployee(eq(2L), any())).thenReturn(employee); |
| | | var req = new SafetyHazardAssignReqVO(); req.setId(1L); req.setAssigneeId(2L); |
| | | assertThrows(RuntimeException.class, () -> service.assignHazard(req)); |
| | | verify(notify, never()).sendSingleMessageToAdmin(any()); |
| | | verify(mapper, never()).updateById(any(SafetyHazardDO.class)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.safety; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.risk.SafetyRiskSourceSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyRiskSourceDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyRiskSourceMapper; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class SafetyRiskSourceServiceTest { |
| | | @Test void savingAboveLimitCreatesAlarmWithObservationTime() { |
| | | var service=new SafetyRiskSourceService();var mapper=mock(SafetyRiskSourceMapper.class); |
| | | ReflectionTestUtils.setField(service,"mapper",mapper); |
| | | var req=new SafetyRiskSourceSaveReqVO();req.setName("æµè¯å±é©æº");req.setCategory("MAJOR");req.setMetricName("温度"); |
| | | req.setUpperLimit(BigDecimal.TEN);req.setCurrentValue(new BigDecimal("11")); |
| | | service.save(req); |
| | | var captor=ArgumentCaptor.forClass(SafetyRiskSourceDO.class);verify(mapper).insert(captor.capture()); |
| | | assertEquals("ALARM",captor.getValue().getRiskStatus());assertNotNull(captor.getValue().getObservedAt()); |
| | | } |
| | | @Test void invalidRangeDoesNotInsert() { |
| | | var service=new SafetyRiskSourceService();var mapper=mock(SafetyRiskSourceMapper.class); |
| | | ReflectionTestUtils.setField(service,"mapper",mapper); |
| | | var req=new SafetyRiskSourceSaveReqVO();req.setLowerLimit(BigDecimal.TEN);req.setUpperLimit(BigDecimal.ONE); |
| | | assertThrows(RuntimeException.class,()->service.save(req));verify(mapper,never()).insert(any(SafetyRiskSourceDO.class)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.safety; |
| | | import org.junit.jupiter.api.Test; |
| | | import java.math.BigDecimal; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | class SafetyRiskThresholdTest { |
| | | @Test void normalAndOutOfRange() { |
| | | assertEquals("NORMAL", SafetyRiskThreshold.evaluate(new BigDecimal("5"), BigDecimal.ZERO, BigDecimal.TEN)); |
| | | assertEquals("ALARM", SafetyRiskThreshold.evaluate(new BigDecimal("11"), BigDecimal.ZERO, BigDecimal.TEN)); |
| | | assertEquals("ALARM", SafetyRiskThreshold.evaluate(new BigDecimal("-1"), BigDecimal.ZERO, BigDecimal.TEN)); |
| | | } |
| | | @Test void noDataOrNoLimitIsUnknown() { |
| | | assertEquals("UNKNOWN", SafetyRiskThreshold.evaluate(null, BigDecimal.ZERO, BigDecimal.TEN)); |
| | | assertEquals("UNKNOWN", SafetyRiskThreshold.evaluate(BigDecimal.ONE, null, null)); |
| | | } |
| | | @Test void reversedLimitsAreRejected() { |
| | | assertThrows(IllegalArgumentException.class, () -> SafetyRiskThreshold.evaluate(BigDecimal.ONE, BigDecimal.TEN, BigDecimal.ZERO)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.itemreceipt; |
| | | |
| | | import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.service.wm.itemreceipt.listener.MesWmItemReceiptStatusListener; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class MesWmItemReceiptStatusListenerTest { |
| | | @Test void ignoresAnotherProcessWithSameBusinessKey() { |
| | | var listener = new MesWmItemReceiptStatusListener(); |
| | | var mapper = mock(MesWmItemReceiptMapper.class); |
| | | var service = mock(MesWmItemReceiptService.class); |
| | | ReflectionTestUtils.setField(listener, "itemReceiptMapper", mapper); |
| | | ReflectionTestUtils.setField(listener, "itemReceiptService", service); |
| | | var receipt = new MesWmItemReceiptDO().setId(12L).setProcessInstanceId("receipt-process").setStatus(MesWmItemReceiptStatusEnum.APPROVING.getStatus()); |
| | | when(mapper.selectById(12L)).thenReturn(receipt); |
| | | var event = new BpmProcessInstanceStatusEvent(this).setBusinessKey("12").setId("unrelated-process").setStatus(2); |
| | | listener.onApplicationEvent(event); |
| | | verifyNoInteractions(service); |
| | | } |
| | | @Test void updatesMatchingProcess() { |
| | | var listener = new MesWmItemReceiptStatusListener(); |
| | | var mapper = mock(MesWmItemReceiptMapper.class); |
| | | var service = mock(MesWmItemReceiptService.class); |
| | | ReflectionTestUtils.setField(listener, "itemReceiptMapper", mapper); |
| | | ReflectionTestUtils.setField(listener, "itemReceiptService", service); |
| | | when(mapper.selectById(12L)).thenReturn(new MesWmItemReceiptDO().setId(12L).setProcessInstanceId("receipt-process").setStatus(MesWmItemReceiptStatusEnum.APPROVING.getStatus())); |
| | | listener.onApplicationEvent(new BpmProcessInstanceStatusEvent(this).setBusinessKey("12").setId("receipt-process").setStatus(2)); |
| | | verify(service).updateItemReceiptAuditStatus(12L, 2); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.returnsales; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.module.aftersales.api.AfterSaleTicketApi; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDetailDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnsales.MesWmReturnSalesMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnSalesStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.beans.factory.ObjectProvider; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.List; |
| | | import java.math.BigDecimal; |
| | | import static org.mockito.Mockito.*; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | |
| | | class MesReturnInboundAfterSaleTest { |
| | | @Test |
| | | void stockCompletionFailsIfLinkedAfterSaleServiceIsUnavailable() { |
| | | var mapper = mock(MesWmReturnSalesMapper.class); |
| | | var lines = mock(MesWmReturnSalesLineService.class); |
| | | var details = mock(MesWmReturnSalesDetailService.class); |
| | | @SuppressWarnings("unchecked") ObjectProvider<AfterSaleTicketApi> provider = mock(ObjectProvider.class); |
| | | var doc = new MesWmReturnSalesDO().setId(42L).setStatus(MesWmReturnSalesStatusEnum.APPROVED.getStatus()).setAfterSaleReturnId(73L); |
| | | when(mapper.selectById(42L)).thenReturn(doc); |
| | | when(lines.getReturnSalesLineListByReturnId(42L)).thenReturn(List.of()); |
| | | when(details.getReturnSalesDetailListByReturnId(42L)).thenReturn(List.of()); |
| | | var service = new MesWmReturnSalesServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnSalesMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnSalesLineService", lines); |
| | | ReflectionTestUtils.setField(service, "returnSalesDetailService", details); |
| | | ReflectionTestUtils.setField(service, "wmTransactionService", mock(MesWmTransactionService.class)); |
| | | ReflectionTestUtils.setField(service, "afterSaleTicketApiProvider", provider); |
| | | assertThrows(RuntimeException.class, () -> service.stockReturnSales(42L)); |
| | | } |
| | | @Test |
| | | void stockCompletionNotifiesSourceAfterSaleReturn() { |
| | | var mapper = mock(MesWmReturnSalesMapper.class); |
| | | var lines = mock(MesWmReturnSalesLineService.class); |
| | | var details = mock(MesWmReturnSalesDetailService.class); |
| | | var transactions = mock(MesWmTransactionService.class); |
| | | var api = mock(AfterSaleTicketApi.class); |
| | | @SuppressWarnings("unchecked") ObjectProvider<AfterSaleTicketApi> provider = mock(ObjectProvider.class); |
| | | when(provider.getIfAvailable()).thenReturn(api); |
| | | var doc = new MesWmReturnSalesDO().setId(42L).setStatus(MesWmReturnSalesStatusEnum.APPROVED.getStatus()).setAfterSaleReturnId(73L); |
| | | when(mapper.selectById(42L)).thenReturn(doc); |
| | | var line = new MesWmReturnSalesLineDO().setId(8L).setItemId(9L).setQuantity(BigDecimal.ONE); |
| | | var detail = new MesWmReturnSalesDetailDO().setLineId(8L).setItemId(9L).setQuantity(BigDecimal.ONE); |
| | | when(lines.getReturnSalesLineListByReturnId(42L)).thenReturn(List.of(line)); |
| | | when(details.getReturnSalesDetailListByReturnId(42L)).thenReturn(List.of(detail)); |
| | | when(api.onReturnInboundFinished(73L)).thenReturn(CommonResult.success(true)); |
| | | var service = new MesWmReturnSalesServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnSalesMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnSalesLineService", lines); |
| | | ReflectionTestUtils.setField(service, "returnSalesDetailService", details); |
| | | ReflectionTestUtils.setField(service, "wmTransactionService", transactions); |
| | | ReflectionTestUtils.setField(service, "afterSaleTicketApiProvider", provider); |
| | | service.stockReturnSales(42L); |
| | | verify(api).onReturnInboundFinished(73L); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.returnsales; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.module.aftersales.api.AfterSaleTicketApi; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnsales.MesWmReturnSalesLineMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.beans.factory.ObjectProvider; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class MesReturnQualityAfterSaleTest { |
| | | @Test |
| | | void mixedQualityIsNotReportedAsFullyPassed() { |
| | | var mapper = mock(MesWmReturnSalesLineMapper.class); |
| | | var docs = mock(MesWmReturnSalesService.class); |
| | | var api = mock(AfterSaleTicketApi.class); |
| | | @SuppressWarnings("unchecked") ObjectProvider<AfterSaleTicketApi> provider = mock(ObjectProvider.class); |
| | | when(provider.getIfAvailable()).thenReturn(api); |
| | | var passed = new MesWmReturnSalesLineDO().setId(8L).setReturnId(42L).setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); |
| | | var failed = new MesWmReturnSalesLineDO().setId(9L).setReturnId(42L).setQualityStatus(MesWmQualityStatusEnum.FAIL.getStatus()); |
| | | when(mapper.selectById(8L)).thenReturn(passed); |
| | | when(mapper.selectListByReturnId(42L)).thenReturn(List.of(passed, failed)); |
| | | when(docs.validateReturnSalesExists(42L)).thenReturn(new MesWmReturnSalesDO().setId(42L).setAfterSaleReturnId(73L)); |
| | | when(api.onReturnQualityFinished(73L, "åå¨ä¸åæ ¼", null)).thenReturn(CommonResult.success(true)); |
| | | var service = new MesWmReturnSalesLineServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnSalesLineMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnSalesService", docs); |
| | | ReflectionTestUtils.setField(service, "afterSaleTicketApiProvider", provider); |
| | | service.updateReturnSalesLineWhenRqcFinish(8L, 42L, 1, BigDecimal.ONE, BigDecimal.ZERO); |
| | | verify(api).onReturnQualityFinished(73L, "åå¨ä¸åæ ¼", null); |
| | | } |
| | | @Test |
| | | void pendingLinesDoNotReportCompletion() { |
| | | var mapper = mock(MesWmReturnSalesLineMapper.class); |
| | | var docs = mock(MesWmReturnSalesService.class); |
| | | var api = mock(AfterSaleTicketApi.class); |
| | | @SuppressWarnings("unchecked") ObjectProvider<AfterSaleTicketApi> provider = mock(ObjectProvider.class); |
| | | var line = new MesWmReturnSalesLineDO().setId(8L).setReturnId(42L).setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); |
| | | var pending = new MesWmReturnSalesLineDO().setId(9L).setReturnId(42L).setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()); |
| | | when(mapper.selectById(8L)).thenReturn(line); |
| | | when(mapper.selectListByReturnId(42L)).thenReturn(List.of(line, pending)); |
| | | var service = new MesWmReturnSalesLineServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnSalesLineMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnSalesService", docs); |
| | | ReflectionTestUtils.setField(service, "afterSaleTicketApiProvider", provider); |
| | | service.updateReturnSalesLineWhenRqcFinish(8L, 42L, 1, BigDecimal.ONE, BigDecimal.ZERO); |
| | | verifyNoInteractions(api); |
| | | verify(docs, never()).updateReturnSalesStatus(anyLong(), anyInt()); |
| | | } |
| | | @Test |
| | | void allPassedResultsAreReportedToSourceReturn() { |
| | | var mapper = mock(MesWmReturnSalesLineMapper.class); |
| | | var docs = mock(MesWmReturnSalesService.class); |
| | | var api = mock(AfterSaleTicketApi.class); |
| | | @SuppressWarnings("unchecked") ObjectProvider<AfterSaleTicketApi> provider = mock(ObjectProvider.class); |
| | | when(provider.getIfAvailable()).thenReturn(api); |
| | | var line = new MesWmReturnSalesLineDO().setId(8L).setReturnId(42L).setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); |
| | | when(mapper.selectById(8L)).thenReturn(line); |
| | | when(mapper.selectListByReturnId(42L)).thenReturn(List.of(line)); |
| | | when(docs.validateReturnSalesExists(42L)).thenReturn(new MesWmReturnSalesDO().setId(42L).setAfterSaleReturnId(73L)); |
| | | when(api.onReturnQualityFinished(73L, "åæ ¼", null)).thenReturn(CommonResult.success(true)); |
| | | var service = new MesWmReturnSalesLineServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "returnSalesLineMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "returnSalesService", docs); |
| | | ReflectionTestUtils.setField(service, "afterSaleTicketApiProvider", provider); |
| | | service.updateReturnSalesLineWhenRqcFinish(8L, 42L, 1, BigDecimal.ONE, BigDecimal.ZERO); |
| | | verify(api).onReturnQualityFinished(73L, "åæ ¼", null); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.salesnotice; |
| | | |
| | | import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.salesnotice.MesWmSalesNoticeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class SalesNoticeOrderGuardTest { |
| | | @Test |
| | | void cancelledOrderCannotGenerateDeliveryNotice() { |
| | | var orders = mock(ErpSaleOrderApi.class); |
| | | var notices = mock(MesWmSalesNoticeMapper.class); |
| | | when(orders.lockSaleOrderForNotice(1L, null)).thenThrow(new IllegalArgumentException("订åå·²ä½åº")); |
| | | var service = new MesWmSalesNoticeServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "saleOrderApi", orders); |
| | | ReflectionTestUtils.setField(service, "salesNoticeMapper", notices); |
| | | assertThrows(RuntimeException.class, () -> service.generateSalesNoticeFromSaleOrder(1L)); |
| | | verify(notices, never()).insert(any(MesWmSalesNoticeDO.class)); |
| | | } |
| | | } |
| | |
| | | if (CollUtil.isEmpty(quotes)) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_QUOTE_NOT_EXISTS); |
| | | } |
| | | Map<Long, SrmSupplierQuoteDO> quoteMap = quotes.stream() |
| | | .collect(Collectors.toMap( |
| | | SrmSupplierQuoteDO::getTenderMaterialId, |
| | | Function.identity(), |
| | | (a, b) -> a)); |
| | | Map<Long, SrmSupplierQuoteDO> quoteMap = SrmPurchaseQuoteValidator.validate(materials, quotes); |
| | | |
| | | // 5. æ¹éæ¥ MDM ç©æï¼è·å计éåä½ |
| | | Set<Long> productIds = materials.stream() |
| | |
| | | .collect(Collectors.toSet()); |
| | | Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemList(productIds).getCheckedData() |
| | | .stream().collect(Collectors.toMap(MdmItemRespDTO::getId, Function.identity())); |
| | | if (productIds.stream().anyMatch(productId -> !itemMap.containsKey(productId))) { |
| | | throw ServiceExceptionUtil.invalidParamException("å卿 æ³å¹é
çç©æï¼è¯·å
ç»´æ¤ç©æååçæéè´è®¢å"); |
| | | } |
| | | |
| | | // 6. æå»ºè®¢åé¡¹ï¼æ¥ä»·ä¸ºâåä»·âå£å¾ï¼è¡éé¢ = æ¥ä»·åä»· à æ°éï¼ |
| | | // 宿 éé¢ä¸æ¥ä»·å计ä¸ä¸è´æ¶æè¡é颿¯ä¾åæï¼è®¢åè¡åä»·ä»åæ¥ä»·åä»·ï¼ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.service.tender; |
| | | |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierQuoteDO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| | | |
| | | /** çæéè´ååæ ¸å¯¹æ¯ä¸é¡¹ç©æï¼é¿å
éé»ä¸¢å¤±è®¢åæç»ã */ |
| | | final class SrmPurchaseQuoteValidator { |
| | | private SrmPurchaseQuoteValidator() {} |
| | | |
| | | static Map<Long, SrmSupplierQuoteDO> validate(List<SrmTenderMaterialDO> materials, List<SrmSupplierQuoteDO> quotes) { |
| | | if (materials == null || materials.isEmpty()) throw invalidParamException("ææ é¡¹ç®æ²¡æç©æï¼ä¸è½çæéè´è®¢å"); |
| | | Map<Long, SrmSupplierQuoteDO> byMaterial = new HashMap<>(); |
| | | if (quotes != null) for (var quote : quotes) { |
| | | if (quote.getTenderMaterialId() == null || byMaterial.putIfAbsent(quote.getTenderMaterialId(), quote) != null) |
| | | throw invalidParamException("åå¨é夿æ ç©æçæ¥ä»·ï¼è¯·å
æ ¸å¯¹"); |
| | | } |
| | | for (var material : materials) { |
| | | var quote = byMaterial.get(material.getId()); |
| | | if (material.getId() == null || material.getProductId() == null || material.getQuantity() == null |
| | | || material.getQuantity().signum() <= 0 || quote == null || quote.getQuotePrice() == null |
| | | || quote.getQuotePrice().signum() < 0) |
| | | throw invalidParamException("å卿ªæ¥ä»·æç©ææ°éæ æçæç»ï¼è¯·è¡¥é½ååçæéè´è®¢å"); |
| | | } |
| | | if (byMaterial.size() != materials.size()) throw invalidParamException("æ¥ä»·å
å«ä¸å±äºå½å项ç®çç©æï¼è¯·æ ¸å¯¹"); |
| | | return byMaterial; |
| | | } |
| | | } |
| | |
| | | if (CollUtil.isEmpty(quotes)) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_QUOTE_NOT_EXISTS); |
| | | } |
| | | Map<Long, SrmSupplierQuoteDO> quoteMap = quotes.stream() |
| | | .collect(Collectors.toMap( |
| | | SrmSupplierQuoteDO::getTenderMaterialId, |
| | | Function.identity(), |
| | | (a, b) -> a)); |
| | | Map<Long, SrmSupplierQuoteDO> quoteMap = SrmPurchaseQuoteValidator.validate(materials, quotes); |
| | | |
| | | // æ¥ MDM ç©æ |
| | | Set<Long> productIds = materials.stream() |
| | |
| | | .collect(Collectors.toSet()); |
| | | Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemList(productIds).getCheckedData() |
| | | .stream().collect(Collectors.toMap(MdmItemRespDTO::getId, Function.identity())); |
| | | if (productIds.stream().anyMatch(id -> !itemMap.containsKey(id))) { |
| | | throw ServiceExceptionUtil.invalidParamException("å卿 æ³å¹é
çç©æï¼è¯·å
ç»´æ¤ç©æååçæéè´è®¢å"); |
| | | } |
| | | |
| | | // 计ç®ä»·æ ¼ï¼æ¥ä»·ä¸ºâåä»·âå£å¾ï¼è¡éé¢ = æ¥ä»·åä»· à æ°éï¼ |
| | | // 宿 éé¢ä¸æ¥ä»·å计ä¸ä¸è´æ¶æè¡é颿¯ä¾åæï¼è®¢åè¡åä»·ä»åæ¥ä»·åä»·ï¼ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.service.tender; |
| | | |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierQuoteDO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO; |
| | | import org.junit.jupiter.api.Test; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | class SrmPurchaseQuoteValidationTest { |
| | | private SrmTenderMaterialDO material(long id) { return new SrmTenderMaterialDO().setId(id).setProductId(id).setQuantity(BigDecimal.ONE); } |
| | | private SrmSupplierQuoteDO quote(long id) { return new SrmSupplierQuoteDO().setTenderMaterialId(id).setQuotePrice(BigDecimal.TEN); } |
| | | @Test void missingQuoteCannotCreatePartialOrder() { |
| | | assertThrows(RuntimeException.class, () -> SrmPurchaseQuoteValidator.validate(List.of(material(1),material(2)), List.of(quote(1)))); |
| | | } |
| | | @Test void duplicateQuoteCannotSilentlyChooseOne() { |
| | | assertThrows(RuntimeException.class, () -> SrmPurchaseQuoteValidator.validate(List.of(material(1)), List.of(quote(1),quote(1)))); |
| | | } |
| | | @Test void completeQuoteProducesAllMaterials() { |
| | | assertEquals(2, SrmPurchaseQuoteValidator.validate(List.of(material(1),material(2)), List.of(quote(1),quote(2))).size()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.service.tender; |
| | | |
| | | import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmSupplierQuoteSaveReqVO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.*; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.*; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.mockito.Mockito.*; |
| | | import static org.mockito.ArgumentMatchers.*; |
| | | |
| | | class SrmQuoteOwnershipTest { |
| | | @Test void rejectsMaterialFromAnotherProject() { |
| | | var service = new SrmTenderBidServiceImpl(); |
| | | var bids = mock(SrmTenderBidMapper.class); var projects = mock(SrmTenderProjectMapper.class); |
| | | var quotes = mock(SrmSupplierQuoteMapper.class); var materials = mock(SrmTenderMaterialMapper.class); |
| | | ReflectionTestUtils.setField(service, "tenderBidMapper", bids); |
| | | ReflectionTestUtils.setField(service, "tenderProjectMapper", projects); |
| | | ReflectionTestUtils.setField(service, "supplierQuoteMapper", quotes); |
| | | ReflectionTestUtils.setField(service, "tenderMaterialMapper", materials); |
| | | var bid = new SrmTenderBidDO(); bid.setId(1L); bid.setTenderProjectId(10L); bid.setBidStatus(0); |
| | | var project = new SrmTenderProjectDO(); project.setId(10L); project.setTenderStatus(10); |
| | | var material = new SrmTenderMaterialDO(); material.setId(9L); material.setTenderProjectId(11L); |
| | | when(bids.selectById(1L)).thenReturn(bid); when(projects.selectById(10L)).thenReturn(project); |
| | | when(materials.selectById(9L)).thenReturn(material); |
| | | var req = new SrmSupplierQuoteSaveReqVO(); req.setBidId(1L); req.setTenderMaterialId(9L); req.setQuotePrice(BigDecimal.ONE); |
| | | assertThrows(RuntimeException.class, () -> service.createQuote(req)); |
| | | verify(quotes, never()).insert(any(SrmSupplierQuoteDO.class)); |
| | | } |
| | | |
| | | @Test void rejectsEditingQuoteWithAnotherBidId() { |
| | | var service = new SrmTenderBidServiceImpl(); |
| | | var bids = mock(SrmTenderBidMapper.class); var projects = mock(SrmTenderProjectMapper.class); |
| | | var quotes = mock(SrmSupplierQuoteMapper.class); var materials = mock(SrmTenderMaterialMapper.class); |
| | | ReflectionTestUtils.setField(service, "tenderBidMapper", bids); |
| | | ReflectionTestUtils.setField(service, "tenderProjectMapper", projects); |
| | | ReflectionTestUtils.setField(service, "supplierQuoteMapper", quotes); |
| | | ReflectionTestUtils.setField(service, "tenderMaterialMapper", materials); |
| | | var bid = new SrmTenderBidDO(); bid.setId(1L); bid.setTenderProjectId(10L); bid.setBidStatus(0); |
| | | var project = new SrmTenderProjectDO(); project.setId(10L); project.setTenderStatus(10); |
| | | var quote = new SrmSupplierQuoteDO(); quote.setId(5L); quote.setBidId(2L); quote.setTenderMaterialId(9L); |
| | | when(bids.selectById(1L)).thenReturn(bid); when(projects.selectById(10L)).thenReturn(project); |
| | | when(quotes.selectById(5L)).thenReturn(quote); |
| | | var req = new SrmSupplierQuoteSaveReqVO(); req.setId(5L); req.setBidId(1L); req.setTenderMaterialId(9L); req.setQuotePrice(BigDecimal.ONE); |
| | | assertThrows(RuntimeException.class, () -> service.updateQuote(req)); |
| | | verify(quotes, never()).updateById(any(SrmSupplierQuoteDO.class)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.srm.service.tender; |
| | | |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderConfigDO; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderConfigMapper; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import static org.junit.jupiter.api.Assertions.assertSame; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class SrmTenderConfigServiceTest { |
| | | @Test void readsAndUpdatesTheSameMostRecentlyEditedConfiguration() { |
| | | var mapper = mock(SrmTenderConfigMapper.class); |
| | | var config = new SrmTenderConfigDO(); config.setId(2L); config.setDefaultBiddingMode("FULL"); |
| | | when(mapper.selectLatest()).thenReturn(config); |
| | | var service = new SrmTenderConfigServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "srmTenderConfigMapper", mapper); |
| | | assertSame(config, service.getConfig()); |
| | | service.saveConfig("SIMPLE"); |
| | | verify(mapper).updateById(config); |
| | | } |
| | | } |