package cn.iocoder.yudao.module.mes.service.pro.feedback;
|
|
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.pro.task.MesProTaskDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
|
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.MesProWorkOrderBomService;
|
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 cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
|
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
|
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
|
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
|
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
|
import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent;
|
import jakarta.annotation.Resource;
|
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.annotation.Lazy;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
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.*;
|
|
/**
|
* MES 生产报工 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
@lombok.extern.slf4j.Slf4j
|
public class MesProFeedbackServiceImpl implements MesProFeedbackService {
|
|
@Resource
|
private MesProFeedbackEquipmentService equipmentService;
|
|
@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;
|
@Resource
|
@Lazy
|
private MesProWorkOrderParamRecordService paramRecordService;
|
@Resource
|
private MesProRouteProcessService routeProcessService;
|
@Resource
|
private MesProProcessService processService;
|
@Resource
|
private MesProWorkOrderBomService workOrderBomService;
|
@Resource
|
private MesProWorkOrderProcessService workOrderProcessService;
|
@Resource
|
@Lazy // 避免循环依赖
|
private MesProTaskService taskService;
|
@Resource
|
private MesWmItemConsumeService itemConsumeService;
|
@Resource
|
private MesWmProductProduceService productProduceService;
|
@Resource
|
private MesWmProductProduceLineService produceLineService;
|
@Resource
|
private MesProMpsApi mpsApi;
|
@Resource
|
private MesMdAutoCodeRecordService autoCodeRecordService;
|
@Resource
|
private ApplicationEventPublisher eventPublisher;
|
@Resource
|
private MesMdmItemService mdmItemService;
|
@Resource
|
private MdmItemBatchConfigApi itemBatchConfigApi;
|
@Resource
|
private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
|
// 1. 校验
|
MesProTaskDO task = validateFeedbackData(createReqVO);
|
|
// 2. 插入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId)
|
MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class)
|
.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();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) {
|
equipmentService.lock();
|
// 1.1 校验存在 + 草稿状态
|
validateFeedbackStatusPrepare(updateReqVO.getId());
|
// 1.2 校验业务数据
|
MesProTaskDO task = validateFeedbackData(updateReqVO);
|
|
// 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) {
|
// 即使参数为空也调用更新,以便触发必填参数校验(任务绑定了参数时必须填写)
|
if (CollUtil.isEmpty(paramValues)) {
|
paramRecordService.updateTaskParamValues(taskId, null, null);
|
return;
|
}
|
paramRecordService.updateTaskParamValues(taskId,
|
paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getDetailId).toList(),
|
paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getValue).toList());
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteFeedback(Long id) {
|
equipmentService.lock();
|
// 1. 校验存在 + 草稿状态
|
validateFeedbackStatusPrepare(id);
|
|
// 2. 删除(辅助工时明细一并删除)
|
feedbackMapper.deleteById(id);
|
feedbackAuxMapper.deleteByFeedbackId(id);
|
}
|
|
@Override
|
public MesProFeedbackDO getFeedback(Long id) {
|
return feedbackMapper.selectById(id);
|
}
|
|
@Override
|
public List<MesProFeedbackDO> getFeedbackListByTaskIds(Collection<Long> taskIds) {
|
if (CollUtil.isEmpty(taskIds)) {
|
return Collections.emptyList();
|
}
|
return feedbackMapper.selectListByTaskIds(taskIds);
|
}
|
|
@Override
|
public List<MesProFeedbackDO> getFeedbackListByTaskId(Long taskId) {
|
return feedbackMapper.selectListByTaskId(taskId);
|
}
|
|
@Override
|
public PageResult<MesProFeedbackDO> getFeedbackPage(MesProFeedbackPageReqVO pageReqVO) {
|
return feedbackMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void submitFeedback(MesProFeedbackSubmitReqVO reqVO) {
|
equipmentService.lock();
|
Long id = reqVO.getId();
|
// 1. 校验存在 + 草稿状态
|
MesProFeedbackDO feedback = validateFeedbackStatusPrepare(id);
|
equipmentService.validateSubmission(feedback);
|
// 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许提交
|
if (feedback.getTaskId() != null) {
|
MesProTaskDO submitTask = taskService.getTask(feedback.getTaskId());
|
if (submitTask != null) {
|
validatePreviousProcessFinished(submitTask);
|
}
|
}
|
|
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())) {
|
// 2. 更新状态为审批中,并启动流程
|
String processInstanceId = processInstanceApi.createProcessInstance(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId(),
|
new cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO()
|
.setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
|
.setBusinessKey(String.valueOf(id)));
|
|
feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
|
.setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
|
.setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
|
.setProcessInstanceId(processInstanceId));
|
} else {
|
// 2. 判断这个报工需不需要批次号
|
MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
|
if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()) {
|
if (StrUtil.isBlank(reqVO.getLotNumber())) {
|
throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
|
}
|
}
|
|
// 3. 更新状态为审批中,然后直接通过审批
|
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, false);
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void handleBpmProcessResult(Long id, Integer bpmResult,String reason) {
|
// 1. 柔性校验:获取报工单并判断状态
|
MesProFeedbackDO feedback = feedbackMapper.selectById(id);
|
if (feedback == null) {
|
log.warn("[handleBpmProcessResult] 生产报工({}) 不存在", id);
|
return;
|
}
|
if (!MesProFeedbackStatusEnum.APPROVING.getStatus().equals(feedback.getStatus())) {
|
log.warn("[handleBpmProcessResult] 生产报工({}) 不处于审批中状态,当前状态:{}", id, feedback.getStatus());
|
return;
|
}
|
|
// 2. 根据审批结果进行处理
|
if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
|
MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
|
reqVO.setId(id);
|
// 判断这个报工需不需要批次号
|
MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
|
if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()){
|
if (StrUtil.isBlank(reason)){
|
// 报错 请在审批意见里填写批次号
|
throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
|
}else {
|
BeanUtils.copyProperties(mesProFeedbackBatchCheckRespVO,reqVO);
|
reqVO.setLotNumber(reason);
|
}
|
}
|
approveFeedback(reqVO);
|
} else if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult) ||
|
cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
|
rejectFeedback(id);
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void rejectFeedback(Long id) {
|
equipmentService.lock();
|
// 1. 校验存在 + 审批中状态
|
validateFeedbackStatusApproving(id);
|
|
// 2. 更新状态为草稿
|
feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
|
.setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus()));
|
}
|
|
@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, enforceQualityGate);
|
}
|
}
|
|
private boolean approveValidatedFeedback(MesProFeedbackApproveReqVO reqVO, MesProFeedbackDO feedback,
|
boolean enforceQualityGate) {
|
// 1.2 校验报工数量 > 0
|
if (feedback.getFeedbackQuantity() == null
|
|| feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
|
}
|
// 1.3 校验任务未完成
|
taskService.validateTaskNotFinished(feedback.getTaskId());
|
|
// 报工与质检分离:报工审批不再依赖「是否质检」开关,质量数据由质检单回填。
|
// 1.4 工资按报工时点的快照重算,避免工序定额事后调整影响已提交的报工
|
recalculateWageFromSnapshot(feedback, null);
|
|
// 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());
|
}
|
}
|
|
// 报工与质检分开:报工审批完成即累计产量,质量数量由质检回填。
|
feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
|
.setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())
|
.setQualifiedQuantity(BigDecimal.ZERO)
|
.setUnqualifiedQuantity(BigDecimal.ZERO)
|
.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);
|
|
// 每道工序只累计本任务;工单与 MPS 由最后一道工序回写,与关键工序标记无关。
|
updateTaskAndWorkOrderByFeedback(feedback);
|
equipmentService.afterApproved(feedback);
|
return false; // 产出等待质检,报工产量已累计
|
}
|
|
/**
|
* 根据当前报工单的最终结果,更新生产任务和生产工单的进度
|
*
|
* <ul>
|
* <li>使用报工数量(feedbackQuantity),累加任务和工单的已生产数量</li>
|
* <li>使用产出单行按 qualityStatus 聚合的合格品/不合格品数量,累加任务的合格品/不合格品数量
|
* (不直接用 feedback 上的数量,确保质检回调场景下数量来源正确)</li>
|
* isPush 是否推送生产完成事件
|
* </ul>
|
*
|
* @param feedback 报工单
|
*/
|
void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) {
|
// 报工审批只累计产量;合格与不合格数量在质检完成时累计。
|
taskService.updateProducedQuantity(feedback.getTaskId(),
|
feedback.getFeedbackQuantity(), BigDecimal.ZERO, BigDecimal.ZERO);
|
// 只有最后一道工序的报工计入工单与 MPS,避免中间工序重复累计。
|
MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
|
if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) {
|
workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
|
feedback.getFeedbackQuantity());
|
// MPS 只记录销售订单需求量;实际超产仍由工单和产出入库记录完整保留。
|
allocateMpsProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity());
|
}
|
|
|
// 5. 检查并发布生产完成事件
|
checkAndPublishProductionFinished(feedback.getWorkOrderId());
|
}
|
|
/**
|
* 检查并发布生产完成事件
|
*/
|
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());
|
}
|
}
|
for (Long saleOrderId : newlyCompletedSaleOrderIds) {
|
if (mpsApi.checkSaleOrderProductionFinished(saleOrderId)) {
|
eventPublisher.publishEvent(new ProductionFinishedEvent(saleOrderId, 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);
|
}
|
}
|
|
// ==================== 校验方法 ====================
|
|
@Override
|
public MesProFeedbackDO validateFeedbackExists(Long id) {
|
MesProFeedbackDO feedback = feedbackMapper.selectById(id);
|
if (feedback == null) {
|
throw exception(PRO_FEEDBACK_NOT_EXISTS);
|
}
|
return feedback;
|
}
|
|
private MesProFeedbackDO validateFeedbackStatusPrepare(Long id) {
|
MesProFeedbackDO feedback = validateFeedbackExists(id);
|
if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus())) {
|
throw exception(PRO_FEEDBACK_NOT_PREPARE);
|
}
|
return feedback;
|
}
|
|
private MesProFeedbackDO validateFeedbackStatusApproving(Long id) {
|
MesProFeedbackDO feedback = validateFeedbackExists(id);
|
if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus())) {
|
throw exception(PRO_FEEDBACK_NOT_APPROVING);
|
}
|
return feedback;
|
}
|
|
/**
|
* 校验报工单的业务数据(创建 & 修改共用)
|
*
|
* <p>以生产任务为核心:前端只需传 taskId,workOrderId、routeId、processId、itemId、workstationId
|
* 从任务自动填充,无需前端重复传入。
|
*
|
* @param reqVO 报工请求
|
* @return 关联的生产任务
|
*/
|
private MesProTaskDO validateFeedbackData(MesProFeedbackSaveReqVO reqVO) {
|
// 1. 校验任务存在且未终态
|
MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
|
|
// 校验停工状态不能报工
|
if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
|
throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
|
}
|
|
// 2. 从任务自动填充关联字段(前端无需传入,后端统一从任务获取)
|
reqVO.setWorkOrderId(task.getWorkOrderId());
|
reqVO.setRouteId(task.getRouteId());
|
reqVO.setProcessId(task.getProcessId());
|
reqVO.setItemId(task.getItemId());
|
reqVO.setWorkstationId(task.getWorkstationId());
|
|
// 3. 校验工艺路线 + 工序配置有效
|
MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
|
task.getRouteId(), task.getProcessId());
|
if (routeProcess == null) {
|
throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
|
}
|
// 4.1 校验数量:无论是否需要检验,报工只需填报工数量,且必须 > 0
|
// 合格品/不良品数量由质检确定。
|
if (reqVO.getFeedbackQuantity() == null
|
|| reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
|
}
|
|
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);
|
}
|
|
/**
|
* 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报本道工序
|
*
|
* <p>判定口径:上一道工序的有效任务(未取消)累计审批通过的产量已达到其排产数量,
|
* 即任一有效任务 produced_quantity < quantity 都视为未完成并拦截。
|
* 之所以不依赖任务 status:任务状态只有整单完工/取消时才置终态
|
* (finishTaskByOrderId / cancelTaskByOrderId),无法作为"单道工序已完成"的依据,
|
* produced_quantity 是唯一实时可靠的进度信号。
|
*
|
* @param task 当前待报工任务
|
*/
|
private void validatePreviousProcessFinished(MesProTaskDO task) {
|
if (task == null || task.getWorkOrderId() == null || task.getProcessId() == null) {
|
return; // 数据不完整,无法推导顺序,放行
|
}
|
// 1. 当前任务所属工序在工单工序链(按 sort 升序)中的位置
|
List<MesProWorkOrderProcessDO> processes = workOrderProcessService
|
.getWorkOrderProcessListByWorkOrderId(task.getWorkOrderId());
|
if (CollUtil.isEmpty(processes)) {
|
return; // 工单未配置工序,放行
|
}
|
// 拷贝后再排序,避免修改调用方返回的(可能是不可变)集合
|
processes = new ArrayList<>(processes);
|
processes.sort(Comparator.comparing(MesProWorkOrderProcessDO::getSort,
|
Comparator.nullsLast(Comparator.naturalOrder())));
|
int index = -1;
|
for (int i = 0; i < processes.size(); i++) {
|
if (ObjUtil.equal(processes.get(i).getProcessId(), task.getProcessId())) {
|
index = i;
|
break;
|
}
|
}
|
if (index <= 0) {
|
return; // 当前工序不在链中(数据异常,放行)或本身就是第一道工序,无需前置
|
}
|
Long prevProcessId = processes.get(index - 1).getProcessId();
|
if (prevProcessId == null) {
|
return;
|
}
|
// 2. 上一道工序的有效任务(排除已取消)累计待完成量
|
List<MesProTaskDO> prevTasks = taskService.getTaskListByWorkOrderIds(
|
Collections.singletonList(task.getWorkOrderId())).stream()
|
.filter(t -> ObjUtil.equal(t.getProcessId(), prevProcessId))
|
.filter(t -> !ObjUtil.equal(t.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus()))
|
.toList();
|
if (CollUtil.isEmpty(prevTasks)) {
|
return; // 上一道工序无有效生产任务(含全部取消),视为无前置要求,放行
|
}
|
BigDecimal remaining = BigDecimal.ZERO;
|
for (MesProTaskDO prevTask : prevTasks) {
|
BigDecimal qty = prevTask.getQuantity() == null ? BigDecimal.ZERO : prevTask.getQuantity();
|
BigDecimal produced = prevTask.getProducedQuantity() == null ? BigDecimal.ZERO : prevTask.getProducedQuantity();
|
if (produced.compareTo(qty) < 0) {
|
remaining = remaining.add(qty.subtract(produced));
|
}
|
}
|
if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
|
return; // 上一道工序已全部报满,放行
|
}
|
// 3. 拦截并提示上一道工序名称与剩余待报数量
|
MesProProcessDO prevProcess = processService.getProcess(prevProcessId);
|
String prevProcessName = prevProcess != null ? prevProcess.getName() : String.valueOf(prevProcessId);
|
throw exception(PRO_FEEDBACK_PREV_PROCESS_NOT_FINISHED, prevProcessName, remaining);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) {
|
equipmentService.lock();
|
// 1. 校验任务存在且非终态
|
MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
|
|
// 校验停工状态不能报工
|
if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
|
throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
|
}
|
// 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报工
|
validatePreviousProcessFinished(task);
|
|
// 2. 校验工艺路线工序配置
|
MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
|
task.getRouteId(), task.getProcessId());
|
if (routeProcess == null) {
|
throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
|
}
|
// 3. 校验数量:只填报工数量,且必须 > 0(合格品/不良品数量由质检确定)
|
if (reqVO.getFeedbackQuantity() == null
|
|| reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
|
}
|
// 4. 构建报工DO
|
MesProFeedbackDO feedback = new MesProFeedbackDO()
|
.setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode()))
|
.setType(MesProFeedbackTypeEnum.SELF.getType())
|
.setWorkstationId(task.getWorkstationId())
|
.setRouteId(task.getRouteId())
|
.setProcessId(task.getProcessId())
|
.setWorkOrderId(task.getWorkOrderId())
|
.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());
|
// 5.1 报工后剩余可报工数量 = 排产 - 已生产 - 在途(含本次报工)
|
BigDecimal reportable = task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity();
|
BigDecimal produced = task.getProducedQuantity() == null ? BigDecimal.ZERO : task.getProducedQuantity();
|
BigDecimal inTransit = BigDecimal.ZERO;
|
for (MesProFeedbackDO fb : getFeedbackListByTaskId(task.getId())) {
|
if (!ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())
|
&& !ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
|
inTransit = inTransit.add(fb.getFeedbackQuantity() == null ? BigDecimal.ZERO : fb.getFeedbackQuantity());
|
}
|
}
|
BigDecimal reportableQuantity = reportable.subtract(produced).subtract(inTransit).max(BigDecimal.ZERO);
|
return new MesProFeedbackQuickCreateRespVO()
|
.setFeedbackId(feedback.getId())
|
.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
|
@Transactional(rollbackFor = Exception.class)
|
public MesProFeedbackBatchCreateRespVO batchCreateFeedback(MesProFeedbackBatchCreateReqVO reqVO) {
|
List<MesProFeedbackBatchCreateRespVO.BatchResult> results = new java.util.ArrayList<>();
|
int successCount = 0;
|
int failCount = 0;
|
for (Long taskId : reqVO.getTaskIds()) {
|
try {
|
MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO();
|
singleReq.setTaskId(taskId);
|
singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity());
|
singleReq.setFeedbackTime(reqVO.getFeedbackTime());
|
singleReq.setRemark(reqVO.getRemark());
|
MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq);
|
results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
|
.setTaskId(taskId)
|
.setSuccess(true)
|
.setFeedbackId(resp.getFeedbackId()));
|
successCount++;
|
} catch (Exception e) {
|
results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
|
.setTaskId(taskId)
|
.setSuccess(false)
|
.setErrorMsg(e.getMessage()));
|
failCount++;
|
}
|
}
|
return new MesProFeedbackBatchCreateRespVO()
|
.setSuccessCount(successCount)
|
.setFailCount(failCount)
|
.setResults(results);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateProFeedbackWhenIpqcFinish(Long feedbackId, Long sourceLineId,
|
BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
|
BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) {
|
// 1. 校验报工单存在且为待检验状态
|
MesProFeedbackDO feedback = feedbackMapper.selectByIdForUpdate(feedbackId);
|
if (feedback == null) {
|
throw exception(PRO_FEEDBACK_NOT_EXISTS);
|
}
|
if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
|
throw exception(PRO_FEEDBACK_NOT_UNCHECK);
|
}
|
|
// 2. 拆分待检产出行(合格/不合格),生成明细,完成产出入库
|
productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, qualifiedQty, unqualifiedQty);
|
|
// 3. 回写合格/不合格/废品数量,更新状态为已完成
|
feedbackMapper.updateById(new MesProFeedbackDO().setId(feedbackId)
|
.setQualifiedQuantity(qualifiedQty)
|
.setUnqualifiedQuantity(unqualifiedQty)
|
.setUncheckQuantity(BigDecimal.ZERO)
|
.setLaborScrapQuantity(laborScrapQty)
|
.setMaterialScrapQuantity(materialScrapQty)
|
.setOtherScrapQuantity(otherScrapQty)
|
.setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()));
|
|
// 4. 产量已在报工审批时累计,此处只回填质量数量。
|
feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
|
taskService.updateQualityQuantity(feedback.getTaskId(),
|
ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO),
|
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
|
public MesProFeedbackBatchCheckRespVO checkBatchRequirements(Long feedbackId) {
|
MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
|
MesProFeedbackBatchCheckRespVO respVO = new MesProFeedbackBatchCheckRespVO();
|
|
// 1. 检查物料是否启用批次管理
|
MesMdmItemDO item = mdmItemService.getItem(feedback.getItemId());
|
if (item == null || Boolean.FALSE.equals(item.getIsBatchManaged())) {
|
respVO.setNeedBatchInput(false);
|
respVO.setIsBatchManaged(false);
|
return respVO;
|
}
|
respVO.setIsBatchManaged(true);
|
|
// 2. 获取批次配置
|
MdmItemBatchConfigRespDTO config = itemBatchConfigApi.getItemBatchConfig(feedback.getItemId());
|
if (config == null) {
|
respVO.setNeedBatchInput(false);
|
return respVO;
|
}
|
|
// 3. 填充配置标志
|
respVO.setProduceDateFlag(config.getProduceDateFlag());
|
respVO.setExpireDateFlag(config.getExpireDateFlag());
|
respVO.setReceiptDateFlag(config.getReceiptDateFlag());
|
respVO.setVendorFlag(config.getVendorFlag());
|
respVO.setClientFlag(config.getClientFlag());
|
respVO.setSalesOrderCodeFlag(config.getSalesOrderCodeFlag());
|
respVO.setPurchaseOrderCodeFlag(config.getPurchaseOrderCodeFlag());
|
respVO.setWorkOrderFlag(config.getWorkOrderFlag());
|
respVO.setTaskFlag(config.getTaskFlag());
|
respVO.setWorkstationFlag(config.getWorkstationFlag());
|
respVO.setToolFlag(config.getToolFlag());
|
respVO.setMoldFlag(config.getMoldFlag());
|
respVO.setLotNumberFlag(config.getLotNumberFlag());
|
respVO.setQualityStatusFlag(config.getQualityStatusFlag());
|
|
// 4. 判断哪些字段已自动填充(报工和生产过程中必然有值的字段)
|
List<String> autoFilled = new ArrayList<>();
|
// 生产日期:审批时自动取当天
|
autoFilled.add("produceDate");
|
// 生产工单:从报工记录获取
|
autoFilled.add("workOrderId");
|
// 生产任务:从报工记录获取
|
autoFilled.add("taskId");
|
// 工作站:从报工记录获取
|
autoFilled.add("workstationId");
|
respVO.setAutoFilledFields(autoFilled);
|
|
// 5. 判断是否需要弹窗:存在必填但无法自动填充的字段
|
boolean needBatchInput = false;
|
if (Boolean.TRUE.equals(config.getExpireDateFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getReceiptDateFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getVendorFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getClientFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getSalesOrderCodeFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getToolFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getMoldFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getLotNumberFlag())) needBatchInput = true;
|
if (Boolean.TRUE.equals(config.getQualityStatusFlag())) needBatchInput = true;
|
respVO.setNeedBatchInput(needBatchInput);
|
|
return respVO;
|
}
|
|
}
|