云
10 天以前 bf5f52656e128f578ed42c7979c31cf4b476ce46
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package cn.iocoder.yudao.module.mes.service.pro.route;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.*;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.product.MesProRouteProductSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.productbom.MesProRouteProductBomSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.*;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.process.MesProProcessMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.*;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.warehouse.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
/** 完整模板按路线分组逐组事务导入,不删除表格中未出现的其他产品或投料。 */
@Service
@Slf4j
public class MesProRouteFullImportService {
    @Resource private MesProRouteMapper routeMapper;
    @Resource private MesProRouteProductMapper productMapper;
    @Resource private MesProRouteProcessMapper processMapper;
    @Resource private MesProRouteProcessOutputMapper outputMapper;
    @Resource private MesProRouteProductBomMapper bomMapper;
    @Resource private MesMdItemMapper itemMapper;
    @Resource private MesMdItemTypeService itemTypeService;
    @Resource private MesProProcessMapper operationMapper;
    @Resource private MesProProcessService operationService;
    @Resource private MesWmWarehouseMapper warehouseMapper;
    @Resource private MesWmWarehouseLocationMapper locationMapper;
    @Resource private MesWmWarehouseAreaMapper areaMapper;
    @Resource private MesProRouteService routeService;
    @Resource private MesProRouteProductService productService;
    @Resource private MesProRouteProcessService processService;
    @Resource private MesProRouteProductBomService bomService;
 
    public MesProRouteImportRespVO importRows(List<MesProRouteFullExcelVO> rows, boolean updateSupport) {
        if (CollUtil.isEmpty(rows)) throw exception(PRO_ROUTE_IMPORT_LIST_IS_EMPTY);
        var response = MesProRouteImportRespVO.builder().createCodes(new ArrayList<>())
                .updateCodes(new ArrayList<>()).failureCodes(new LinkedHashMap<>()).build();
        Map<String, List<MesProRouteFullExcelVO>> groups = new LinkedHashMap<>();
        for (int i = 0; i < rows.size(); i++) {
            var row = rows.get(i);
            if (row == null) continue;
            if (StrUtil.isBlank(row.getRecordType()) && StrUtil.isBlank(row.getRouteCode())) {
                boolean hasContent = cn.hutool.core.bean.BeanUtil.beanToMap(row).values().stream()
                        .anyMatch(v -> v != null && (!(v instanceof String) || StrUtil.isNotBlank((String) v)));
                if (hasContent) response.getFailureCodes().put("第 " + (i + 2) + " 行", "记录类型和路线编码不能为空");
                continue;
            }
            if (StrUtil.isBlank(row.getRouteCode())) {
                response.getFailureCodes().put("第 " + (i + 2) + " 行", "路线编码不能为空"); continue;
            }
            groups.computeIfAbsent(row.getRouteCode().trim(), k -> new ArrayList<>()).add(row);
        }
        // 缺失路线编码的行无法归组,拒绝整个文件以避免关联明细被悄悄遗漏。
        if (!response.getFailureCodes().isEmpty()) return response;
        if (groups.isEmpty()) throw exception(PRO_ROUTE_IMPORT_LIST_IS_EMPTY);
        for (var entry : groups.entrySet()) {
            try {
                boolean created = SpringUtil.getBean(MesProRouteFullImportService.class)
                        .importGroup(entry.getKey(), entry.getValue(), updateSupport);
                (created ? response.getCreateCodes() : response.getUpdateCodes()).add(entry.getKey());
            } catch (ServiceException ex) {
                response.getFailureCodes().put(entry.getKey(), ex.getMessage());
            } catch (Exception ex) {
                log.error("[importRows] 工艺路线完整模板导入失败,路线={}", entry.getKey(), ex);
                response.getFailureCodes().put(entry.getKey(), "导入失败,请检查模板内容或联系管理员");
            }
        }
        return response;
    }
 
