| | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService; |
| | |
| | | import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | |
| | | import java.util.Objects; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcImportExcelVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcImportRespVO; |
| | |
| | | private MesQcIpqcMapper ipqcMapper; |
| | | |
| | | @Resource |
| | | private MesWmProductProduceLineMapper produceLineMapper; |
| | | @Resource |
| | | private MesProFeedbackMapper feedbackMapper; |
| | | |
| | | @Resource |
| | | private MesQcTemplateItemService templateItemService; |
| | | @Resource |
| | | private MesQcIpqcLineService ipqcLineService; |
| | |
| | | private MesQcIndicatorResultService indicatorResultService; |
| | | @Resource |
| | | @Lazy |
| | | private MesWmProductProduceLineService productProduceLineService; |
| | | |
| | | @Resource |
| | | @Lazy |
| | | private MesQcNcrService ncrService; |
| | | |
| | | @Resource |
| | |
| | | SaveDataContext ctx = validateIpqcSaveData(createReqVO); |
| | | // 1.2 校验工单存在 |
| | | MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(createReqVO.getWorkOrderId()); |
| | | // 1.3 确定检验物料:有任务时取任务的物料,无任务时取工单的产品 |
| | | Long itemId = (ctx.task() != null) ? ctx.task().getItemId() : workOrder.getProductId(); |
| | | // 1.4 根据物料 + 检验类型自动匹配模板 |
| | | MesQcTemplateItemDO templateItem = templateItemService.getRequiredTemplateByItemIdAndType( |
| | | itemId, MesQcTypeEnum.IPQC.getType()); |
| | | Long templateId = templateItem.getTemplateId(); |
| | | // 1.5 获取来源单据编号 |
| | | String sourceDocCode = validateAndGetSourceDocCode( |
| | | // 1.3 获取来源单据编号,并确定本次检验的物料与工序 |
| | | // 报工来源:一个报工可能产出多个物料,每个产出行各自检验,物料取产出行、工序取报工工序 |
| | | // 其它来源:有任务时取任务的物料,无任务时取工单的产品 |
| | | SourceDocInfo sourceDoc = validateAndGetSourceDoc( |
| | | createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId()); |
| | | Long itemId; |
| | | Long processId; |
| | | if (Objects.equals(createReqVO.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)) { |
| | | MesProFeedbackDO feedback = feedbackService.validateFeedbackExists(createReqVO.getSourceDocId()); |
| | | if (!Objects.equals(feedback.getWorkOrderId(), createReqVO.getWorkOrderId()) |
| | | || !Objects.equals(feedback.getTaskId(), createReqVO.getTaskId())) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG); |
| | | } |
| | | if (!ipqcMapper.selectBySourceLineForUpdate(MesBizTypeConstants.PRO_FEEDBACK, |
| | | createReqVO.getSourceLineId()).isEmpty()) { |
| | | throw exception(QC_IPQC_SOURCE_ALREADY_LINKED); |
| | | } |
| | | if (sourceDoc.line() == null || sourceDoc.line().getItemId() == null) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG); |
| | | } |
| | | itemId = sourceDoc.line().getItemId(); |
| | | processId = feedback.getProcessId(); |
| | | } else { |
| | | itemId = (ctx.task() != null) ? ctx.task().getItemId() : workOrder.getProductId(); |
| | | processId = getProcessId(itemId, ctx.workstation().getProcessId()); |
| | | } |
| | | // 1.4 根据物料 + 检验类型自动匹配模板(缺失时给出物料维度的明确提示) |
| | | MesQcTemplateItemDO templateItem; |
| | | try { |
| | | templateItem = templateItemService.getRequiredTemplateByItemIdAndType( |
| | | itemId, MesQcTypeEnum.IPQC.getType()); |
| | | } catch (ServiceException e) { |
| | | if (!Objects.equals(e.getCode(), QC_NO_TEMPLATE.getCode())) { |
| | | throw e; |
| | | } |
| | | throw exception(QC_IPQC_NO_TEMPLATE_FOR_ITEM, getItemInfo(itemId)); |
| | | } |
| | | Long templateId = templateItem.getTemplateId(); |
| | | String sourceDocCode = sourceDoc.code(); |
| | | |
| | | // 2. 插入主表 |
| | | MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class) |
| | | .setItemId(itemId) |
| | | .setProcessId(getProcessId(itemId, ctx.workstation().getProcessId())) |
| | | .setProcessId(processId) |
| | | .setTemplateId(templateId) |
| | | .setSourceDocCode(sourceDocCode) |
| | | .setStatus(MesQcStatusEnum.DRAFT.getStatus()); |
| | | // 工废/料废/其他废品、检测人员、检测日期未填写时使用默认值,不影响流程 |
| | | if (ipqc.getLaborScrapQuantity() == null) { |
| | | ipqc.setLaborScrapQuantity(BigDecimal.ZERO); |
| | | } |
| | | if (ipqc.getMaterialScrapQuantity() == null) { |
| | | ipqc.setMaterialScrapQuantity(BigDecimal.ZERO); |
| | | } |
| | | if (ipqc.getOtherScrapQuantity() == null) { |
| | | ipqc.setOtherScrapQuantity(BigDecimal.ZERO); |
| | | } |
| | | if (ipqc.getInspectorUserId() == null) { |
| | | ipqc.setInspectorUserId(getLoginUserId()); |
| | | } |
| | | if (ipqc.getInspectDate() == null) { |
| | | ipqc.setInspectDate(LocalDateTime.now()); |
| | | } |
| | | ipqcMapper.insert(ipqc); |
| | | |
| | | // 3. 从模板指标自动生成检验行 |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateIpqc(MesQcIpqcSaveReqVO updateReqVO) { |
| | | // 1.1 校验存在 + 草稿状态 |
| | | validateIpqcStatusPrepare(updateReqVO.getId()); |
| | | validateIpqcStatusPrepareForUpdate(updateReqVO.getId()); |
| | | // 1.2 校验数据 |
| | | SaveDataContext ctx = validateIpqcSaveData(updateReqVO); |
| | | |
| | | // 2. 更新主表 |
| | | MesQcIpqcDO existIpqc = validateIpqcExists(updateReqVO.getId()); |
| | | if (Objects.equals(existIpqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK) |
| | | && (!Objects.equals(existIpqc.getTaskId(), updateReqVO.getTaskId()) |
| | | || !Objects.equals(existIpqc.getWorkOrderId(), updateReqVO.getWorkOrderId()) |
| | | || !Objects.equals(existIpqc.getWorkstationId(), updateReqVO.getWorkstationId()))) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG); |
| | | } |
| | | MesQcIpqcDO updateObj = BeanUtils.toBean(updateReqVO, MesQcIpqcDO.class); |
| | | updateObj.setSourceDocType(null).setSourceDocId(null).setSourceLineId(null); // 不允许修改来源单据 |
| | | updateObj.setTemplateId(null); // 不允许修改模板 |
| | | updateObj.setItemId(null); // 不允许修改物料 |
| | | updateObj.setWorkOrderId(null); // 不允许修改工单 |
| | | updateObj.setProcessId(getProcessId(existIpqc.getItemId(), ctx.workstation().getProcessId())); |
| | | // 报工来源的工序已在创建时按报工工序确定,更新时保持不变,避免被路线推导成空值 |
| | | if (Objects.equals(existIpqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)) { |
| | | updateObj.setProcessId(existIpqc.getProcessId()); |
| | | } else { |
| | | updateObj.setProcessId(getProcessId(existIpqc.getItemId(), ctx.workstation().getProcessId())); |
| | | } |
| | | ipqcMapper.updateById(updateObj); |
| | | } |
| | | |
| | |
| | | validateIpqcCodeUnique(reqVO.getId(), reqVO.getCode()); |
| | | // 校验工位、检测人员存在 |
| | | MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId()); |
| | | adminUserApi.validateUser(reqVO.getInspectorUserId()); |
| | | // 校验检测人员存在(非必填,未填写时由创建逻辑默认当前登录人) |
| | | if (reqVO.getInspectorUserId() != null) { |
| | | adminUserApi.validateUser(reqVO.getInspectorUserId()); |
| | | } |
| | | // 校验工单存在 |
| | | MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()); |
| | | if (reqVO.getProcessId() != null && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) { |
| | |
| | | task = taskService.validateTaskNotFinished(reqVO.getTaskId()); |
| | | validateTaskRelation(task, workstation, workOrder, reqVO); |
| | | } else { |
| | | // 无任务时,itemId 仍需与工单产品一致 |
| | | if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) { |
| | | // 无任务时,itemId 仍需与工单产品一致(报工来源的联产出物料以产出明细行为准,不在此校验) |
| | | if (!Objects.equals(reqVO.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK) |
| | | && reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) { |
| | | throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH); |
| | | } |
| | | } |
| | |
| | | throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH); |
| | | } |
| | | // 校验检验物料与任务物料一致(有任务时,IPQC 检验的物料应与任务加工的物料一致) |
| | | if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), task.getItemId())) { |
| | | // 报工来源允许检验联产出物料,物料以产出明细行为准,此处不参与校验 |
| | | if (!Objects.equals(reqVO.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK) |
| | | && reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), task.getItemId())) { |
| | | throw exception(PRO_TASK_ITEM_MISMATCH); |
| | | } |
| | | } |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void finishIpqc(Long id) { |
| | | // 1.1 校验存在 + 草稿状态 |
| | | MesQcIpqcDO ipqc = validateIpqcStatusPrepare(id); |
| | | MesQcIpqcDO ipqc = validateIpqcStatusPrepareForUpdate(id); |
| | | // 1.2 校验检测结论必填 |
| | | if (ipqc.getCheckResult() == null) { |
| | | throw exception(QC_IPQC_CHECK_RESULT_EMPTY); |
| | | } |
| | | // 1.3 校验至少存在一条检测结果 |
| | | indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.IPQC.getType()); |
| | | |
| | | BigDecimal check = ipqc.getCheckQuantity(); |
| | | BigDecimal qualified = ipqc.getQualifiedQuantity(); |
| | | BigDecimal unqualified = ipqc.getUnqualifiedQuantity(); |
| | | if (check == null || qualified == null || unqualified == null || check.signum() <= 0 |
| | | || qualified.signum() < 0 || unqualified.signum() < 0 |
| | | || check.compareTo(qualified.add(unqualified)) != 0) { |
| | | throw exception(QC_IPQC_QUANTITY_INVALID); |
| | | } |
| | | BigDecimal labor = ObjectUtil.defaultIfNull(ipqc.getLaborScrapQuantity(), BigDecimal.ZERO); |
| | | BigDecimal material = ObjectUtil.defaultIfNull(ipqc.getMaterialScrapQuantity(), BigDecimal.ZERO); |
| | | BigDecimal other = ObjectUtil.defaultIfNull(ipqc.getOtherScrapQuantity(), BigDecimal.ZERO); |
| | | if (labor.signum() < 0 || material.signum() < 0 || other.signum() < 0 |
| | | || labor.add(material).add(other).compareTo(unqualified) > 0) { |
| | | throw exception(QC_IPQC_SCRAP_QUANTITY_INVALID); |
| | | } |
| | | if (Objects.equals(ipqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)) { |
| | | if (ipqc.getSourceDocId() == null || ipqc.getSourceLineId() == null) throw exception(QC_IPQC_SOURCE_INCOMPLETE); |
| | | MesWmProductProduceLineDO line = produceLineMapper.selectByIdForUpdate(ipqc.getSourceLineId()); |
| | | if (line == null || !Objects.equals(line.getFeedbackId(), ipqc.getSourceDocId())) throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG); |
| | | if (!Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())) throw exception(QC_IPQC_SOURCE_LINE_NOT_PENDING); |
| | | if (line.getQuantity() == null || check.compareTo(line.getQuantity()) != 0) throw exception(QC_IPQC_SOURCE_QUANTITY_INVALID); |
| | | } |
| | | |
| | | // 2. 更新状态为已完成 |
| | | ipqcMapper.updateById(new MesQcIpqcDO() |
| | |
| | | Long sourceDocId = ipqc.getSourceDocId(); |
| | | Long sourceLineId = ipqc.getSourceLineId(); |
| | | |
| | | // 如果 IPQC 单没有设置来源,尝试通过 taskId 查找关联的待检验报工 |
| | | if (sourceDocId == null && ipqc.getTaskId() != null) { |
| | | MesProFeedbackDO feedback = feedbackService.getFeedbackListByTaskId(ipqc.getTaskId()).stream() |
| | | .filter(f -> MesProFeedbackStatusEnum.UNCHECK.getStatus().equals(f.getStatus())) |
| | | .findFirst().orElse(null); |
| | | if (feedback != null) { |
| | | sourceDocId = feedback.getId(); |
| | | log.info("[writeBackSourceDoc] IPQC({}) 未设置来源单据,通过 taskId({}) 找到待检验报工({})", |
| | | ipqc.getId(), ipqc.getTaskId(), feedback.getId()); |
| | | } |
| | | } |
| | | |
| | | if (sourceDocId == null) { |
| | | log.warn("[writeBackSourceDoc] IPQC({}) 无法确定来源报工,跳过回写", ipqc.getId()); |
| | | if (ipqc.getSourceDocType() == null && sourceDocId == null && sourceLineId == null) { |
| | | return; |
| | | } |
| | | if (!Objects.equals(ipqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK) |
| | | || sourceDocId == null || sourceLineId == null) throw exception(QC_IPQC_SOURCE_INCOMPLETE); |
| | | |
| | | // 2. 回写报工 |
| | | feedbackService.updateProFeedbackWhenIpqcFinish(sourceDocId, sourceLineId, |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteIpqc(Long id) { |
| | | // 1. 校验存在 + 草稿状态 |
| | | validateIpqcStatusPrepare(id); |
| | | validateIpqcStatusPrepareForUpdate(id); |
| | | |
| | | // 2.1 删除主表 |
| | | ipqcMapper.deleteById(id); |
| | |
| | | return ipqc; |
| | | } |
| | | |
| | | /** |
| | | * 校验过程检验单存在且为草稿状态 |
| | | * |
| | | * @param id 过程检验单 ID |
| | | * @return 过程检验单 |
| | | */ |
| | | private MesQcIpqcDO validateIpqcStatusPrepare(Long id) { |
| | | MesQcIpqcDO ipqc = validateIpqcExists(id); |
| | | if (ObjUtil.notEqual(ipqc.getStatus(), MesQcStatusEnum.DRAFT.getStatus())) { |
| | | throw exception(QC_IPQC_NOT_PREPARE); |
| | | private MesQcIpqcDO validateIpqcStatusPrepareForUpdate(Long id) { |
| | | // 与报工回调一致:报工 → 产出行 → 检验单,避免相互等待。 |
| | | MesQcIpqcDO snapshot = validateIpqcExists(id); |
| | | if (Objects.equals(snapshot.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK) |
| | | && snapshot.getSourceDocId() != null && snapshot.getSourceLineId() != null) { |
| | | lockFeedback(snapshot.getSourceDocId()); |
| | | produceLineMapper.selectByIdForUpdate(snapshot.getSourceLineId()); |
| | | } |
| | | MesQcIpqcDO ipqc = ipqcMapper.selectByIdForUpdate(id); |
| | | if (ipqc == null) throw exception(QC_IPQC_NOT_EXISTS); |
| | | if (ObjUtil.notEqual(ipqc.getStatus(), MesQcStatusEnum.DRAFT.getStatus())) throw exception(QC_IPQC_NOT_PREPARE); |
| | | return ipqc; |
| | | } |
| | | |
| | | private MesProFeedbackDO lockFeedback(Long id) { |
| | | MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>() |
| | | .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE")); |
| | | if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS); |
| | | return feedback; |
| | | } |
| | | |
| | | private void validateIpqcCodeUnique(Long id, String code) { |
| | |
| | | } |
| | | } |
| | | |
| | | private String validateAndGetSourceDocCode(Integer sourceDocType, Long sourceDocId, Long sourceLineId) { |
| | | if (sourceDocType == null || sourceDocId == null) { |
| | | return null; |
| | | /** |
| | | * 来源单据校验上下文 |
| | | * |
| | | * @param code 来源单据编号 |
| | | * @param line 报工来源时对应的产出明细行,其它来源为 null |
| | | */ |
| | | private record SourceDocInfo(String code, MesWmProductProduceLineDO line) {} |
| | | |
| | | private SourceDocInfo validateAndGetSourceDoc(Integer sourceDocType, Long sourceDocId, Long sourceLineId) { |
| | | if (sourceDocType == null && sourceDocId == null && sourceLineId == null) { |
| | | return new SourceDocInfo(null, null); |
| | | } |
| | | if (sourceDocType == null || sourceDocId == null || sourceLineId == null) throw exception(QC_IPQC_SOURCE_INCOMPLETE); |
| | | if (Objects.equals(sourceDocType, MesBizTypeConstants.PRO_FEEDBACK)) { |
| | | MesProFeedbackDO feedback = feedbackService.validateFeedbackExists(sourceDocId); |
| | | MesProFeedbackDO feedback = lockFeedback(sourceDocId); |
| | | // 校验 sourceLineId 必填 |
| | | if (sourceLineId == null) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_REQUIRED); |
| | | } |
| | | // 校验 sourceLineId 存在,且属于该报工的产出行 |
| | | MesWmProductProduceLineDO targetLine = productProduceLineService |
| | | .validateProductProduceLineExists(sourceLineId); |
| | | MesWmProductProduceLineDO targetLine = produceLineMapper.selectByIdForUpdate(sourceLineId); |
| | | if (targetLine == null) throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG); |
| | | if (ObjUtil.notEqual(targetLine.getFeedbackId(), sourceDocId)) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG); |
| | | } |
| | |
| | | if (ObjUtil.notEqual(targetLine.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_NOT_PENDING); |
| | | } |
| | | return feedback.getCode(); |
| | | return new SourceDocInfo(feedback.getCode(), targetLine); |
| | | } |
| | | // 未知来源类型应报错,而不是静默忽略 |
| | | throw exception(QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN); |
| | |
| | | + String.format("%03d", seq.incrementAndGet()); |
| | | } |
| | | |
| | | private String getItemInfo(Long itemId) { |
| | | if (itemId == null) { |
| | | return "未知物料"; |
| | | } |
| | | MesMdItemDO item = itemService.getItem(itemId); |
| | | if (item != null) { |
| | | return item.getCode() + "-" + item.getName(); |
| | | } |
| | | return "物料ID:" + itemId; |
| | | } |
| | | |
| | | } |