'use strict'; /** * * * @author Mai * @date * @version */ const _ = require('lodash'); const colDefineType = { match: 1, pos: 2, }; const aeUtils = { toNumber: function (value) { let num = _.toNumber(value); return _.isNaN(num) ? null : num; }, checkColHeader: function (row, colHeaderMatch) { const colsDef = {}; for (const iCol in row) { const text = row[iCol]; if (text === null) continue; for (const header in colHeaderMatch) { const match = colHeaderMatch[header]; switch (match.type) { case colDefineType.match: if (match.value.indexOf(text) >= 0) { colsDef[header] = iCol; } break; case colDefineType.pos: for (const v of match.value) { if (text.indexOf(v) >= 0) { colsDef[header] = iCol; break; } } break; } } } return colsDef; } }; const mainReg = /^(GD*)?G?(\d\d)*\d$/i; const subReg = /^(GD*)?G?[A-Z]{2}(\d\d)+$/i; const gdXmjPartReg = /^(GD*)?G?/; const specCode106 = { code: ['102', '103', '104'], reg: /^(GD*)?G?106/i }; const specCode109 = { code: ['102', '103', '104', '105', '106', '107', '108'], reg: /^(GD*)?G?109/i }; class ImportBaseTree { /** * 构造函数 * @param {Array} tempData - 清单模板数据 */ constructor (tempData, ctx) { this.ctx = ctx; // 常量 this.splitChar = '-'; // 索引 // 以code为索引 this.codeNodes = {}; this.items = []; this.roots = []; this.pos = []; this.tempData = []; // 以id为索引 this.nodes = {}; // 缓存 this.finalNode = null; this.finalPrecision = null; this.finalXmjNode = null; this.keyNodeId = 1; this._loadTemplateTree(tempData); } /** * 加载 清单模板 * @param {Array} data - 模板数据 * @private */ _loadTemplateTree(data) { let loadCodeNodes = true; for (const node of data) { node.ledger_id = node.template_id; node.ledger_pid = node.pid; node.id = this.ctx.app.uuid.v4(); delete node.pid; if (node.code && loadCodeNodes) { this.codeNodes[node.code] = node; } if (node.code === '3') { loadCodeNodes = false; } this.items.push(node); if (node.ledger_pid === -1) { this.roots.push(node); } if (node.ledger_id >= this.keyNodeId) { this.keyNodeId = node.ledger_id + 1; } this.tempData.push(node); this.nodes[node.ledger_id] = node; } for (const node of this.items) { node.tender_id = this.ctx.tender.id; node.children = this.items.filter(function (i) { return i.ledger_pid === node.ledger_id; }); } } /** * 根据 编号、名称 查找模板节点 * @param {Object} node - 要查找的节点 * @returns {*} */ findTempData(node) { return this.tempData.find(function (td) { return td.code === node.code && td.name === node.name; }); } /** * 根据 编号 查找 父项项目节 * @param {String} code - 子项编号 * @returns {*} */ findXmjParent(code) { const codePath = code.split(this.splitChar); if (codePath.length > 1) { codePath.splice(codePath.length - 1, 1); return this.codeNodes[codePath.join(this.splitChar)]; } } getPosterity(node) { let posterity = [].concat(node.children); for (const c of node.children) { posterity = posterity.concat(this.getPosterity(c)); } return posterity; } findGclParent(code, xmj) { let parent; const codePath = code.split(this.splitChar); if (codePath.length > 1) { codePath.splice(codePath.length - 1, 1); const parentCode = codePath.join(this.splitChar); const posterity = this.getPosterity(xmj); parent = posterity.find(function (x) { return x.b_code === parentCode; }) } return parent ? parent : xmj; } /** * 添加 树节点 并完善该节点的树结构 * @param {Object} node - 添加节点 * @param {Object} parent - 父项 * @returns {*} */ addNodeWithParent(node, parent) { node.id = this.ctx.app.uuid.v4(); node.ledger_id = this.keyNodeId; this.keyNodeId += 1; node.ledger_pid = parent ? parent.ledger_id : -1; node.level = parent ? parent.level + 1 : 1; node.order = parent ? parent.children.length + 1 : this.roots.length + 1; node.full_path = parent ? parent.full_path + '-' + node.ledger_id : '' + node.ledger_id; if (parent) { parent.children.push(node); } else { this.roots.push(node); } this.items.push(node); this.codeNodes[node.code] = node; this.defineCacheData(node); return node; } /** * 定义缓存节点(添加工程量清单、部位明细需使用缓存定位) * @param {Object} node - 当前添加的节点 */ defineCacheData(node) { this.nodes[node.ledger_id] = node; this.finalNode = node; this.finalPrecision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit); if (node.code) { this.finalXmjNode = node; } } _assignRelaField(temp, node) { if (!temp.quantity) temp.quantity = node.quantity; if (!temp.dgn_qty1) temp.dgn_qty1 = node.dgn_qty1; if (!temp.dgn_qty2) temp.dgn_qty2 = node.dgn_qty2; if (!temp.unit_price) temp.unit_price = node.unit_price; if (!temp.drawing_code) temp.drawing_code = node.drawing_code; if (!temp.memo) temp.memo = node.memo; if (!temp.total_price) temp.total_price = node.total_price; } /** * 添加 项目节 * @param {Object} node - 项目节 * @returns {*} */ addXmjNode(node) { node.id = this.ctx.app.uuid.v4(); node.tender_id = this.ctx.tender.id; node.children = []; if (node.code.split(this.splitChar).length > 1) { const temp = this.findTempData(node); if (temp) { this.defineCacheData(temp); this._assignRelaField(temp, node); return temp; } else { const parent = this.findXmjParent(node.code); if (parent) { return this.addNodeWithParent(node, parent); } else { const newNode = this.addNodeWithParent(node, this.codeNodes['1']); newNode.error = 1; return null; } } } else { const n = this.codeNodes[node.code]; if (!n) { return this.addNodeWithParent(node, null); } else { this.defineCacheData(n); this._assignRelaField(n, node); return n; } } } /** * 添加 工程量清单 * @param {Object} node - 工程量清单 */ addGclNode(node) { node.id = this.ctx.app.uuid.v4(); node.tender_id = this.ctx.tender.id; node.pos = []; node.children = []; if (this.finalXmjNode) { const parent = node.b_code ? this.findGclParent(node.b_code, this.finalXmjNode) : this.finalXmjNode; return this.addNodeWithParent(node, parent); } } /** * 添加 部位明细 * @param {object} pos - 部位明细 * @returns {*} */ addPos (pos){ if (this.finalNode && this.finalNode.pos) { pos.id = this.ctx.app.uuid.v4(); pos.lid = this.finalNode.id; pos.tid = this.ctx.tender.id; pos.add_stage = 0; pos.add_times = 0; pos.in_time = new Date(); pos.porder = this.finalNode.pos.length + 1; pos.add_user = this.ctx.session.sessionUser.accountId; this.finalNode.pos.push(pos); this.pos.push(pos); pos.sgfh_qty = this.ctx.helper.round(pos.sgfh_qty, this.finalPrecision.value); pos.quantity = this.ctx.helper.round(pos.quantity, this.finalPrecision.value); return pos; } } /** * 第一部分的子节点,顺序重排 */ resortFirstPartChildren () { const splitChar = this.splitChar; const firstPart = this.roots[0]; firstPart.children.sort(function (a, b) { if (!a.code) { return 1; } else if (!b.code) { return -1; } if (a.error === b.error) { const codeA = a.code.split(splitChar); const numA = _.toNumber(codeA[codeA.length -1]); const codeB = b.code.split(splitChar); const numB = _.toNumber(codeB[codeB.length -1]); return numA - numB; } else if (a.error) { return 1 } else if (b.error) { return -1; } }); for (const [i, c] of firstPart.children.entries()) { c.order = i + 1; } } calculateLeafWithPos () { for (const node of this.items) { if (node.children && node.children.length > 0) { node.unit_price = null; node.quantity = null; node.total_price = null; } if (!node.pos || node.pos.length === 0) { continue; } node.quantity = this.ctx.helper.sum(_.map(node.pos, 'quantity')); if (node.quantity && node.unit_price) { node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp); } else { node.total_price = null; } } } } class ImportStd18Tree extends ImportBaseTree { /** * 检查是否是父项 * @param parent * @param code * @returns {boolean} * @private */ _checkParent(parent, code) { if (code === 'LJ0703') console.log(parent.code, code); const codeNumberPart = code.replace(gdXmjPartReg, ''); if (!parent.code) return false; const numberPart = parent.code.replace(gdXmjPartReg, ''); if (code === 'LJ0703') console.log(numberPart, codeNumberPart); if (!numberPart || !codeNumberPart || numberPart.length >= codeNumberPart.length) return false; return code.indexOf(numberPart) === 0 || code.indexOf('G' + numberPart) === 0 || code.indexOf('GD' + numberPart) === 0; } /** * 查找主表项目节父项 * @param code * @returns {*} */ findMainXmjParent(code) { const numberPart = code.replace(gdXmjPartReg, ''); if (numberPart.length <= 1) throw '首层项目节模板中未定义,不支持导入'; let parent = this.cacheMainXmjNode; while (parent) { if (this._checkParent(parent, code)) return parent; parent = this.nodes[parent.ledger_pid]; } return null; } /** * 查找分表项目节父项 * @param code * @returns {*} */ findSubXmjParent(code) { let parent = this.cacheSubXmjNode; while (parent && parent.is_sub && parent.code.match(subReg)) { if (this._checkParent(parent, code)) return parent; parent = this.nodes[parent.ledger_pid]; } return this.cacheMainXmjNode; } /** * 根据 编号 查找 父项项目节 * @param {String} code - 子项编号 * @returns {*} */ findXmjParent(code) { if (code.match(mainReg)) { if (!this.cacheMainXmjNode) throw '主表项目节找不到父项'; return this.findMainXmjParent(code); } else if (code.match(subReg)) { if (!this.cacheMainXmjNode) throw '分表项目节找不到所属主表项目节'; return this.findSubXmjParent(code); } } /** * 定义缓存节点(添加工程量清单、部位明细需使用缓存定位) * @param {Object} node - 当前添加的节点 */ defineCacheData(node) { super.defineCacheData(node); if (node.code) { if (node.code.match(mainReg)) { node.is_main = true; this.cacheMainXmjNode = node; this.cacheSubXmjNode = null; } else if (node.code.match(subReg)) { node.is_sub = true; this.cacheSubXmjNode = node; } if (node.code.match(specCode106.reg)) { if (this.cacheSpecMainXmj1 && this.cacheSpecMainXmj1.code.match(specCode109.reg)) { this.cacheSpecMainXmj2 = node; } else { this.cacheSpecMainXmj1 = node; } } else if (node.code.match(specCode109.reg)) { this.cacheSpecMainXmj1 = node; this.cacheSpecMainXmj2 = null; } } } /** * 添加 项目节 * @param {Object} node - 项目节 * @returns {*} */ addXmjNode(node) { if (!node.code || (!node.code.match(mainReg) && !node.code.match(subReg))) return null; node.id = this.ctx.app.uuid.v4(); node.tender_id = this.ctx.tender.id; node.children = []; if ((specCode106.code.indexOf(node.code) >= 0)) { if (this.cacheSpecMainXmj2 && this.cacheSpecMainXmj2.code.match(specCode106.reg)) return this.addNodeWithParent(node, this.cacheSpecMainXmj2); if (this.cacheSpecMainXmj1 && this.cacheSpecMainXmj1.code.match(specCode106.reg)) return this.addNodeWithParent(node, this.cacheSpecMainXmj1); } if ((specCode109.code.indexOf(node.code) >= 0) && (this.cacheSpecMainXmj1 && this.cacheSpecMainXmj1.code.match(specCode109.reg))) { return this.addNodeWithParent(node, this.cacheSpecMainXmj1) } const temp = this.findTempData(node); if (temp) { this.defineCacheData(temp); this._assignRelaField(temp, node); return temp; } else { const parent = this.findXmjParent(node.code); return this.addNodeWithParent(node, parent); } } } class AnalysisExcelTree { /** * 构造函数 */ constructor(ctx) { this.ctx = ctx; this.decimal = ctx.tender.info.decimal; this.colsDef = null; this.colHeaderMatch = { code: {value: ['项目节编号', '预算项目节'], type: colDefineType.match}, b_code: {value: ['清单子目号', '清单编号', '子目号'], type: colDefineType.match}, pos: {value: ['计量单元'], type: colDefineType.match}, name: {value: ['名称'], type: colDefineType.match}, unit: {value: ['单位'], type: colDefineType.match}, quantity: {value: ['清单数量'], type: colDefineType.match}, dgn_qty1: {value: ['设计数量1'], type: colDefineType.match}, dgn_qty2: {value: ['设计数量2'], type: colDefineType.match}, unit_price: {value: ['单价'], type: colDefineType.match}, drawing_code: {value: ['图号'], type: colDefineType.match}, memo: {value: ['备注'], type: colDefineType.match}, }; } _isMatch11(tempData) { return _.find(tempData, x => { return x.code.indexOf('-') > 0; }) } _isMatch18(tempData) { return _.every(tempData, x => { return !x.code || !!x.code.match(mainReg); }); } _getNewCacheTree(tempData) { // 模板符合11编办规则,使用11编办树 if (this._isMatch18(tempData)) { return new ImportStd18Tree(tempData, this.ctx); // 反之使用11编办(未校验模板是否符合,替换注释部分即可实现) // } else if (this._isMatch11(tempData)){ } else { return new ImportBaseTree(tempData, this.ctx); } } /** * 读取项目节节点 * @param {Array} row - excel行数据 * @returns {*} * @private */ _loadXmjNode(row) { try { const node = {}; node.code = this.ctx.helper.replaceReturn(row[this.colsDef.code]); node.name = this.ctx.helper.replaceReturn(row[this.colsDef.name]); node.unit = this.ctx.helper.replaceReturn(row[this.colsDef.unit]); const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit); node.quantity = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.quantity]), precision.value); node.dgn_qty1 = aeUtils.toNumber(row[this.colsDef.dgn_qty1]); node.dgn_qty2 = aeUtils.toNumber(row[this.colsDef.dgn_qty2]); node.unit_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.unit_price]), this.decimal.up); node.drawing_code = this.ctx.helper.replaceReturn(row[this.colsDef.drawing_code]); node.memo = this.ctx.helper.replaceReturn(row[this.colsDef.memo]); if (node.quantity && node.unit_price) { node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp); } else { node.total_price = null; } return this.cacheTree.addXmjNode(node); } catch (error) { console.log(error); if (error.stack) { this.ctx.logger.error(error); } else { this.ctx.getLogger('fail').info(JSON.stringify({ error, project: this.ctx.session.sessionProject, user: this.ctx.session.sessionUser, body: row, })); } return null; } } /** * 读取工程量清单数据 * @param {Array} row - excel行数据 * @returns {*} * @private */ _loadGclNode(row) { const node = {}; node.b_code = this.ctx.helper.replaceReturn(row[this.colsDef.b_code]); node.name = this.ctx.helper.replaceReturn(row[this.colsDef.name]); node.unit = this.ctx.helper.replaceReturn(row[this.colsDef.unit]); const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit); node.quantity = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.quantity]), precision.value); node.sgfh_qty = node.quantity; node.unit_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.unit_price]), this.decimal.up); node.drawing_code = this.ctx.helper.replaceReturn(row[this.colsDef.drawing_code]); node.memo = this.ctx.helper.replaceReturn(row[this.colsDef.memo]); if (node.quantity && node.unit_price) { node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp); } else { node.total_price = null; } if (this.filter && !node.quantity && !node.total_price) { return this.filter; } return this.cacheTree.addGclNode(node); } /** * 读取部位明细数据 * @param {Array} row - excel行数据 * @returns {*} * @private */ _loadPos(row) { const pos = {}; pos.name = this.ctx.helper.replaceReturn(row[this.colsDef.name]); pos.quantity = aeUtils.toNumber(row[this.colsDef.quantity]); pos.sgfh_qty = pos.quantity; pos.drawing_code = this.ctx.helper.replaceReturn(row[this.colsDef.drawing_code]); return this.cacheTree.addPos(pos); } /** * 读取数据行 * @param {Array} row - excel数据行 * @param {Number} index - 行索引号 */ loadRowData(row, index) { let result; // 含code识别为项目节,含posCode识别为部位明细,其他识别为工程量清单 if (row[this.colsDef.code]) { // 第三部分(编号为'3')及之后的数据不读取 if (row[this.colsDef.code] === '3') { this.loadEnd = true; return; } result = this._loadXmjNode(row) } else if (row[this.colsDef.pos]) { result = this._loadPos(row); } else { result = this._loadGclNode(row); } // 读取失败则写入错误数据 todo 返回前端告知用户? if (!result) { this.errorData.push({ serialNo: index, data: row, }); } } /** * 读取表头并检查 * @param {Number} row - Excel数据行 */ checkColHeader(row) { const colsDef = aeUtils.checkColHeader(row, this.colHeaderMatch); if (colsDef.code && colsDef.b_code && colsDef.pos) { this.colsDef = colsDef; } } /** * 将excel清单 平面数据 解析为 树结构数据 * @param {object} sheet - excel清单数据 * @param {Array} tempData - 新建项目使用的清单模板 * @returns {ImportBaseTree} */ analysisData(sheet, tempData, filter) { this.filter = filter; this.colsDef = null; this.cacheTree = this._getNewCacheTree(tempData); this.errorData = []; this.loadEnd = false; for (const iRow in sheet.rows) { const row = sheet.rows[iRow]; if (this.colsDef && !this.loadEnd) { this.loadRowData(row, iRow); } else { this.checkColHeader(row); } } this.cacheTree.resortFirstPartChildren(); this.cacheTree.calculateLeafWithPos(); return this.cacheTree; } } class ImportGclBaseTree { /** * 构造函数 * @param {Array} tempData - 清单模板数据 */ constructor (ctx, parent, maxId, defaultData) { this.ctx = ctx; this.parent = parent; this.defaultData = defaultData; // 常量 this.splitChar = '-'; // 索引 // 以code为索引 this.codeNodes = {}; this.items = []; // 缓存 this.keyNodeId = maxId ? maxId + 1 : 1; this.blankParent = null; } /** * 根据 编号 查找 父项项目节 * @param {String} code - 子项编号 * @returns {*} */ findParent(code) { const codePath = code.split(this.splitChar); if (codePath.length > 1) { codePath.splice(codePath.length - 1, 1); return this.codeNodes[codePath.join(this.splitChar)]; } } /** * 添加 树节点 并完善该节点的树结构 * @param {Object} node - 添加节点 * @param {Object} parent - 父项 * @returns {*} */ addNodeWithParent(node, parent) { parent = parent ? parent : this.parent; if (!parent.children) parent.children = []; node.id = this.ctx.app.uuid.v4(); node.tender_id = this.ctx.tender.id; node.ledger_id = this.keyNodeId; this.keyNodeId += 1; node.ledger_pid = parent.ledger_id; node.level = parent.level + 1; node.order = parent.children.length + 1; node.full_path = parent.full_path + '-' + node.ledger_id; parent.children.push(node); node.children = []; if (this.defaultData) _.assignIn(node, this.defaultData); this.items.push(node); if (!_.isNil(node.b_code) && node.b_code !== '') { this.codeNodes[node.b_code] = node; } else { this.blankParent = node; } return node; } /** * 添加 项目节 * @param {Object} node - 项目节 * @returns {*} */ addNode(node) { if (_.isNil(node.b_code) || node.b_code === '') { return this.addNodeWithParent(node, null); } else if (node.b_code.split(this.splitChar).length > 1) { const parent = this.findParent(node.b_code); return this.addNodeWithParent(node, parent ? parent : this.blankParent); } else { return this.addNodeWithParent(node, this.blankParent); } } } class AnalysisGclExcelTree { /** * 构造函数 */ constructor(ctx) { this.ctx = ctx; this.colsDef = null; this.colHeaderMatch = { b_code: {value: ['编号', '清单编号', '子目号', '子目编号', '清单号'], type: colDefineType.match}, name: {value: ['名称', '清单名称', '子目名称'], type: colDefineType.match}, unit: {value: ['单位'], type: colDefineType.pos}, quantity: {value: ['数量'], type: colDefineType.pos}, // 施工图复核数量 unit_price: {value: ['单价'], type: colDefineType.pos}, }; } /** * 读取表头并检查 * @param {Number} row - Excel数据行 */ checkColHeader(row) { const colsDef = aeUtils.checkColHeader(row, this.colHeaderMatch); if (colsDef.b_code) { this.colsDef = colsDef; } } loadRowData(row) { const node = {}; node.b_code = this.ctx.helper.replaceReturn(row[this.colsDef.b_code]); node.name = this.ctx.helper.replaceReturn(row[this.colsDef.name]); if ((_.isNil(node.b_code) || node.b_code === '') && (_.isNil(node.name) || node.name === '')) return node; node.unit = this.ctx.helper.replaceReturn(row[this.colsDef.unit]); const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, node.unit); node.quantity = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.quantity]), precision.value); node.unit_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.unit_price]), this.ctx.tender.info.decimal.up); if (node.quantity && node.unit_price) { node.total_price = this.ctx.helper.mul(node.quantity, node.unit_price, this.ctx.tender.info.decimal.tp); } else { node.total_price = null; } return this.cacheTree.addNode(node); } /** * 将excel清单 平面数据 解析为 树结构数据 * @param {object} sheet - excel清单数据 * @param {Array} parentId - 导入至的节点id * @returns {ImportBaseTree} */ analysisData(sheet, parent, maxId, defaultData) { try { this.colsDef = null; this.cacheTree = new ImportGclBaseTree(this.ctx, parent, maxId, defaultData); this.errorData = []; this.loadEnd = false; // 识别表头导入 for (const iRow in sheet.rows) { const row = sheet.rows[iRow]; if (this.colsDef && !this.loadEnd) { const result = this.loadRowData(row); // 读取失败则写入错误数据 if (!result) { this.errorData.push({ serialNo: iRow, data: row }); } } else { this.checkColHeader(row); } } // 固定列导入 // this.colsDef = { // b_code: 0, // name: 1, // unit: 2, // quantity: 3, // unit_price: 4 // }; // for (let iRow = 1, iLen = sheet.rows.length; iRow < iLen; iRow++) { // const row = sheet.rows[iRow]; // const result = this.loadRowData(row); // // 读取失败则写入错误数据 todo 返回前端告知用户? // if (!result) { // this.errorData.push({ // serialNo: iRow, // data: row, // }); // } // } return this.cacheTree; } catch(err) { this.ctx.helper.log(err); } } } module.exports = { AnalysisExcelTree, AnalysisGclExcelTree };