    @Transactional(rollbackFor = Exception.class)
    public boolean importGroup(String code, List<MesProRouteFullExcelVO> rows, boolean updateSupport) {
        Map<String, List<MesProRouteFullExcelVO>> types = new LinkedHashMap<>();
        for (var row : rows) {
            String type = StrUtil.trimToEmpty(row.getRecordType());
            if (!Set.of("路线", "产品", "工序", "产出", "投料").contains(type)) fail("记录类型必须为路线、产品、工序、产出或投料");
            types.computeIfAbsent(type, k -> new ArrayList<>()).add(row);
        }
        var headers = types.getOrDefault("路线", List.of());
        if (headers.size() != 1 || StrUtil.isBlank(headers.get(0).getRouteName())) fail("每条路线须有且仅有一条路线记录并填写名称");
        var products = types.getOrDefault("产品", List.of());
        List<MesProRouteFullExcelVO> references = products.stream().filter(p -> yes(p.getTemplateReference(), "模板引用")).toList();
        if (references.size() != 1 || StrUtil.isBlank(references.get(0).getProductCode())) fail("每条路线须有且仅有一个模板引用产品");
        String referenceCode = references.get(0).getProductCode().trim();
        if (MesProRouteFullExcelService.REFERENCE.equals(referenceCode)) fail("产品行须填写实际产品编码,不能填写 @模板产品");
        var steps = types.getOrDefault("工序", List.of());
        if (steps.isEmpty()) fail("至少需要一条工序记录");
        if (steps.stream().anyMatch(p -> StrUtil.isBlank(p.getProcessCode()) || p.getProcessSort() == null))
            fail("工序记录须填写工序编码和工序顺序");
        if (steps.stream().noneMatch(p -> yes(p.getKeyFlag(), "关键工序"))) fail("至少需要一个关键工序");
        var lastStep = steps.stream().max(Comparator.comparing(MesProRouteFullExcelVO::getProcessSort)).orElseThrow();
        boolean hasFinalOutput = types.getOrDefault("产出", List.of()).stream().anyMatch(out ->
                StrUtil.equals(StrUtil.trimToEmpty(out.getProcessCode()), StrUtil.trimToEmpty(lastStep.getProcessCode()))
                && isReference(out.getProductCode(), referenceCode) && isReference(out.getItemCode(), referenceCode));
        if (!hasFinalOutput) fail("末道工序必须有归属到模板产品自身的产出记录");
        if (types.getOrDefault("投料", List.of()).stream().noneMatch(bom -> isReference(bom.getProductCode(), referenceCode)))
            fail("模板产品至少需要一条工序投料记录");
        for (var row : products) if (StrUtil.isBlank(row.getProductCode()) || StrUtil.isBlank(row.getWarehouseCode())
                || StrUtil.isBlank(row.getLocationCode()) || StrUtil.isBlank(row.getAreaCode())) fail("产品记录须填写产品、仓库、库区和库位编码");
 
        MesProRouteDO route = routeMapper.selectByCode(code);
        boolean created = route == null;
        if (!created) {
            if (!updateSupport) fail("路线编码已存在,请勾选更新已有数据");
            route = routeMapper.selectOne(new LambdaQueryWrapperX<MesProRouteDO>().eq(MesProRouteDO::getId, route.getId()).last("FOR UPDATE"));
            if (route == null) fail("路线不存在");
            if (CommonStatusEnum.ENABLE.getStatus().equals(route.getStatus())) fail("路线已启用,请先停用后再导入更新");
        }
        MesProRouteSaveReqVO routeReq = new MesProRouteSaveReqVO();
        routeReq.setCode(code); routeReq.setName(headers.get(0).getRouteName().trim());
        routeReq.setDescription(headers.get(0).getRouteDescription()); routeReq.setRemark(headers.get(0).getRouteRemark());
        Long routeId;
        if (created) routeId = routeService.createRoute(routeReq);
        else { routeId = route.getId(); routeReq.setId(routeId); routeService.updateRoute(routeReq); }
 
        Set<Long> proposedProductIds = new HashSet<>();
        for (var row : products) proposedProductIds.add(item(row.getProductCode(), referenceCode).getId());
        boolean hasOtherProducts = productMapper.selectListByRouteId(routeId).stream()
                .anyMatch(p -> !proposedProductIds.contains(p.getItemId()));
        if (!created && hasOtherProducts && (!Objects.equals(route.getName(), routeReq.getName())
                || !Objects.equals(StrUtil.nullToEmpty(route.getDescription()), StrUtil.nullToEmpty(routeReq.getDescription()))
                || !Objects.equals(StrUtil.nullToEmpty(route.getRemark()), StrUtil.nullToEmpty(routeReq.getRemark()))))
            fail("路线包含其他产品,不能通过单产品模板修改路线基本信息");
        Set<Long> productIds = new HashSet<>(); Long refProductId = null;
        for (var row : products) {
            MesMdItemDO item = item(row.getProductCode(), referenceCode);
            if (!productIds.add(item.getId())) fail("产品重复:" + row.getProductCode());
            var wh = warehouseMapper.selectByCode(row.getWarehouseCode().trim());
            if (wh == null) fail("仓库不存在:" + row.getWarehouseCode());
            var loc = locationMapper.selectByCode(wh.getId(), row.getLocationCode().trim());
            if (loc == null) fail("库区不存在:" + row.getLocationCode());
            var area = areaMapper.selectByCode(loc.getId(), row.getAreaCode().trim());
            if (area == null) fail("库位不存在:" + row.getAreaCode());
            if (row.getQuantity() != null && row.getQuantity() <= 0) fail("生产数量必须大于零");
            if (row.getProductionTime() != null && row.getProductionTime().signum() < 0) fail("生产用时不能为负数");
            MesProRouteProductSaveReqVO req = new MesProRouteProductSaveReqVO();
            req.setRouteId(routeId); req.setItemId(item.getId()); req.setWarehouseId(wh.getId());
            req.setLocationId(loc.getId()); req.setAreaId(area.getId()); req.setQuantity(row.getQuantity());
            req.setProductionTime(row.getProductionTime()); req.setTimeUnitType(timeUnit(row.getTimeUnitType())); req.setRemark(row.getProductRemark());
            MesProRouteProductDO existing = productMapper.selectByItemId(item.getId());
            Long id;
            if (existing == null) id = productService.createRouteProduct(req);
            else {
                if (!existing.getRouteId().equals(routeId)) fail("产品已绑定其他路线:" + item.getCode());
                if (hasOtherProducts && !item.getCode().equals(referenceCode)
                        && (!Objects.equals(existing.getWarehouseId(), req.getWarehouseId())
                        || !Objects.equals(existing.getLocationId(), req.getLocationId())
                        || !Objects.equals(existing.getAreaId(), req.getAreaId())
                        || !Objects.equals(existing.getQuantity(), req.getQuantity())
                        || (existing.getProductionTime() == null ? req.getProductionTime() != null
                            : req.getProductionTime() == null || existing.getProductionTime().compareTo(req.getProductionTime()) != 0)
                        || !Objects.equals(StrUtil.nullToEmpty(existing.getTimeUnitType()), StrUtil.nullToEmpty(req.getTimeUnitType()))
                        || !Objects.equals(StrUtil.nullToEmpty(existing.getRemark()), StrUtil.nullToEmpty(req.getRemark()))))
                    fail("路线包含其他产品,不能通过单产品模板修改共用产出产品:" + item.getCode());
                req.setId(existing.getId()); productService.updateRouteProduct(req); id = existing.getId();
            }
            if (yes(row.getTemplateReference(), "模板引用")) refProductId = id;
        }
        productService.setTemplateReference(refProductId);
        Map<String, MesProRouteProcessDO> existingProcesses = new HashMap<>();
        for (var process : processMapper.selectListByRouteId(routeId)) {
            var op = operationMapper.selectById(process.getProcessId());
            if (op != null) existingProcesses.put(op.getCode(), process);
        }
        Set<String> stepCodes = new HashSet<>(); Set<Integer> sorts = new HashSet<>();
        for (var row : steps) {
            if (StrUtil.isBlank(row.getProcessCode()) || row.getProcessSort() == null) fail("工序记录须填写工序编码和工序顺序");
            String processCode = row.getProcessCode().trim();
            if (row.getProcessSort() < 0 || (row.getPrepareTime() != null && row.getPrepareTime() < 0)
                    || (row.getWaitTime() != null && row.getWaitTime() < 0))
                fail("工序顺序、准备时间和等待时间不能为负数:" + processCode);
            if (row.getLinkType() != null && (row.getLinkType() < 0 || row.getLinkType() > 3))
                fail("工序关系只能填写 0、1、2 或 3:" + processCode);
            if (!stepCodes.add(processCode) || !sorts.add(row.getProcessSort())) fail("工序编码和工序顺序不能重复");
            var operation = operationMapper.selectByCode(processCode);
            if (operation == null) fail("工序不存在:" + processCode);
            operationService.validateProcessExistsAndEnable(operation.getId());
            MesProRouteProcessDO existing = existingProcesses.get(processCode);
            if (existing == null && hasOtherProducts) fail("路线包含其他产品,不能通过单产品模板新增共用工序:" + processCode);
            MesProRouteProcessSaveReqVO req = new MesProRouteProcessSaveReqVO();
            req.setRouteId(routeId); req.setProcessId(operation.getId()); req.setSort(row.getProcessSort());
            req.setLinkType(row.getLinkType()); req.setPrepareTime(row.getPrepareTime()); req.setWaitTime(row.getWaitTime());
            req.setColorCode(row.getColorCode()); req.setKeyFlag(yes(row.getKeyFlag(), "关键工序"));
            req.setCheckFlag(yes(row.getCheckFlag(), "质检工序")); req.setBackflushFlag(yes(row.getBackflushFlag(), "倒冲"));
            req.setRemark(row.getProcessRemark());
            if (existing != null && hasOtherProducts
                    && (!Objects.equals(existing.getSort(), req.getSort())
                    || !Objects.equals(existing.getLinkType(), req.getLinkType())
                    || !Objects.equals(existing.getPrepareTime(), req.getPrepareTime())
                    || !Objects.equals(existing.getWaitTime(), req.getWaitTime())
                    || !Objects.equals(existing.getColorCode(), req.getColorCode())
                    || !Objects.equals(Boolean.TRUE.equals(existing.getKeyFlag()), req.getKeyFlag())
                    || !Objects.equals(Boolean.TRUE.equals(existing.getCheckFlag()), req.getCheckFlag())
                    || !Objects.equals(Boolean.TRUE.equals(existing.getBackflushFlag()), req.getBackflushFlag())
                    || !Objects.equals(StrUtil.nullToEmpty(existing.getRemark()), StrUtil.nullToEmpty(req.getRemark())))) {
                fail("路线包含其他产品,不能通过单产品模板修改共用工序:" + processCode);
            }
            // 保留其他产品的既有产出,避免一次导入删除整条路线的产出。
            Map<String, MesProRouteProcessSaveReqVO.OutputItem> merged = new LinkedHashMap<>();
            if (existing != null) {
                for (var out : outputMapper.selectListByRouteProcessId(existing.getId())) {
                    var v = new MesProRouteProcessSaveReqVO.OutputItem();
                    v.setItemId(out.getItemId()); v.setProductId(out.getProductId()); v.setSort(out.getSort()); v.setRemark(out.getRemark());
                    merged.put(out.getProductId() + ":" + out.getItemId(), v);
                }
                // 旧数据只有 outputItemId 时也必须保留,不能因导入而清空。
                if (merged.isEmpty() && StrUtil.isNotBlank(existing.getOutputItemId())) {
                    for (String text : existing.getOutputItemId().split(",")) if (StrUtil.isNotBlank(text)) {
                        Long id = Long.valueOf(text.trim()); var v = new MesProRouteProcessSaveReqVO.OutputItem();
                        Long owner = isFinished(id) && productMapper.selectListByRouteId(routeId).stream().anyMatch(p -> id.equals(p.getItemId())) ? id : null;
                        v.setItemId(id); v.setProductId(owner); merged.put(owner + ":" + id, v);
                    }
                }
            }
            Set<String> importedOutputKeys = new HashSet<>();
            for (var out : types.getOrDefault("产出", List.of())) {
                if (!processCode.equals(StrUtil.trimToEmpty(out.getProcessCode()))) continue;
                MesMdItemDO outputItem = item(out.getItemCode(), referenceCode);
                Long ownerId = StrUtil.isBlank(out.getProductCode()) ? null : item(out.getProductCode(), referenceCode).getId();
                if (!productIds.contains(outputItem.getId())) fail("产出物料未出现在产品记录:" + outputItem.getCode());
                if (ownerId != null && !productIds.contains(ownerId)) fail("产出归属产品未出现在产品记录");
                var v = new MesProRouteProcessSaveReqVO.OutputItem();
                v.setItemId(outputItem.getId()); v.setProductId(ownerId); v.setSort(out.getOutputSort()); v.setRemark(out.getDetailRemark());
                String outputKey = ownerId + ":" + outputItem.getId();
                if (!importedOutputKeys.add(outputKey)) fail("工序产出记录重复:" + processCode + " / " + outputItem.getCode());
                if (hasOtherProducts && ownerId == null) {
                    var old = merged.get(outputKey);
                    if (old == null || !Objects.equals(old.getSort(), v.getSort())
                            || !Objects.equals(StrUtil.nullToEmpty(old.getRemark()), StrUtil.nullToEmpty(v.getRemark())))
                        fail("路线包含其他产品,不能通过单产品模板修改共用产出:" + outputItem.getCode());
                }
                merged.put(outputKey, v);
            }
            req.setOutputs(new ArrayList<>(merged.values()));
            if (existing == null) processService.createRouteProcess(req);
            else { req.setId(existing.getId()); processService.updateRouteProcess(req); }
        }
        for (var row : types.getOrDefault("产出", List.of()))
            if (!stepCodes.contains(StrUtil.trimToEmpty(row.getProcessCode()))) fail("产出记录找不到对应的工序记录:" + row.getProcessCode());
        Set<String> bomKeys = new HashSet<>();
        for (var row : types.getOrDefault("投料", List.of())) {
            String operationCode = StrUtil.trimToEmpty(row.getProcessCode());
            if (!stepCodes.contains(operationCode)) fail("投料记录找不到对应的工序记录:" + operationCode);
            var op = operationMapper.selectByCode(operationCode);
            MesMdItemDO product = item(row.getProductCode(), referenceCode), material = item(row.getItemCode(), referenceCode);
            if (!productIds.contains(product.getId())) fail("投料产品未出现在产品记录:" + product.getCode());
            if (row.getBomQuantity() == null || row.getBomQuantity().signum() <= 0) fail("投料比例必须大于零");
            if (!bomKeys.add(op.getId() + ":" + product.getId() + ":" + material.getId())) fail("投料记录重复:" + material.getCode());
            MesProRouteProductBomSaveReqVO req = new MesProRouteProductBomSaveReqVO();
            req.setRouteId(routeId); req.setProcessId(op.getId()); req.setProductId(product.getId()); req.setItemId(material.getId());
            req.setQuantity(row.getBomQuantity()); req.setRemark(row.getDetailRemark());
            var existing = bomMapper.selectByUnique(material.getId(), op.getId(), product.getId());
            if (hasOtherProducts && !product.getCode().equals(referenceCode)
                    && (existing == null || existing.getQuantity() == null
                    || existing.getQuantity().compareTo(req.getQuantity()) != 0
                    || !Objects.equals(StrUtil.nullToEmpty(existing.getRemark()), StrUtil.nullToEmpty(req.getRemark()))))
                fail("路线包含其他产品,不能通过单产品模板修改共用产品投料:" + product.getCode());
            if (existing == null) bomService.createRouteProductBom(req);
            else { if (!existing.getRouteId().equals(routeId)) fail("投料记录属于其他路线"); req.setId(existing.getId()); bomService.updateRouteProductBom(req); }
        }
        return created;
    }
 
