package com.ruoyi.quality.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum; import com.ruoyi.common.utils.HackLoopTableRenderPolicy; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.approve.pojo.ApproveProcess; import com.ruoyi.approve.service.IApproveProcessService; import com.ruoyi.approve.vo.ApproveProcessVO; import com.ruoyi.common.enums.ApproveTypeEnum; import com.ruoyi.procurementrecord.service.ProcurementRecordService; import com.ruoyi.procurementrecord.utils.StockUtils; import com.ruoyi.quality.dto.QualityInspectDto; import com.ruoyi.quality.mapper.QualityInspectMapper; import com.ruoyi.quality.mapper.QualityTestStandardMapper; import com.ruoyi.quality.mapper.QualityUnqualifiedMapper; import com.ruoyi.quality.pojo.QualityInspect; import com.ruoyi.quality.pojo.QualityInspectParam; import com.ruoyi.quality.pojo.QualityUnqualified; import com.ruoyi.quality.service.IQualityInspectParamService; import com.ruoyi.quality.service.IQualityInspectService; import com.ruoyi.purchase.mapper.PurchaseLedgerMapper; import com.ruoyi.purchase.pojo.PurchaseLedger; import com.ruoyi.sales.mapper.SalesLedgerProductMapper; import com.ruoyi.sales.pojo.SalesLedgerProduct; import com.ruoyi.framework.security.LoginUser; import lombok.AllArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; import java.io.OutputStream; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @AllArgsConstructor @Service @Transactional(rollbackFor = Exception.class) public class QualityInspectServiceImpl extends ServiceImpl implements IQualityInspectService { private final StockUtils stockUtils; private QualityInspectMapper qualityInspectMapper; private IQualityInspectParamService qualityInspectParamService; private QualityTestStandardMapper qualityTestStandardMapper; private QualityUnqualifiedMapper qualityUnqualifiedMapper; private SalesLedgerProductMapper salesLedgerProductMapper; private PurchaseLedgerMapper purchaseLedgerMapper; private ProcurementRecordService procurementRecordService; private IApproveProcessService approveProcessService; @Override public int add(QualityInspectDto qualityInspectDto) { QualityInspect qualityInspect = new QualityInspect(); BeanUtils.copyProperties(qualityInspectDto, qualityInspect); qualityInspect.setInspectState(0);//默认未提交 ensureQualifiedSplitDefaults(qualityInspect); // 前端若已传合格/不合格数量,defaults 不会写 pass_rate,这里补算列表展示用合格率 refreshPassRateFromQuantities(qualityInspect); qualityInspectMapper.insert(qualityInspect); for (QualityInspectParam qualityInspectParam : qualityInspectDto.getQualityInspectParams()) { qualityInspectParam.setInspectId(qualityInspect.getId()); } qualityInspectParamService.saveBatch(qualityInspectDto.getQualityInspectParams()); return 0; } @Override public QualityInspectDto getDetailById(Integer id) { QualityInspect qualityInspect = qualityInspectMapper.selectById(id); List qualityInspectParams = qualityInspectParamService.list(Wrappers.lambdaQuery().eq(QualityInspectParam::getInspectId, id)); QualityInspectDto qualityInspectDto = new QualityInspectDto(); BeanUtils.copyProperties(qualityInspect, qualityInspectDto); qualityInspectDto.setQualityInspectParams(qualityInspectParams); return qualityInspectDto; } //提交 @Override public int submit(QualityInspect inspect) { QualityInspect qualityInspect = qualityInspectMapper.selectById(inspect.getId()); if (qualityInspect == null) { throw new RuntimeException("质检单不存在"); } if (Objects.equals(qualityInspect.getInspectState(), 1)) { throw new RuntimeException("该质检单已提交,不能重复提交"); } if (inspect != null) { if (inspect.getQualifiedQuantity() != null) { qualityInspect.setQualifiedQuantity(inspect.getQualifiedQuantity()); } if (inspect.getUnqualifiedQuantity() != null) { qualityInspect.setUnqualifiedQuantity(inspect.getUnqualifiedQuantity()); } } validateAndCalculateQuantities(qualityInspect); BigDecimal qualifiedQty = qualityInspect.getQualifiedQuantity(); BigDecimal unqualifiedQty = qualityInspect.getUnqualifiedQuantity(); if (unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) { QualityUnqualified qualityUnqualified = new QualityUnqualified(); BeanUtils.copyProperties(qualityInspect, qualityUnqualified); qualityUnqualified.setId(null); qualityUnqualified.setQuantity(unqualifiedQty); qualityUnqualified.setInspectState(0); qualityUnqualified.setDefectivePhenomena(buildDefectivePhenomena(qualityInspect)); qualityUnqualified.setInspectId(qualityInspect.getId()); qualityUnqualifiedMapper.insert(qualityUnqualified); } if (qualifiedQty.compareTo(BigDecimal.ZERO) > 0) { if (Objects.equals(qualityInspect.getInspectType(), 0)) { Long ledgerId = qualityInspect.getPurchaseLedgerId(); PurchaseLedger purchaseLedger = ledgerId == null ? null : purchaseLedgerMapper.selectById(ledgerId); if (purchaseLedger != null) { submitQualifiedInboundApprove(qualityInspect); } else { // 手动新增的原材料检验:无采购台账,不走采购入库审批,直接入合格库存(与过程/出厂检验一致) stockUtils.addStock( null, null, qualityInspect.getProductModelId(), qualifiedQty, StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId() ); syncQualifiedInboundToPurchaseProducts(qualityInspect, qualifiedQty); } } else { stockUtils.addStock( qualityInspect.getPurchaseLedgerId() == null ? null : qualityInspect.getPurchaseLedgerId(), null, qualityInspect.getProductModelId(), qualifiedQty, StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId() ); syncQualifiedInboundToPurchaseProducts(qualityInspect, qualifiedQty); } } qualityInspect.setCheckResult(resolveCheckResult(qualifiedQty, unqualifiedQty)); qualityInspect.setInspectState(1); int updated = qualityInspectMapper.updateById(qualityInspect); refreshPurchaseLedgerStockStatusByInspect(qualityInspect.getPurchaseLedgerId()); return updated; } private void validateAndCalculateQuantities(QualityInspect qualityInspect) { if (qualityInspect.getQualifiedQuantity() == null || qualityInspect.getUnqualifiedQuantity() == null) { throw new RuntimeException("请填写合格数量和不合格数量"); } if (qualityInspect.getQualifiedQuantity().compareTo(BigDecimal.ZERO) < 0 || qualityInspect.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) < 0) { throw new RuntimeException("合格数量和不合格数量不能为负数"); } if (qualityInspect.getQuantity() == null) { throw new RuntimeException("质检单总数量异常"); } BigDecimal total = qualityInspect.getQualifiedQuantity().add(qualityInspect.getUnqualifiedQuantity()); BigDecimal qtyScaled = qualityInspect.getQuantity().setScale(4, RoundingMode.HALF_UP); BigDecimal sumScaled = total.setScale(4, RoundingMode.HALF_UP); if (sumScaled.compareTo(qtyScaled) > 0) { throw new RuntimeException("合格数量与不合格数量之和不能超过总数量"); } qualityInspect.setPassRate(calculatePassRate(qualityInspect.getQualifiedQuantity(), qualityInspect.getQuantity())); } /** * 新增时若未拆分合格/不合格,默认全部为待检合格数 */ private void ensureQualifiedSplitDefaults(QualityInspect q) { if (q.getQuantity() == null) { return; } if (q.getQualifiedQuantity() == null && q.getUnqualifiedQuantity() == null) { q.setQualifiedQuantity(q.getQuantity()); q.setUnqualifiedQuantity(BigDecimal.ZERO); q.setPassRate(calculatePassRate(q.getQualifiedQuantity(), q.getQuantity())); if (q.getCheckResult() == null || q.getCheckResult().isEmpty()) { q.setCheckResult("合格"); } } } private BigDecimal calculatePassRate(BigDecimal qualifiedQty, BigDecimal totalQty) { if (totalQty == null || totalQty.compareTo(BigDecimal.ZERO) <= 0 || qualifiedQty == null) { return BigDecimal.ZERO; } return qualifiedQty.multiply(BigDecimal.valueOf(100)) .divide(totalQty, 2, RoundingMode.HALF_UP); } private void refreshPassRateFromQuantities(QualityInspect q) { if (q.getQuantity() == null || q.getQualifiedQuantity() == null) { return; } q.setPassRate(calculatePassRate(q.getQualifiedQuantity(), q.getQuantity())); } private String resolveCheckResult(BigDecimal qualifiedQty, BigDecimal unqualifiedQty) { if (unqualifiedQty.compareTo(BigDecimal.ZERO) <= 0) { return "合格"; } if (qualifiedQty.compareTo(BigDecimal.ZERO) <= 0) { return "不合格"; } return "部分合格"; } private String buildDefectivePhenomena(QualityInspect qualityInspect) { if (ObjectUtils.isNotEmpty(qualityInspect.getDefectivePhenomena())) { return qualityInspect.getDefectivePhenomena(); } List inspectParams = qualityInspectParamService.list( Wrappers.lambdaQuery().eq(QualityInspectParam::getInspectId, qualityInspect.getId())); if (inspectParams.isEmpty()) { return "质检不合格数量:" + qualityInspect.getUnqualifiedQuantity(); } String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(",")); return text + "等指标检验不合格,不合格数量:" + qualityInspect.getUnqualifiedQuantity(); } private void submitQualifiedInboundApprove(QualityInspect qualityInspect) { PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(qualityInspect.getPurchaseLedgerId()); if (purchaseLedger == null) { throw new RuntimeException("提交失败,采购台账不存在"); } String approveUserIds = ObjectUtils.isNotEmpty(qualityInspect.getApproveUserIds()) ? qualityInspect.getApproveUserIds() : purchaseLedger.getApproveUserIds(); if (ObjectUtils.isEmpty(approveUserIds)) { throw new RuntimeException("提交失败,请先维护质检单审批人(或采购台账审批人)"); } String approveReason = "原材料质检入库审批:" + purchaseLedger.getPurchaseContractNumber(); String approveRemark = "qualityQualifiedInbound:" + qualityInspect.getId() + ":" + purchaseLedger.getId(); ApproveProcess exist = approveProcessService.getOne(new LambdaQueryWrapper() .eq(ApproveProcess::getApproveType, ApproveTypeEnum.STOCK_IN.getCode()) .eq(ApproveProcess::getApproveRemark, approveRemark) .eq(ApproveProcess::getApproveDelete, 0) .orderByDesc(ApproveProcess::getCreateTime) .last("limit 1")); if (exist != null && !Objects.equals(exist.getApproveStatus(), 3)) { throw new RuntimeException("提交失败,该质检单已发起入库审批,审批完成前不能重复提交"); } LoginUser loginUser = SecurityUtils.getLoginUser(); ApproveProcessVO approveProcessVO = new ApproveProcessVO(); approveProcessVO.setApproveType(ApproveTypeEnum.STOCK_IN.getCode()); approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId()); approveProcessVO.setApproveReason(approveReason); approveProcessVO.setApproveRemark(approveRemark); approveProcessVO.setApproveUserIds(approveUserIds); approveProcessVO.setApproveUser(loginUser.getUserId()); approveProcessVO.setApproveTime(java.time.LocalDate.now().toString()); try { approveProcessService.addApprove(approveProcessVO); } catch (Exception e) { throw new RuntimeException("提交失败,入库审批发起异常:" + e.getMessage()); } qualityInspect.setApprovalStatus(2); qualityInspect.setApproveUserIds(approveUserIds); qualityInspectMapper.updateById(qualityInspect); } @Override public void executeQualifiedInboundApproval(Integer inspectId) { if (inspectId == null) { throw new RuntimeException("审批失败,质检单ID不能为空"); } QualityInspect qualityInspect = qualityInspectMapper.selectById(inspectId); if (qualityInspect == null) { throw new RuntimeException("审批失败,质检单不存在"); } if (!Objects.equals(qualityInspect.getInspectType(), 0)) { throw new RuntimeException("审批失败,仅原材料检验支持入库审批"); } if (!Objects.equals(qualityInspect.getInspectState(), 1)) { throw new RuntimeException("审批失败,当前质检单状态不允许入库"); } BigDecimal qualifiedQty = qualityInspect.getQualifiedQuantity(); if (qualifiedQty == null || qualifiedQty.compareTo(BigDecimal.ZERO) <= 0) { throw new RuntimeException("审批失败,无合格数量可入库"); } stockUtils.addStock( qualityInspect.getPurchaseLedgerId() == null ? null : qualityInspect.getPurchaseLedgerId().longValue(), null, qualityInspect.getProductModelId(), qualifiedQty, StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId() ); syncQualifiedInboundToPurchaseProducts(qualityInspect, qualifiedQty); refreshPurchaseLedgerStockStatusByInspect(qualityInspect.getPurchaseLedgerId()); qualityInspect.setApprovalStatus(3); qualityInspectMapper.updateById(qualityInspect); } @Override public void markQualifiedInboundApprovalStatus(Integer inspectId, Integer approvalStatus) { if (inspectId == null || approvalStatus == null) { return; } QualityInspect qualityInspect = qualityInspectMapper.selectById(inspectId); if (qualityInspect == null) { return; } qualityInspect.setApprovalStatus(approvalStatus); qualityInspectMapper.updateById(qualityInspect); } /*生成检验报告*/ @Override public void down(HttpServletResponse response, QualityInspect qualityInspect) { QualityInspect inspect = qualityInspectMapper.selectById(qualityInspect.getId()); String inspectType = ""; switch (inspect.getInspectType()) { case 0: inspectType = "原材料检验"; break; case 1: inspectType = "过程检验"; break; case 2: inspectType = "出厂检验"; break; } List paramList = qualityInspectParamService.list(Wrappers.lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId())); int index = 1; for (QualityInspectParam detail : paramList) { detail.setIndex(index); index++; } InputStream inputStream = this.getClass().getResourceAsStream("/static/report-template.docx"); Configure configure = Configure.builder() .bind("paramList", new HackLoopTableRenderPolicy()) .build(); String finalInspectType = inspectType; XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render( new HashMap() {{ put("inspect", inspect); put("inspectType", finalInspectType); put("paramList", paramList); }}); try { response.setContentType("application/msword"); String fileName = URLEncoder.encode( "检验报告", "UTF-8"); response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".docx"); OutputStream os = response.getOutputStream(); template.write(os); os.flush(); os.close(); inputStream.close(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("导出失败"); } } @Override public int updateQualityInspect(QualityInspectDto qualityInspectDto) { QualityInspect existing = qualityInspectMapper.selectById(qualityInspectDto.getId()); if (existing == null) { throw new RuntimeException("质检单不存在"); } if (Objects.equals(existing.getInspectState(), 1)) { throw new RuntimeException("已提交的数据不允许修改"); } if (ObjectUtils.isNotNull(qualityInspectDto.getQualityInspectParams())) { qualityInspectParamService.remove(Wrappers.lambdaQuery().eq(QualityInspectParam::getInspectId, qualityInspectDto.getId())); for (QualityInspectParam qualityInspectParam : qualityInspectDto.getQualityInspectParams()) { qualityInspectParam.setInspectId(qualityInspectDto.getId()); } qualityInspectParamService.saveBatch(qualityInspectDto.getQualityInspectParams()); } QualityInspect qualityInspect = new QualityInspect(); BeanUtils.copyProperties(qualityInspectDto, qualityInspect); qualityInspect.setQuantity(existing.getQuantity()); BigDecimal qf = qualityInspect.getQualifiedQuantity() != null ? qualityInspect.getQualifiedQuantity() : existing.getQualifiedQuantity(); BigDecimal uqf = qualityInspect.getUnqualifiedQuantity() != null ? qualityInspect.getUnqualifiedQuantity() : existing.getUnqualifiedQuantity(); if (qf == null || uqf == null) { BigDecimal qty = existing.getQuantity() != null ? existing.getQuantity() : BigDecimal.ZERO; if ("不合格".equals(existing.getCheckResult())) { qf = BigDecimal.ZERO; uqf = qty; } else { qf = qty; uqf = BigDecimal.ZERO; } } qualityInspect.setQualifiedQuantity(qf); qualityInspect.setUnqualifiedQuantity(uqf); validateAndCalculateQuantities(qualityInspect); qualityInspect.setCheckResult(resolveCheckResult(qf, uqf)); return qualityInspectMapper.updateById(qualityInspect); } @Override public IPage qualityInspectListPage(Page page, QualityInspect qualityInspect) { return qualityInspectMapper.qualityInspectListPage(page, qualityInspect); } @Override public void qualityInspectExport(HttpServletResponse response, QualityInspect qualityInspect) { List qualityInspects = qualityInspectMapper.qualityInspectExport(qualityInspect); ExcelUtil util = new ExcelUtil(QualityInspect.class); switch (qualityInspect.getInspectType()) { case 0: util.exportExcel(response, qualityInspects, "原材料检验导出"); break; case 1: util.exportExcel(response, qualityInspects, "过程检验导出"); break; case 2: util.exportExcel(response, qualityInspects, "出厂检验导出"); break; } } private void refreshPurchaseLedgerStockStatusByInspect(Long purchaseLedgerId) { if (purchaseLedgerId == null) { return; } List products = salesLedgerProductMapper.selectList(new LambdaQueryWrapper() .eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedgerId) .eq(SalesLedgerProduct::getType, 2)); if (products == null || products.isEmpty()) { return; } boolean allInbound = true; boolean anyInbound = false; for (SalesLedgerProduct product : products) { BigDecimal orderQty = product.getQuantity() == null ? BigDecimal.ZERO : product.getQuantity(); BigDecimal totalInboundQty = product.getStockedQuantity() == null ? BigDecimal.ZERO : product.getStockedQuantity(); if (totalInboundQty.compareTo(BigDecimal.ZERO) > 0) { anyInbound = true; } if (totalInboundQty.compareTo(orderQty) < 0) { allInbound = false; } } PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(purchaseLedgerId); if (purchaseLedger == null) { return; } int targetStockStatus = allInbound ? 2 : (anyInbound ? 1 : 0); if (!Objects.equals(purchaseLedger.getStockStatus(), targetStockStatus)) { purchaseLedger.setStockStatus(targetStockStatus); purchaseLedgerMapper.updateById(purchaseLedger); } } private void syncQualifiedInboundToPurchaseProducts(QualityInspect qualityInspect, BigDecimal inboundQty) { if (qualityInspect == null) { return; } if (!Objects.equals(qualityInspect.getInspectType(), 0) || qualityInspect.getPurchaseLedgerId() == null) { return; } if (qualityInspect.getProductModelId() == null || inboundQty == null) { return; } if (inboundQty.compareTo(BigDecimal.ZERO) <= 0) { return; } List lines = salesLedgerProductMapper.selectList(new LambdaQueryWrapper() .eq(SalesLedgerProduct::getSalesLedgerId, qualityInspect.getPurchaseLedgerId()) .eq(SalesLedgerProduct::getType, 2) .eq(SalesLedgerProduct::getProductModelId, qualityInspect.getProductModelId()) .eq(SalesLedgerProduct::getIsChecked, true) .orderByAsc(SalesLedgerProduct::getId)); if (lines == null || lines.isEmpty()) { return; } BigDecimal remaining = inboundQty; SalesLedgerProduct fallbackLine = null; for (SalesLedgerProduct line : lines) { if (remaining.compareTo(BigDecimal.ZERO) <= 0) { break; } BigDecimal orderQty = line.getQuantity() == null ? BigDecimal.ZERO : line.getQuantity(); BigDecimal stocked = line.getStockedQuantity() == null ? BigDecimal.ZERO : line.getStockedQuantity(); BigDecimal canFill = orderQty.subtract(stocked); if (canFill.compareTo(BigDecimal.ZERO) <= 0) { fallbackLine = line; continue; } BigDecimal add = canFill.min(remaining); BigDecimal newStocked = stocked.add(add); int status; if (newStocked.compareTo(BigDecimal.ZERO) <= 0) { status = 0; } else if (orderQty.compareTo(BigDecimal.ZERO) > 0 && newStocked.compareTo(orderQty) < 0) { status = 1; } else { status = 2; } line.setStockedQuantity(newStocked); line.setProductStockStatus(status); line.fillRemainingQuantity(); salesLedgerProductMapper.updateById(line); remaining = remaining.subtract(add); fallbackLine = line; } // 允许多入库:若仍有剩余,累计到最后一行,确保 remaining_shipped_quantity 能同步增长 if (remaining.compareTo(BigDecimal.ZERO) > 0 && fallbackLine != null) { BigDecimal orderQty = fallbackLine.getQuantity() == null ? BigDecimal.ZERO : fallbackLine.getQuantity(); BigDecimal stocked = fallbackLine.getStockedQuantity() == null ? BigDecimal.ZERO : fallbackLine.getStockedQuantity(); BigDecimal newStocked = stocked.add(remaining); int status; if (newStocked.compareTo(BigDecimal.ZERO) <= 0) { status = 0; } else if (orderQty.compareTo(BigDecimal.ZERO) > 0 && newStocked.compareTo(orderQty) < 0) { status = 1; } else { status = 2; } fallbackLine.setStockedQuantity(newStocked); fallbackLine.setProductStockStatus(status); fallbackLine.fillRemainingQuantity(); salesLedgerProductMapper.updateById(fallbackLine); } } }