| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- 'use strict';
- const crypto = require('crypto');
- const audit = require('../const/audit');
- const changeConst = require('../const/change');
- const GclGather = require('./gcl_gather').gclGather;
- class ChangeAiImport {
- constructor(ctx) {
- this.ctx = ctx;
- }
- async getContext(scene, id, transaction, includeLedger = true) {
- const ctx = this.ctx;
- const configs = {
- apply: {
- service: 'changeApply',
- list: 'changeApplyList',
- att: 'changeApplyAtt',
- key: 'id',
- foreign: 'caid',
- feature: 'openChangeApply',
- },
- plan: {
- service: 'changePlan',
- list: 'changePlanList',
- att: 'changePlanAtt',
- key: 'id',
- foreign: 'cpid',
- feature: 'openChangePlan',
- },
- change: {
- service: 'change',
- list: 'changeAuditList',
- att: 'changeAtt',
- key: 'cid',
- foreign: 'cid',
- },
- };
- const config = configs[scene];
- if (!config || !id) throw '变更识别参数有误';
- if (config.feature && !ctx.subProject.page_show[config.feature]) throw '该功能已关闭';
- const service = ctx.service[config.service];
- const where = { [config.key]: id, tid: ctx.tender.id };
- const change = transaction
- ? (await service.getAllDataForUpdate(where, transaction))[0]
- : await service.getDataByCondition(where);
- const status = audit[config.service].status;
- // 与详情页一致:只允许原报人在未上报、退回或修订时导入。
- if (!change || String(change.uid) !== String(ctx.session.sessionUser.accountId)
- || ![status.uncheck, status.checkNo, status.revise].includes(change.status)) {
- throw '当前单据不可导入,请刷新页面检查权限或审批状态';
- }
- const listService = ctx.service[config.list];
- const detailWhere = { [config.foreign]: id, tid: ctx.tender.id };
- const lists = transaction
- ? await listService.getAllDataForUpdate(detailWhere, transaction, 'id')
- : await listService.getAllDataByCondition({ where: detailWhere, orders: [['id', 'asc']] });
- const attachments = await ctx.service[config.att].getAllDataByCondition({
- where: detailWhere,
- columns: ['id'],
- orders: [['id', 'asc']],
- }, transaction);
- change.decimal = typeof change.decimal === 'string' ? JSON.parse(change.decimal) : change.decimal;
- change.decimal = change.decimal || {
- tp: ctx.tender.info.decimal.tp,
- up: ctx.tender.info.decimal.up,
- precision: ctx.tender.info.precision,
- };
- ctx.change = change;
- const ledgerTargets = scene === 'change' && includeLedger ? await this._getLedgerTargets(transaction) : [];
- const ledgerVersion = crypto.createHash('sha256').update(JSON.stringify(ledgerTargets)).digest('hex');
- const version = crypto.createHash('sha256').update(JSON.stringify({ change, lists, attachments })).digest('hex');
- const fields = this._getFields(scene);
- const original = {};
- for (const field of fields) {
- original[field.key] = change[field.key] === null || change[field.key] === undefined ? '' : String(change[field.key]);
- }
- const classes = (ctx.subProject.fun_set.change_class || []).filter(item => item.checked).map(item => ({
- value: String(scene === 'change' ? item.value : item.new_name || item.name),
- name: item.new_name || item.name,
- aliases: [item.name, item.new_name].filter(Boolean),
- }));
- const qualities = Object.values(changeConst.quality).map(item => ({
- value: String(scene === 'change' ? item.value : item.name),
- name: item.name,
- aliases: [item.name.replace('设计', '')],
- }));
- return {
- scene,
- id,
- config,
- change,
- fields,
- original,
- lists,
- ledgerTargets,
- ledgerVersion,
- version,
- precision: change.decimal.precision || ctx.tender.info.precision,
- upDecimal: change.up_decimal === null || change.up_decimal === undefined ? change.decimal.up : change.up_decimal,
- options: { class: classes, quality: qualities },
- // AI 导入匹配不到已有清单和台账时,允许新增空白清单。
- allowAdd: true,
- };
- }
- _getFields(scene) {
- const relatedKeys = { apply: 'project_code', plan: 'apply_code', change: 'plan_code' };
- const fields = [
- ['code', '编号'],
- ['name', '变更名称'],
- ['peg', '桩号'],
- [relatedKeys[scene], '关联单据编号'],
- ['org_name', '原设计名称'],
- [scene === 'change' ? 'org_code' : 'new_code', '原设计图号'],
- [scene === 'change' ? 'new_name' : 'design_name', '变更设计名称'],
- [scene === 'change' ? 'new_code' : 'c_new_code', '变更设计图号'],
- ['class', '变更类别'],
- ['quality', '变更性质'],
- ];
- const textFields = {
- apply: [['reason', '变更原因'], ['org_content', '原设计情况'], ['site_content', '现场情况'], ['content', '变更内容']],
- plan: [['reason', '拟变更方案'], ['content', '变更内容'], ['memo', '备注'], ['expr', '计算依据']],
- change: [['content', '变更内容'], ['basis', '变更依据'], ['expr', '计算依据'], ['memo', '备注']],
- };
- const result = fields.map(item => ({
- key: item[0],
- name: item[1],
- type: ['class', 'quality'].includes(item[0]) ? 'select' : 'text',
- }));
- return result.concat(textFields[scene].map(item => ({ key: item[0], name: item[1], type: 'textarea' })));
- }
- async save(scene, id, payload, file) {
- if (!payload || !['all', 'main', 'items'].includes(payload.extract_scope)
- || !Array.isArray(payload.rows) || payload.rows.length > 500) {
- throw '导入数据格式有误';
- }
- if (file && scene === 'apply'
- && !changeConst.file_type.some(item => String(item.key) === String(payload.attachment_type))) {
- throw '请选择附件资料类型';
- }
- const ctx = this.ctx;
- const transaction = await ctx.service.change.beginTransaction();
- try {
- // 加锁后重新检查权限和预览版本,所有写入使用同一个事务。
- const needsLedger = payload.extract_scope !== 'main' && payload.rows.some(row => row.action === '台账插入');
- const data = await this.getContext(scene, id, transaction, needsLedger);
- if (payload.version !== data.version) throw '单据已发生变化或本次导入已保存,请关闭窗口并重新识别';
- if (needsLedger && payload.ledgerVersion !== data.ledgerVersion) throw '台账已发生变化,请返回第一步重新识别并核对台账目标';
- const info = {};
- if (payload.extract_scope !== 'items') {
- for (const field of data.fields) {
- if (!Object.prototype.hasOwnProperty.call(payload.main || {}, field.key)) continue;
- const value = payload.main[field.key];
- if (typeof value !== 'string' || value.length > (field.type === 'textarea' ? 60000 : 2000)) {
- throw '基本信息字段格式有误';
- }
- if (value === data.original[field.key]) continue;
- if (field.type === 'select' && !data.options[field.key].some(item => item.value === value)) {
- throw field.name + '不在系统选项内';
- }
- if (['code', 'name'].includes(field.key) && !value.trim()) throw field.name + '不能为空';
- info[field.key] = field.type === 'select' && scene === 'change' ? Number(value) : value;
- }
- }
- if (info.code && await ctx.service[data.config.service].isRepeat(id, info.code, ctx.tender.id)) {
- throw '该编号已存在';
- }
- const updates = [];
- const inserts = [];
- const usedTargets = new Set();
- const usedLedgerTargets = new Set();
- const beforeKey = scene === 'change' ? 'oamount2' : 'oamount';
- const selected = payload.extract_scope === 'main' ? [] : payload.rows;
- for (const row of selected) {
- if (row.action === '跳过') continue;
- if (!['新增', '更新', '台账插入'].includes(row.action)) throw '清单处理方式有误';
- const target = row.action === '更新' ? data.lists.find(item => String(item.id) === String(row.target)) : null;
- const ledgerTarget = row.action === '台账插入' ? data.ledgerTargets.find(item => item.id === row.target) : null;
- if (row.action === '台账插入') {
- if (!ledgerTarget) throw '台账清单或部位已失效,请重新选择';
- if (usedLedgerTargets.has(ledgerTarget.id) || data.lists.some(item =>
- String(item.gcl_id) === String(ledgerTarget.gcl_id) && String(item.mx_id || '') === String(ledgerTarget.mx_id))) {
- throw '该台账清单部位已添加或被多行选中,请改为更新已有清单或重新选择';
- }
- usedLedgerTargets.add(ledgerTarget.id);
- }
- if (row.action === '更新' && (!target || usedTargets.has(String(target.id)))) {
- throw '更新目标不存在或多行选择了同一清单,请重新核对';
- }
- if (target) usedTargets.add(String(target.id));
- if (!target && !ledgerTarget && !data.allowAdd) throw '空白清单添加功能未开启';
- const item = {};
- if (ledgerTarget) {
- Object.assign(item, ledgerTarget);
- delete item.id;
- delete item.source;
- } else if (!target) {
- for (const key of ['code', 'name', 'unit']) {
- if (typeof row[key] !== 'string' || row[key].length > 500) throw '清单文本格式有误';
- item[key] = row[key].trim();
- }
- if (!item.name || !item.unit) throw '新增清单名称、单位不能为空';
- item.unit_price = this._parseNumber(row.price, true);
- }
- const before = this._parseNumber(row.before, false);
- const delta = this._parseNumber(row.delta, false);
- const unit = target ? target.unit : item.unit;
- const precision = data.change.decimal.precision || ctx.tender.info.precision;
- const unitSetting = Object.values(precision || {}).find(value => value.unit === unit);
- const digits = unitSetting ? unitSetting.value : precision && precision.other ? precision.other.value : 3;
- const syncOriginal = scene === 'change' && !this._hasLedgerReference(ledgerTarget || target);
- item[beforeKey] = ctx.helper.round(before, digits);
- if (syncOriginal) item.oamount = item[beforeKey];
- item.camount = ctx.helper.round(delta, digits);
- const sameBefore = target && target[beforeKey] !== null && target[beforeKey] !== undefined
- && target[beforeKey] !== '' && Number(target[beforeKey]) === item[beforeKey]
- && (!syncOriginal || target.oamount !== null && target.oamount !== undefined
- && target.oamount !== '' && Number(target.oamount) === item.oamount);
- const sameDelta = target && target.camount !== null && target.camount !== undefined
- && target.camount !== '' && Number(target.camount) === item.camount;
- if (sameBefore && sameDelta) continue;
- if (!sameDelta) {
- item.spamount = item.camount;
- if (scene === 'change') item.camount_expr = '';
- }
- if (target) {
- if (scene === 'change') await this._checkTarget(target, item);
- item.id = target.id;
- updates.push(item);
- } else {
- item.tid = ctx.tender.id;
- item[data.config.foreign] = id;
- if (item.unit_price !== null) item.unit_price = ctx.helper.round(item.unit_price, data.upDecimal);
- if (scene === 'change') {
- Object.assign(item, {
- lid: '0',
- bwmx: '',
- samount: '',
- detail: '',
- gcl_id: '',
- mx_id: '',
- is_valuation: 1,
- delimit: data.change.delimit === undefined || data.change.delimit === null ? 100 : data.change.delimit,
- order: data.change.order_by ? Math.max(0, ...data.lists.map(list => Number(list.order) || 0)) + inserts.length + 1 : null,
- });
- if (ledgerTarget) {
- for (const key of ['lid', 'gcl_id', 'mx_id', 'bwmx', 'xmj_code', 'xmj_jldy', 'xmj_dwgc', 'xmj_fbgc', 'xmj_fxgc', 'oamount']) {
- item[key] = ledgerTarget[key];
- }
- await this._checkTarget({ gcl_id: item.gcl_id, mx_id: item.mx_id }, item);
- }
- }
- inserts.push(item);
- }
- }
- if (!Object.keys(info).length && !inserts.length && !updates.length && !file) throw '没有需要保存的变更';
- let attachment;
- if (file) {
- attachment = {
- tid: ctx.tender.id,
- [data.config.foreign]: id,
- uid: ctx.session.sessionUser.accountId,
- filename: scene === 'change' ? file.name : file.filename,
- fileext: file.ext,
- filepath: file.filepath,
- filesize: scene === 'change' ? file.size : ctx.helper.bytesToSize(file.size),
- extra_upload: false,
- };
- if (scene === 'change') {
- attachment.in_time = Math.floor(Date.now() / 1000);
- } else {
- attachment.upload_time = new Date();
- attachment.type = scene === 'apply' ? Number(payload.attachment_type) : 5;
- }
- }
- if (Object.keys(info).length) {
- await ctx.service[data.config.service].defaultUpdate(info, {
- where: { [data.config.key]: id, tid: ctx.tender.id },
- }, transaction);
- }
- const listService = ctx.service[data.config.list];
- if (inserts.length) {
- await listService.insertDatasSplit(transaction, listService.tableName, inserts);
- }
- if (updates.length) {
- await listService.defaultUpdateRows(updates, transaction);
- }
- if (inserts.length || updates.length) {
- await listService.calcCamountSum(transaction);
- }
- if (attachment) {
- const attachmentService = ctx.service[data.config.att];
- await attachmentService.insertDatasSplit(transaction, attachmentService.tableName, [attachment]);
- }
- await transaction.commit();
- return { added: inserts.length, updated: updates.length, fields: Object.keys(info).length };
- } catch (err) {
- await transaction.rollback();
- throw err;
- }
- }
- _parseNumber(value, optional) {
- if (value === '' || value === null || value === undefined) {
- if (optional) return null;
- throw '原设计数量和增减数量不能为空,请核对或跳过该行';
- }
- if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(String(value))
- || !Number.isFinite(Number(value)) || Math.abs(Number(value)) > 1e15) {
- throw '清单数值格式有误';
- }
- return Number(value);
- }
- async _checkTarget(target, values) {
- const ctx = this.ctx;
- const ready = await ctx.service.settle.getReadySettle(ctx.tender.id);
- if (ready && target.gcl_id) {
- const service = target.mx_id ? ctx.service.settlePos : ctx.service.settleBills;
- const where = { settle_id: ready.id, lid: target.gcl_id };
- if (target.mx_id) where.pid = target.mx_id;
- const settled = await service.getDataByCondition(where);
- if (settled && settled.settle_status === ctx.service.settle.settleStatus.finish) throw '已结算清单不能修改';
- }
- if (!target.id) return;
- const used = await ctx.service.stageChange.getAllFinalUsedData(ctx.tender.id, ctx.change.cid);
- let quantity = 0;
- for (const item of used.filter(item => String(item.cbid) === String(target.id))) {
- quantity = ctx.helper.add(quantity, item.qty || 0);
- }
- if ((quantity > 0 && values.camount < quantity) || (quantity < 0 && values.camount > quantity)) {
- throw '清单增减数量不能小于已调用数量范围';
- }
- }
- async _getLedgerTargets(transaction) {
- const ctx = this.ctx;
- const lastRevise = await ctx.service.ledgerRevise.getLastestRevise(ctx.tender.id);
- const includeChange = !lastRevise || lastRevise.status === audit.revise.status.checked;
- const ledgerCondition = { where: { tender_id: ctx.tender.id }, orders: [['ledger_id', 'asc']] };
- const posCondition = { where: { tid: ctx.tender.id }, orders: [['porder', 'asc'], ['id', 'asc']] };
- const bills = await ctx.service.ledger.getAllDataByCondition(ledgerCondition, transaction);
- const positions = await ctx.service.pos.getAllDataByCondition(posCondition, transaction);
- if (includeChange) {
- bills.push(...await ctx.service.changeLedger.getAllDataByCondition(ledgerCondition, transaction));
- positions.push(...await ctx.service.changePos.getAllDataByCondition(posCondition, transaction));
- }
- return this.buildLedgerContext(bills, positions).ledgerTargets;
- }
- buildLedgerContext(bills, positions) {
- const ctx = this.ctx;
- const gather = new GclGather(ctx);
- gather.billsTree.loadDatas(bills.slice().sort((a, b) => (Number(a.order) || 0) - (Number(b.order) || 0) || String(a.id).localeCompare(String(b.id))));
- const posByLedger = new Map();
- for (const pos of positions.slice().sort((a, b) => (Number(a.porder) || 0) - (Number(b.porder) || 0) || String(a.id).localeCompare(String(b.id)))) {
- const key = String(pos.lid);
- if (!posByLedger.has(key)) posByLedger.set(key, []);
- posByLedger.get(key).push(pos);
- }
- const groups = new Map();
- const targets = [];
- for (const node of gather.billsTree.nodes) {
- if (!node.b_code || node.children.length) continue;
- const groupKey = JSON.stringify([node.b_code, node.name || '', node.unit || '', Number(node.unit_price) || 0]);
- if (!groups.has(groupKey)) groups.set(groupKey, node.id);
- let section = node;
- while (section.b_code && gather.billsTree.getParent(section)) section = gather.billsTree.getParent(section);
- const peg = gather.getPegNode(section);
- const details = posByLedger.get(String(node.id)) || [node];
- for (const detail of details) {
- const mxId = detail === node ? '' : String(detail.id);
- const quantity = Number(detail.quantity) || 0;
- targets.push({
- id: JSON.stringify([String(node.id), mxId]),
- source: 'ledger',
- lid: groups.get(groupKey),
- gcl_id: node.id,
- mx_id: mxId,
- code: node.b_code,
- name: node.name || '',
- unit: node.unit || '',
- unit_price: node.unit_price === undefined ? null : node.unit_price,
- oamount: quantity,
- oamount2: quantity,
- camount: 0,
- bwmx: detail.name !== node.name ? detail.name || section.name || '' : section.name || '',
- xmj_code: section.code || '',
- xmj_jldy: section.name || '',
- xmj_dwgc: gather.getDwgc(peg, section),
- xmj_fbgc: gather.getFbgc(peg, section),
- xmj_fxgc: gather.getFxgc(peg, section),
- });
- }
- }
- const ledgerVersion = crypto.createHash('sha256').update(JSON.stringify(targets)).digest('hex');
- return { ledgerTargets: targets, ledgerVersion };
- }
- _hasLedgerReference(item) {
- return !!item && [item.lid, item.gcl_id, item.mx_id].some(value => {
- const id = value === null || value === undefined ? '' : String(value).trim();
- return id !== '' && id !== '0';
- });
- }
- }
- module.exports = ChangeAiImport;
|