    private MesMdItemDO item(String code, String referenceCode) {
        if (StrUtil.isBlank(code)) { fail("物料编码不能为空"); return null; }
        String resolved = MesProRouteFullExcelService.REFERENCE.equals(code.trim()) ? referenceCode : code.trim();
        MesMdItemDO item = itemMapper.selectByCode(resolved);
        if (item == null) fail("物料不存在:" + resolved);
        return item;
    }
    private boolean isReference(String value, String referenceCode) {
        return StrUtil.isNotBlank(value) && (MesProRouteFullExcelService.REFERENCE.equals(value.trim()) || referenceCode.equals(value.trim()));
    }
    private boolean isFinished(Long itemId) {
        MesMdItemDO source = itemMapper.selectById(itemId);
        if (source == null) fail("原工序的产出物料不存在:" + itemId);
        var type = itemTypeService.getItemType(source.getItemTypeId());
        Set<Long> seen = new HashSet<>();
        while (type != null && seen.add(type.getId())) {
            if ("FG".equals(type.getCode())) return true;
            if (type.getParentId() == null || type.getParentId() == 0L) break;
            type = itemTypeService.getItemType(type.getParentId());
        }
        return false;
    }
    private String timeUnit(String value) {
        if (StrUtil.isBlank(value)) return null;
        return switch (value.trim()) {
            case "分钟", "MINUTE" -> "MINUTE";
            case "小时", "HOUR" -> "HOUR";
            case "天", "DAY" -> "DAY";
            // 保留历史数据中的旧单位编码,不推测其含义。
            case "1", "2", "3" -> value.trim();
            default -> throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, "生产时间单位只能填写分钟、小时或天");
        };
    }
    private boolean yes(String value, String field) {
        if (StrUtil.isBlank(value) || "否".equals(value.trim())) return false;
        if ("是".equals(value.trim())) return true;
        fail(field + "只能填写是或否"); return false;
    }
    private void fail(String message) { throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, message); }
}