| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.feedback; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*; |
| | | import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackAuxDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackAuxMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProProcessBillingTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcWageFieldService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService; |
| | |
| | | import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderParamRecordService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackQualityResult; |
| | | import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageBreakdown; |
| | | import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageCalculator; |
| | | import cn.iocoder.yudao.module.mes.service.pro.wage.MesProProcessWageRuleService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.worktime.MesProWorktimeService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService; |
| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; |
| | |
| | | |
| | | @Resource |
| | | private MesProFeedbackMapper feedbackMapper; |
| | | @Resource |
| | | private MesProFeedbackAuxMapper feedbackAuxMapper; |
| | | @Resource |
| | | private MesProWorktimeService worktimeService; |
| | | @Resource |
| | | private MesProFeedbackWageCalculator wageCalculator; |
| | | @Resource |
| | | private MesProProcessWageRuleService wageRuleService; |
| | | @Resource |
| | | private MesQcIpqcMapper ipqcMapper; |
| | | @Resource |
| | | @Lazy // IPQC 完成回写报工,避免循环依赖 |
| | | private MesQcIpqcService ipqcService; |
| | | @Resource |
| | | @Lazy // 过程检验单工资字段值(质检填写),避免与 IPQC 主服务循环依赖 |
| | | private MesQcIpqcWageFieldService ipqcWageFieldService; |
| | | |
| | | @Resource |
| | | private MesProWorkOrderService workOrderService; |
| | |
| | | .setId(null) |
| | | .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus()) |
| | | .setItemId(task.getItemId()); |
| | | // 2.1 计算标准工时快照与辅助工时明细(只用于考核对比,参数缺失不阻断报工) |
| | | List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(feedback, createReqVO.getAuxList()); |
| | | // 2.2 按工序计费类型、单价规则、辅助计薪与工资字段快照工资,谁报的工就记在谁名下 |
| | | applyProcessBilling(feedback, processService.getProcess(task.getProcessId()), auxList); |
| | | feedbackMapper.insert(feedback); |
| | | saveFeedbackAux(feedback.getId(), auxList); |
| | | // 3. 保存参数值;必填参数校验失败时整体回滚,避免残留草稿 |
| | | saveParamValues(task.getId(), createReqVO.getParamValues()); |
| | | return feedback.getId(); |
| | |
| | | // 2. 更新(自动填充 itemId) |
| | | MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class) |
| | | .setItemId(task.getItemId()); |
| | | // 2.1 重新计算标准工时快照与辅助工时明细 |
| | | List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(updateObj, updateReqVO.getAuxList()); |
| | | // 2.2 按工序当前的计费类型/单价规则/辅助计薪/工资字段重算工资(草稿阶段允许改工时/数量) |
| | | applyProcessBilling(updateObj, processService.getProcess(task.getProcessId()), auxList); |
| | | feedbackMapper.updateById(updateObj); |
| | | feedbackAuxMapper.deleteByFeedbackId(updateReqVO.getId()); |
| | | saveFeedbackAux(updateReqVO.getId(), auxList); |
| | | equipmentService.resetSnapshot(updateReqVO.getId()); |
| | | // 3. 保存参数值;必填参数校验失败时整体回滚 |
| | | saveParamValues(task.getId(), updateReqVO.getParamValues()); |
| | | } |
| | | |
| | | private void saveFeedbackAux(Long feedbackId, List<MesProFeedbackAuxDO> auxList) { |
| | | if (CollUtil.isEmpty(auxList)) { |
| | | return; |
| | | } |
| | | auxList.forEach(aux -> { |
| | | aux.setId(null).setFeedbackId(feedbackId); |
| | | feedbackAuxMapper.insert(aux); |
| | | }); |
| | | } |
| | | |
| | | private void saveParamValues(Long taskId, List<MesProFeedbackSaveReqVO.ParamValue> paramValues) { |
| | |
| | | // 1. 校验存在 + 草稿状态 |
| | | validateFeedbackStatusPrepare(id); |
| | | |
| | | // 2. 删除 |
| | | // 2. 删除(辅助工时明细一并删除) |
| | | feedbackMapper.deleteById(id); |
| | | feedbackAuxMapper.deleteByFeedbackId(id); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId()); |
| | | |
| | | // 1.2 报工与质检分离:提交报工不再自动生成过程检验单,质检由质检部门手工建单 |
| | | if (!processInstanceApi.isGlobalAutoApprove() && workOrder != null |
| | | && Boolean.TRUE.equals(workOrder.getFeedbackApprove()) |
| | | && cn.hutool.core.util.StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) { |
| | |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(id) |
| | | .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())); |
| | | MesProFeedbackApproveReqVO approveReqVO = cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(reqVO, MesProFeedbackApproveReqVO.class); |
| | | approveFeedback(approveReqVO); |
| | | // 自动审批:过程检验单刚生成尚未填写,不做质检工资前置校验(质量调整留空) |
| | | approveFeedback(approveReqVO, false); |
| | | } |
| | | } |
| | | |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) { |
| | | return approveFeedback(reqVO, true); |
| | | } |
| | | |
| | | private boolean approveFeedback(MesProFeedbackApproveReqVO reqVO, boolean enforceQualityGate) { |
| | | equipmentService.lock(); |
| | | // 1.1 校验存在 + 审批中状态 |
| | | MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId()); |
| | | equipmentService.validateApproval(feedback); |
| | | try (var equipmentScope = equipmentService.beginApproval(feedback)) { |
| | | return approveValidatedFeedback(reqVO, feedback); |
| | | return approveValidatedFeedback(reqVO, feedback, enforceQualityGate); |
| | | } |
| | | } |
| | | |
| | | private boolean approveValidatedFeedback(MesProFeedbackApproveReqVO reqVO, MesProFeedbackDO feedback) { |
| | | private boolean approveValidatedFeedback(MesProFeedbackApproveReqVO reqVO, MesProFeedbackDO feedback, |
| | | boolean enforceQualityGate) { |
| | | // 1.2 校验报工数量 > 0 |
| | | if (feedback.getFeedbackQuantity() == null |
| | | || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | |
| | | // 1.3 校验任务未完成 |
| | | taskService.validateTaskNotFinished(feedback.getTaskId()); |
| | | |
| | | // 工序检验标识不再控制产出入库:所有新报工产出均需人工质检。 |
| | | MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( |
| | | feedback.getRouteId(), feedback.getProcessId()); |
| | | boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag()); |
| | | // 报工与质检分离:报工审批不再依赖「是否质检」开关,质量数据由质检单回填。 |
| | | // 1.4 工资按报工时点的快照重算,避免工序定额事后调整影响已提交的报工 |
| | | recalculateWageFromSnapshot(feedback, null); |
| | | |
| | | // 3. 物料消耗:倒冲工序按本工序投料 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料) |
| | | boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag()); |
| | | if (backflushFlag) { |
| | | // 3.1 校验倒冲工序必须配置本工序的投料 BOM,避免静默不扣料 |
| | | // (只校验当前报工工序的投料,其他工序投料由各自工序报工时扣除) |
| | | List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId( |
| | | feedback.getWorkOrderId(), feedback.getProcessId()); |
| | | if (CollUtil.isEmpty(boms)) { |
| | | throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED); |
| | | } |
| | | // 3.2 生成消耗记录并执行扣减(本工序投料用量比例 × 报工数量,FIFO 扣线边库) |
| | | // 3. 物料消耗:只要本工序配置了生产 BOM 就自动扣料,不再依赖倒冲开关; |
| | | // 未配置 BOM 的工序跳过(物料走手工领料/投料),不阻断报工审批 |
| | | List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId( |
| | | feedback.getWorkOrderId(), feedback.getProcessId()); |
| | | if (CollUtil.isNotEmpty(boms)) { |
| | | // 3.1 生成消耗记录并执行扣减(本工序投料用量比例 × 报工数量,FIFO 扣线边库) |
| | | MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback); |
| | | if (itemConsume != null) { |
| | | itemConsumeService.finishItemConsume(itemConsume.getId()); |
| | |
| | | .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()) |
| | | .setQualifiedQuantity(BigDecimal.ZERO) |
| | | .setUnqualifiedQuantity(BigDecimal.ZERO) |
| | | .setUncheckQuantity(feedback.getFeedbackQuantity())); |
| | | .setUncheckQuantity(feedback.getFeedbackQuantity()) |
| | | .setBillingType(feedback.getBillingType()) |
| | | .setWageRate(feedback.getWageRate()) |
| | | .setWageRuleId(feedback.getWageRuleId()) |
| | | .setWorkHours(feedback.getWorkHours()) |
| | | .setWageAmount(feedback.getWageAmount()) |
| | | .setAuxPaidHours(feedback.getAuxPaidHours()) |
| | | .setAuxWageAmount(feedback.getAuxWageAmount()) |
| | | .setQualityPassRate(feedback.getQualityPassRate()) |
| | | .setQualityCoefficient(feedback.getQualityCoefficient()) |
| | | .setQualityDeduction(feedback.getQualityDeduction()) |
| | | .setWageBreakdown(feedback.getWageBreakdown())); |
| | | feedback.setQualifiedQuantity(BigDecimal.ZERO).setUnqualifiedQuantity(BigDecimal.ZERO); |
| | | productProduceService.generateProductProduce(feedback, true, reqVO); |
| | | |
| | | // 6. 根据是否需要检验,决定产出和入库方式 |
| | | // 6.2 调整 只要审批成功直接更新 |
| | | // 关键工序:更新任务 + 工单的已生产数量;非关键工序:只更新任务的已生产数量(工单只看关键工序产出) |
| | | if (keyFlag) { |
| | | updateTaskAndWorkOrderByFeedback(feedback); |
| | | } else { |
| | | taskService.updateProducedQuantity(feedback.getTaskId(), |
| | | feedback.getFeedbackQuantity(), |
| | | ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO)); |
| | | checkAndPublishProductionFinished(feedback.getWorkOrderId()); |
| | | |
| | | } |
| | | // 每道工序只累计本任务;工单与 MPS 由最后一道工序回写,与关键工序标记无关。 |
| | | updateTaskAndWorkOrderByFeedback(feedback); |
| | | equipmentService.afterApproved(feedback); |
| | | return false; // 产出等待质检,报工产量已累计 |
| | | } |
| | |
| | | * |
| | | * @param feedback 报工单 |
| | | */ |
| | | private void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) { |
| | | void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) { |
| | | // 报工审批只累计产量;合格与不合格数量在质检完成时累计。 |
| | | taskService.updateProducedQuantity(feedback.getTaskId(), |
| | | feedback.getFeedbackQuantity(), BigDecimal.ZERO, BigDecimal.ZERO); |
| | | // 3. 更新订单的已生产数量 |
| | | // 新增逻辑:只有在该笔订单最后一个工序才更新生产订单的已生产数量 |
| | | // 只有最后一道工序的报工计入工单与 MPS,避免中间工序重复累计。 |
| | | MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId()); |
| | | if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) { |
| | | workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), |
| | | feedback.getFeedbackQuantity()); |
| | | // 4. 更新 MPS 的已生产数量(如果是销售订单来源) |
| | | MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(feedback.getWorkOrderId()); |
| | | if (mps != null) { |
| | | mpsApi.updateMpsProducedQuantity(mps.getId(), feedback.getFeedbackQuantity()); |
| | | } |
| | | // MPS 只记录销售订单需求量;实际超产仍由工单和产出入库记录完整保留。 |
| | | allocateMpsProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity()); |
| | | } |
| | | |
| | | |
| | |
| | | /** |
| | | * 检查并发布生产完成事件 |
| | | */ |
| | | private void checkAndPublishProductionFinished(Long workOrderId) { |
| | | // 1. 获取工单关联的 MPS |
| | | MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(workOrderId); |
| | | if (mps == null || mps.getSaleOrderId() == null |
| | | || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.FINISHED.getStatus()) |
| | | || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) { |
| | | return; // 非销售订单来源的工单,跳过 |
| | | void checkAndPublishProductionFinished(Long workOrderId) { |
| | | Set<Long> newlyCompletedSaleOrderIds = new LinkedHashSet<>(); |
| | | for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) { |
| | | if (mps.getSaleOrderId() == null |
| | | || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.FINISHED.getStatus()) |
| | | || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) { |
| | | continue; |
| | | } |
| | | if (Boolean.TRUE.equals(mpsApi.checkAndFinishMps(mps.getId()))) { |
| | | newlyCompletedSaleOrderIds.add(mps.getSaleOrderId()); |
| | | } |
| | | } |
| | | |
| | | // 2. 检查并自动完成 MPS(如果满足条件) |
| | | if (!Boolean.TRUE.equals(mpsApi.checkAndFinishMps(mps.getId()))) { |
| | | return; |
| | | for (Long saleOrderId : newlyCompletedSaleOrderIds) { |
| | | if (mpsApi.checkSaleOrderProductionFinished(saleOrderId)) { |
| | | eventPublisher.publishEvent(new ProductionFinishedEvent(saleOrderId, java.time.LocalDateTime.now())); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 3. 检查销售订单所有生产是否完成 |
| | | if (mpsApi.checkSaleOrderProductionFinished(mps.getSaleOrderId())) { |
| | | eventPublisher.publishEvent(new ProductionFinishedEvent(mps.getSaleOrderId(), java.time.LocalDateTime.now())); |
| | | void allocateMpsProducedQuantity(Long workOrderId, BigDecimal actualQuantity) { |
| | | BigDecimal remaining = actualQuantity; |
| | | for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) { |
| | | if (remaining.signum() <= 0) { |
| | | break; |
| | | } |
| | | if (ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) { |
| | | continue; |
| | | } |
| | | BigDecimal planned = mps.getQuantity() == null ? BigDecimal.ZERO : mps.getQuantity(); |
| | | BigDecimal produced = mps.getQuantityProduced() == null ? BigDecimal.ZERO : mps.getQuantityProduced(); |
| | | BigDecimal need = planned.subtract(produced).max(BigDecimal.ZERO); |
| | | BigDecimal assigned = remaining.min(need); |
| | | if (assigned.signum() > 0) { |
| | | mpsApi.updateMpsProducedQuantity(mps.getId(), assigned); |
| | | remaining = remaining.subtract(assigned); |
| | | } |
| | | } |
| | | if (remaining.signum() > 0) { |
| | | log.info("[allocateMpsProducedQuantity] 工单({}) 超产数量({})保留在产出库存,不分配到销售订单", workOrderId, remaining); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | return task; |
| | | } |
| | | |
| | | /** |
| | | * 按工序配置把计费类型、单价规则、辅助计薪与工资字段快照到报工单,并计算本次报工工资。 |
| | | * |
| | | * <p>口径: |
| | | * <ul> |
| | | * <li>计时:主工资 = 本次报工工时(小时)× 单价(元/小时);单价大于 0 时必须填写工时</li> |
| | | * <li>计件:主工资 = 合格数量(未填时取报工数量)× 单价(元/件)</li> |
| | | * <li>辅助:辅助明细分钟数 ÷ 60 × 辅助小时单价;工序辅助小时工资兜底</li> |
| | | * <li>字段:系数连乘作用于(主工资 + 辅助工资),再叠加加减金额与计价数量</li> |
| | | * </ul> |
| | | * 前端传入的 billingType/wageRate/wageAmount 一律以后端计算覆盖,防止被篡改。 |
| | | * 单价为 0 表示该工序尚未定价,按 0 元计薪,不阻断报工(核算时可按人调整)。 |
| | | */ |
| | | private void applyProcessBilling(MesProFeedbackDO feedback, MesProProcessDO process, |
| | | List<MesProFeedbackAuxDO> auxList) { |
| | | if (process == null) { |
| | | throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, feedback.getProcessId()); |
| | | } |
| | | MesProFeedbackWageBreakdown breakdown = wageCalculator.calculate(process, |
| | | feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(), |
| | | feedback.getFeedbackQuantity(), feedback.getQualifiedQuantity(), auxList, |
| | | feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null, |
| | | getWageFieldValues(feedback.getId())); |
| | | applyWageBreakdown(feedback, breakdown); |
| | | } |
| | | |
| | | /** |
| | | * 把工资计算结果写回报工单 |
| | | */ |
| | | private void applyWageBreakdown(MesProFeedbackDO feedback, MesProFeedbackWageBreakdown breakdown) { |
| | | if (CollUtil.isNotEmpty(breakdown.getWarnings())) { |
| | | log.warn("报工工资计算提示:报工单[{}] {}", feedback.getCode(), String.join(";", breakdown.getWarnings())); |
| | | } |
| | | feedback.setBillingType(breakdown.getBillingType()) |
| | | .setWageRate(breakdown.getWageRate()) |
| | | .setWageRuleId(breakdown.getWageRuleId()) |
| | | .setWorkHours(breakdown.getWorkHours()) |
| | | .setAuxPaidHours(breakdown.getAuxPaidHours()) |
| | | .setAuxWageAmount(breakdown.getAuxWage()) |
| | | .setWageAmount(breakdown.getFinalWage()) |
| | | .setQualityPassRate(breakdown.getQuality() != null ? breakdown.getQuality().getPassRate() : null) |
| | | .setQualityCoefficient(breakdown.getQuality() != null ? breakdown.getQuality().getCoefficient() : null) |
| | | .setQualityDeduction(breakdown.getQuality() != null && breakdown.getQuality().getDeduction() != null |
| | | ? breakdown.getQuality().getDeduction() : BigDecimal.ZERO) |
| | | .setWageBreakdown(JsonUtils.toJsonString(breakdown)); |
| | | } |
| | | |
| | | /** |
| | | * 取第一个设备编号作为主计价设备(快捷报工的 machineryIds 为逗号分隔) |
| | | */ |
| | | private static Long firstMachineryId(String machineryIds) { |
| | | if (StrUtil.isBlank(machineryIds)) { |
| | | return null; |
| | | } |
| | | for (String part : machineryIds.split(",")) { |
| | | String trimmed = part.trim(); |
| | | if (StrUtil.isBlank(trimmed)) { |
| | | continue; |
| | | } |
| | | try { |
| | | return Long.parseLong(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return null; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 审批时按报工时点的快照重算工资,避免工序配置事后调整影响已提交的报工。 |
| | | * 历史数据(无工资拆分快照)则按当前工序配置补齐。 |
| | | * |
| | | * @param qualityResult 过程检验汇总;空=非质检工序或自动审批,不做质量调整 |
| | | */ |
| | | private void recalculateWageFromSnapshot(MesProFeedbackDO feedback, MesProFeedbackQualityResult qualityResult) { |
| | | BigDecimal qualifiedQuantity = qualityResult != null |
| | | ? qualityResult.qualifiedQuantity() : feedback.getQualifiedQuantity(); |
| | | // 质检部门在过程检验单上填写的工资字段值,优先于工序固定值 |
| | | Map<String, String> wageFieldValues = getWageFieldValues(feedback.getId()); |
| | | MesProFeedbackWageBreakdown breakdown; |
| | | if (StrUtil.isBlank(feedback.getWageBreakdown())) { |
| | | breakdown = wageCalculator.calculate(processService.getProcess(feedback.getProcessId()), |
| | | feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(), |
| | | feedback.getFeedbackQuantity(), qualifiedQuantity, |
| | | feedback.getId() != null ? feedbackAuxMapper.selectListByFeedbackId(feedback.getId()) |
| | | : Collections.emptyList(), |
| | | feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null, |
| | | wageFieldValues); |
| | | } else { |
| | | breakdown = wageCalculator.fromSnapshot(feedback, qualifiedQuantity, wageFieldValues); |
| | | } |
| | | if (qualityResult != null) { |
| | | wageCalculator.applyQuality(breakdown, feedback.getProcessId(), feedback.getRouteId(), |
| | | qualityResult.passRate(), qualityResult.unqualifiedQuantity()); |
| | | } |
| | | applyWageBreakdown(feedback, breakdown); |
| | | } |
| | | |
| | | /** |
| | | * 取报工单关联的过程检验单上质检填写的工资字段值 |
| | | * |
| | | * <p>同一报工单存在多张检验单时,已完成的优先;字段未填写时不放入 Map,计算时回退工序固定值。 |
| | | */ |
| | | private Map<String, String> getWageFieldValues(Long feedbackId) { |
| | | if (feedbackId == null) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | List<MesQcIpqcDO> ipqcList = ipqcMapper.selectListBySourceDoc( |
| | | MesBizTypeConstants.PRO_FEEDBACK, feedbackId); |
| | | if (CollUtil.isEmpty(ipqcList)) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | List<Long> ipqcIds = new ArrayList<>(ipqcList.size()); |
| | | ipqcList.stream() |
| | | .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) |
| | | .map(MesQcIpqcDO::getId).forEach(ipqcIds::add); |
| | | ipqcList.stream() |
| | | .filter(ipqc -> !ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) |
| | | .map(MesQcIpqcDO::getId).forEach(ipqcIds::add); |
| | | return ipqcWageFieldService.getValueMapByIpqcIds(ipqcIds); |
| | | } |
| | | |
| | | /** |
| | | * 校验质检工序的过程检验已完成,并汇总合格率与不合格数量 |
| | | */ |
| | | private MesProFeedbackQualityResult validateAndGetQualityResult(MesProFeedbackDO feedback) { |
| | | MesProProcessDO process = processService.getProcess(feedback.getProcessId()); |
| | | String processName = process != null ? process.getName() : "ID=" + feedback.getProcessId(); |
| | | List<MesQcIpqcDO> finishedList = ipqcMapper.selectListBySourceDoc( |
| | | MesBizTypeConstants.PRO_FEEDBACK, feedback.getId()).stream() |
| | | .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) |
| | | .toList(); |
| | | if (CollUtil.isEmpty(finishedList)) { |
| | | throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(), |
| | | StrUtil.format("工序[{}]为质检工序,报工单[{}]尚未完成过程检验;" |
| | | + "请到「质量管理-过程检验」完成检验后再审批", |
| | | processName, feedback.getCode())); |
| | | } |
| | | BigDecimal checkQuantity = BigDecimal.ZERO; |
| | | BigDecimal qualifiedQuantity = BigDecimal.ZERO; |
| | | BigDecimal unqualifiedQuantity = BigDecimal.ZERO; |
| | | for (MesQcIpqcDO ipqc : finishedList) { |
| | | checkQuantity = checkQuantity.add(ObjectUtil.defaultIfNull(ipqc.getCheckQuantity(), BigDecimal.ZERO)); |
| | | qualifiedQuantity = qualifiedQuantity.add(ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO)); |
| | | unqualifiedQuantity = unqualifiedQuantity.add( |
| | | ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO)); |
| | | } |
| | | if (checkQuantity.signum() <= 0) { |
| | | throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(), |
| | | StrUtil.format("工序[{}]的过程检验单[{}]检测数量为 0,无法计算合格率;" |
| | | + "请在「质量管理-过程检验」补录检测数量后再审批", |
| | | processName, finishedList.get(0).getCode())); |
| | | } |
| | | BigDecimal passRate = qualifiedQuantity.multiply(BigDecimal.valueOf(100)) |
| | | .divide(checkQuantity, 2, RoundingMode.HALF_UP); |
| | | return new MesProFeedbackQualityResult(passRate, checkQuantity, qualifiedQuantity, unqualifiedQuantity); |
| | | } |
| | | |
| | | /** |
| | |
| | | .setTaskId(reqVO.getTaskId()) |
| | | .setItemId(task.getItemId()) |
| | | .setFeedbackQuantity(reqVO.getFeedbackQuantity()) |
| | | .setWorkHours(reqVO.getWorkHours()) |
| | | .setScheduledQuantity(task.getQuantity()) |
| | | .setFeedbackTime(reqVO.getFeedbackTime() != null ? reqVO.getFeedbackTime() : LocalDateTime.now()) |
| | | .setMachineryIds(reqVO.getMachineryIds()) |
| | | .setMachineryId(firstMachineryId(reqVO.getMachineryIds())) |
| | | .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()) |
| | | .setRemark(reqVO.getRemark()); |
| | | // 4.1 快捷报工默认没有工时录入入口:计时计薪且已定价的工序,必须显式传工时; |
| | | // 批量报工等不传工时的入口会得到可执行的提示,引导到报工页面填写 |
| | | MesProProcessDO process = processService.getProcess(task.getProcessId()); |
| | | if (process == null) { |
| | | throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId()); |
| | | } |
| | | BigDecimal wageRate = ObjectUtil.defaultIfNull(wageRuleService.resolve(task.getProcessId(), task.getRouteId(), |
| | | feedback.getMachineryId(), process.getBillingType(), LocalDate.now()).getWageRate(), BigDecimal.ZERO); |
| | | if (MesProProcessBillingTypeEnum.TIME == MesProProcessBillingTypeEnum.valueOfType(process.getBillingType()) |
| | | && wageRate.signum() > 0 && reqVO.getWorkHours() == null) { |
| | | throw exception(PRO_FEEDBACK_TIME_BILLING_QUICK_UNSUPPORTED, process.getName()); |
| | | } |
| | | // 4.2 快捷报工没有辅助项入口,仅按生产长度(报工单位为米时)计算标准工时快照 |
| | | List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(feedback, null); |
| | | // 4.3 按工序计费类型、单价规则与工资字段快照并计算本次报工工资 |
| | | applyProcessBilling(feedback, process, auxList); |
| | | equipmentService.validateSubmission(feedback); |
| | | feedbackMapper.insert(feedback); |
| | | saveFeedbackAux(feedback.getId(), auxList); |
| | | saveParamValues(task.getId(), reqVO.getParamValues()); |
| | | // 5. 返回结果 |
| | | MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId()); |
| | |
| | | .setTaskStatus(task.getStatus()) |
| | | .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null) |
| | | .setReportableQuantity(reportableQuantity); |
| | | } |
| | | |
| | | @Override |
| | | public MesProFeedbackWageBreakdown previewWage(MesProFeedbackWagePreviewReqVO reqVO) { |
| | | MesProTaskDO task = taskService.getTask(reqVO.getTaskId()); |
| | | if (task == null) { |
| | | throw exception(PRO_TASK_NOT_EXISTS); |
| | | } |
| | | MesProProcessDO process = processService.getProcess(task.getProcessId()); |
| | | if (process == null) { |
| | | throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId()); |
| | | } |
| | | // 借用一个临时报工单对象复用标准工时/辅助工时计算 |
| | | MesProFeedbackDO temp = new MesProFeedbackDO() |
| | | .setTaskId(task.getId()) |
| | | .setRouteId(task.getRouteId()) |
| | | .setProcessId(task.getProcessId()) |
| | | .setWorkstationId(task.getWorkstationId()) |
| | | .setMachineryId(reqVO.getMachineryId()) |
| | | .setWorkHours(reqVO.getWorkHours()) |
| | | .setFeedbackQuantity(reqVO.getFeedbackQuantity()) |
| | | .setQualifiedQuantity(reqVO.getQualifiedQuantity()) |
| | | .setProductionLength(reqVO.getProductionLength()); |
| | | List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(temp, reqVO.getAuxList()); |
| | | return wageCalculator.calculate(process, task.getRouteId(), reqVO.getMachineryId(), |
| | | reqVO.getWorkHours(), reqVO.getFeedbackQuantity(), reqVO.getQualifiedQuantity(), auxList, |
| | | reqVO.getFeedbackDate()); |
| | | } |
| | | |
| | | @Override |
| | |
| | | ObjectUtil.defaultIfNull(unqualifiedQty, BigDecimal.ZERO)); |
| | | //5.检查并发布生产完成事件 |
| | | checkAndPublishProductionFinished(feedback.getWorkOrderId()); |
| | | checkAndAutoFinishAfterLastQc(feedback); |
| | | } |
| | | |
| | | void checkAndAutoFinishAfterLastQc(MesProFeedbackDO feedback) { |
| | | MesProWorkOrderProcessDO lastProcess = workOrderProcessService |
| | | .getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId()); |
| | | if (lastProcess == null || !ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) { |
| | | return; |
| | | } |
| | | // 同一工单的末道报工全部完成质检后,才允许级联完成工单任务。 |
| | | Long pendingCount = feedbackMapper.selectCount(new LambdaQueryWrapperX<MesProFeedbackDO>() |
| | | .eq(MesProFeedbackDO::getWorkOrderId, feedback.getWorkOrderId()) |
| | | .eq(MesProFeedbackDO::getProcessId, feedback.getProcessId()) |
| | | .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus())); |
| | | if (pendingCount == 0) { |
| | | workOrderService.checkAndAutoFinishWorkOrder(feedback.getWorkOrderId()); |
| | | } |
| | | } |
| | | |
| | | @Override |