Browse Source

导入计价文件,暂定版本1.0

MaiXinRong 6 months atrás
parent
commit
7281186aa4

+ 16 - 0
app/controller/ledger_controller.js

@@ -24,6 +24,7 @@ const stdConst = require('../const/standard');
 const sendToWormhole = require('stream-wormhole');
 const spreadSetting = require('../lib/spread_setting');
 const PermissionCheck = require('../const/account_permission').PermissionCheck;
+const streamToArray = require('stream-to-array');
 
 module.exports = app => {
 
@@ -545,6 +546,21 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
         }
+
+        async uploadYbp(ctx) {
+            const stream = await ctx.getFileStream();
+            try {
+                // 读取字节流
+                const parts = await streamToArray(stream);
+                // 转化为buffer
+                const buffer = Buffer.concat(parts);
+                const bills = await ctx.service.ledger.importYbp(ctx.tender, buffer.toString());
+                ctx.body = { err: 0, msg: '', data: { bills, pos: [] }};
+            } catch (err) {
+                await sendToWormhole(stream);
+                ctx.log(err);
+            }
+        }
         // async uploadExcel(ctx) {
         //     let stream;
         //     try {

+ 74 - 0
app/lib/ybp.js

@@ -0,0 +1,74 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const CryptoJS = require('crypto-js');
+const LzString = require('lz-string');
+const { decode } = require('js-base64');
+
+const _interopDefaultLegacy = function(e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; };
+const CryptoJS__default = /* #__PURE__*/_interopDefaultLegacy(CryptoJS);
+const key = CryptoJS__default.default.enc.Utf8.parse('7563850888ABCDEF'); // 十六位十六进制数作为密钥
+const iv = CryptoJS__default.default.enc.Utf8.parse('ABCDEF7563850888'); // 十六位十六进制数作为密钥偏移量
+
+const fs = require('fs');
+const ybpSpr = '|----|', rationSpr = '|====|';
+
+const Decrypt = function(str) {
+    const encryptedHexStr = CryptoJS__default.default.enc.Hex.parse(str);
+    const src = CryptoJS__default.default.enc.Base64.stringify(encryptedHexStr);
+    const decrypt = CryptoJS__default.default.AES.decrypt(src, key, {
+        iv,
+        mode: CryptoJS__default.default.mode.CBC,
+        padding: CryptoJS__default.default.pad.Pkcs7,
+    });
+    const decryptedStr = decrypt.toString(CryptoJS__default.default.enc.Utf8);
+    return decryptedStr.toString();
+};
+
+class ybp {
+    parseSubject(str) {
+        if (str.indexOf(rationSpr) >= 0) {
+            const arr = str.split(rationSpr);
+            const subject = JSON.parse(LzString.decompressFromBase64(arr[0]));
+            for (let i = 1; i < arr.length; i++) {
+                const ration = JSON.parse(LzString.decompressFromBase64(arr[i]));
+                subject.rations.push(...ration);
+            }
+            return subject;
+        } else {
+            return JSON.parse(LzString.decompressFromBase64(str));
+        }
+    }
+    decryptBuffer(buffer) {
+        const arr = buffer.split(ybpSpr);
+        const result = { header: '', subjects: [] };
+        arr.forEach((s, i) => {
+            if (i === 0) {
+                result.header = JSON.parse(Decrypt(s));
+            } else {
+                switch (result.header.version) {
+                    case '3.0':
+                        result.subjects.push(this.parseSubject(s));
+                        break;
+                    default:
+                        result.subjects.push(JSON.parse(Decrypt(s)));
+                        break;
+                }
+            }
+        });
+        return result;
+    }
+    async decrypt(file) {
+        const str = await fs.readFileSync(file, 'utf-8');
+        return this.decryptBuffer(str);
+    }
+}
+
+module.exports = ybp;

+ 299 - 0
app/lib/ybp_tree.js

@@ -0,0 +1,299 @@
+'use strict';
+const YbpNodeKind = {
+    dxfy: 1, // 大项费用
+    fb: 2, // 分部
+    fx: 3, // 分项
+    bill: 4, // 清单
+    bx: 5, // 补项
+    cs: 6, // 分类
+    dt: 7, // 费用明细
+};
+const defaultMerge = [ YbpNodeKind.dxfy, YbpNodeKind.fb, YbpNodeKind.cs ];
+const YbpImportType = { flow: 0, merge: 1 };
+
+class YbpTree {
+    /**
+     *
+     * @param setting - {Object}配置
+     * setting中必须设置id,pid,order,rootId(id, 父id, 同层排序, 根节点id)
+     * e.g.{id: 'ID', pid: 'parentID', order: 'seq'}
+     * 目前仅用于载入建筑ybp文件中的清单部分,生成树结构以便汇总导入
+     */
+    constructor(setting) {
+        this.setting = JSON.parse(JSON.stringify(setting));
+        this.clearDatas();
+    }
+    clearDatas() {
+        // 数据集合
+        this.datas = [];
+        // id索引
+        this.items = {};
+        // 排序索引
+        this.nodes = [];
+        // 首层节点
+        this.children = [];
+    }
+
+    sortChildren(children, recursive) {
+        const setting = this.setting;
+        children.sort((x, y) => { return x[setting.order] > y[setting.order]; });
+
+        if (!recursive) return;
+        for (const c of children) {
+            this.sortChildren(c.children);
+        }
+    }
+    sort() {
+        this.sortChildren(this.children, true);
+        const self = this;
+        const _loadNode = function(node) {
+            self.nodes.push(node);
+            for (const c of node.children) {
+                _loadNode(c);
+            }
+        };
+        for (const child of this.children) {
+            _loadNode(child);
+        }
+    }
+    loadDatas(datas) {
+        this.clearDatas();
+        const setting = this.setting;
+        const self = this;
+        const _loadData = function(d) {
+            if (d[setting.pid] === setting.rootId) {
+                self.children.push(d);
+            } else {
+                let parent = self.items[d[setting.pid]];
+                if (!parent) {
+                    parent = datas.find(x => { return x[setting.id] === d[setting.pid]; });
+                    if (!parent) {
+                        // console.log(d[setting.pid]);
+                        return null;
+                    }
+                    parent = _loadData(parent);
+                }
+                if (!parent) return null;
+                parent.children.push(d);
+            }
+            d.children = [];
+            self.datas.push(d);
+            self.items[d[setting.id]] = d;
+            return d;
+        };
+        for (const d of datas) {
+            if (this.items[d[setting.id]]) continue;
+            _loadData(d);
+        }
+        this.sort();
+    }
+}
+
+class YbpImportTree {
+    /**
+     *
+     * @param {Object} setting - 树结构配置
+     * @param {YbpImportType} type - 导入类型
+     * @param {Object} helper - this.ctx.helper
+     *
+     * setting中必须设置id, pid, level, order, full_path, rootId(id, 父id, 层次, 同层排序, 完整路径, 根节点id)
+     *
+     */
+    constructor(setting, type, helper, decimal) {
+        this.setting = setting;
+        this.importType = type;
+        this.helper = helper;
+        this.decimal = decimal;
+        this.clearDatas();
+    }
+    set newId(num) {
+        this._newId = num;
+    }
+    get newId() {
+        this._newId++;
+        return this._newId;
+    }
+    loadTemplate(template) {
+        if (!template || template.length === 0) return;
+
+        this.clearDatas();
+        const setting = this.setting;
+        const self = this;
+        const _loadData = function(d) {
+            d.children = [];
+            self.datas.push(d);
+            self.items[d[setting.id]] = d;
+            if (d[setting.pid] === setting.rootId) {
+                self.children.push(d);
+            } else {
+                const parent = self.items[d[setting.pid]];
+                if (!parent) {
+                    const parent = self.datas.find(x => { return x[setting.id] === d[setting.pid]; });
+                    if (!parent) throw '找不到父项';
+                    _loadData(parent);
+                }
+                parent.children.push(d);
+            }
+        };
+        for (const t of template) {
+            if (this.items[t[setting.id]]) continue;
+            _loadData(t);
+        }
+    }
+    clearDatas() {
+        // 数据集合
+        this.datas = [];
+        // id索引
+        this.items = {};
+        // 排序索引
+        this.nodes = [];
+        // 首层节点
+        this.children = [];
+        // 新增节点id
+        this.newId = 100;
+    }
+    _findNode(data, parent) {
+        const children = parent ? parent.children : this.children;
+        return this.helper._.find(children, data);
+        // return children.find(x => {
+        // return children.find(x => {
+        //   return x.kind === data.kind &&
+        //       x.code === data.code && x.b_code === data.b_code && x.name === data.name && x.unit === data.unit &&
+        //       x.unit_price === data.unit_price;
+        // });
+    }
+    _importNode(node, parent, loadRelaFun) {
+        const setting = this.setting;
+        const compareData = { kind: node.kind };
+        const hasUp = (!node.children || node.children.length === 0) && defaultMerge.indexOf(compareData.kind) < 0;
+        compareData.code = defaultMerge.indexOf(compareData.kind) < 0 ? '' : node.code || '';
+        compareData.b_code = defaultMerge.indexOf(compareData.kind) < 0 ? node.code || '' : '';
+        compareData.name = node.name || '';
+        compareData.unit = node.unit || '';
+        compareData.unit_price = hasUp
+            ? (node.fees && node.fees.marketCommon ? node.fees.marketCommon.unitPrice : 0)
+            : 0;
+
+        let cur = (this.importType === YbpImportType.merge || defaultMerge.indexOf(compareData.kind) >= 0)
+            ? this._findNode(compareData, parent)
+            : null;
+        if (!cur) {
+            cur = compareData;
+            cur.children = [];
+            cur.source = [];
+            cur[setting.id] = this.newId;
+            cur[setting.pid] = parent ? parent[setting.id] : setting.rootId;
+            cur[setting.level] = parent ? parent[setting.level] + 1 : 1;
+            cur[setting.full_path] = parent ? `${parent[setting.full_path]}-${cur[setting.id]}` : `${cur[setting.id]}`;
+            if (parent) {
+                parent.children.push(cur);
+            } else {
+                this.children.push(cur);
+            }
+            this.datas.push(cur);
+            cur.quantity = node.quantity || 0;
+            cur.total_fee = node.fees ? node.fees.marketCommon.totalFee : 0;
+            cur.labour_fee = node.fees && node.fees.marketLabour ? node.fees.marketLabour.totalFee : 0;
+        } else {
+            cur.quantity = this.helper.add(cur.quantity, node.quantity);
+            cur.total_fee = this.helper.add(cur.total_fee, node.fees ? node.fees.marketCommon.totalFee : 0);
+            cur.labour_fee = this.helper.add(cur.labour_fee, node.fees && node.fees.marketLabour ? node.fees.marketLabour.totalFee : 0);
+        }
+        if (loadRelaFun) loadRelaFun(cur, node);
+        cur.source.push(this.unitName);
+        for (const c of node.children) {
+            this._importNode(c, cur, loadRelaFun);
+        }
+    }
+    importTree(tree, unitName, loadRelaFun) {
+        this.unitName = unitName;
+        for (const n of tree.children) {
+            this._importNode(n, null, loadRelaFun);
+        }
+    }
+
+    sortChildren(children, recursive) {
+        children.sort((x, y) => {
+            return x.kind === YbpNodeKind.bill
+                ? (x.b_code ? x.b_code.localeCompare(y.b_code) : -1)
+                : (x.code ? x.code.localeCompare(y.code) : -1);
+        });
+        children.forEach((c, i) => { c.order = i + 1; });
+
+        if (!recursive) return;
+        for (const c of children) {
+            this.sortChildren(c.children, recursive);
+        }
+    }
+    generateSortNodes() {
+        const self = this;
+        const _loadNode = function(node) {
+            self.nodes.push(node);
+            for (const c of node.children) {
+                _loadNode(c);
+            }
+        };
+        for (const child of this.children) {
+            _loadNode(child);
+        }
+    }
+    sort() {
+        this.sortChildren(this.children, true);
+        this.generateSortNodes();
+    }
+    _mapTreeNode() {
+        let map = {},
+            maxLevel = 0;
+        for (const node of this.nodes) {
+            let levelArr = map[node.level];
+            if (!levelArr) {
+                levelArr = [];
+                map[node.level] = levelArr;
+            }
+            if (node.level > maxLevel) {
+                maxLevel = node.level;
+            }
+            levelArr.push(node);
+        }
+        return [ maxLevel, map ];
+    }
+    _calculateNode(node, fun) {
+        const self = this;
+        if (node.children && node.children.length > 0) {
+            const gather = node.children.reduce(function(rst, x) {
+                const result = {};
+                for (const cf of self.setting.calcFields) {
+                    result[cf] = self.helper.add(rst[cf], x[cf]);
+                }
+                return result;
+            });
+            // 汇总子项
+            for (const cf of this.setting.calcFields) {
+                if (gather[cf]) {
+                    node[cf] = gather[cf];
+                } else {
+                    node[cf] = null;
+                }
+            }
+        }
+        // 自身运算
+        if (fun) fun(node, this.helper, this.decimal);
+    }
+    calculateAll(fun) {
+        const [ maxLevel, levelMap ] = this._mapTreeNode();
+        for (let i = maxLevel; i >= 0; i--) {
+            const levelNodes = levelMap[i];
+            if (levelNodes && levelNodes.length > 0) {
+                for (const node of levelNodes) {
+                    this._calculateNode(node, fun || this.setting.calc);
+                }
+            }
+        }
+    }
+}
+
+module.exports = {
+    YbpImportType,
+    YbpTree,
+    YbpImportTree,
+};

+ 16 - 0
app/public/js/ledger.js

@@ -4962,6 +4962,22 @@ $(document).ready(function() {
           $('#showAttachment').attr('file-id', '');
       });
   });
+    $('#upload-ybp-file').click(function () {
+        const file = $('#ybp-file')[0];
+        const formData = new FormData();
+        formData.append('file', file.files[0]);
+        postDataWithFile('ledger/ybp', formData, function (result) {
+            ledgerTree.loadDatas(result.bills);
+            treeCalc.calculateAll(ledgerTree);
+            SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
+            pos.loadDatas(result.pos);
+            posOperationObj.loadCurPosData();
+            checkShowLast(result.bills.length);
+            $('#upload-ybp').modal('hide');
+        }, function () {
+            $('#upload-ybp').modal('hide');
+        });
+    });
 });
 // 生成当前节点列表
 function getNodeList(node) {

+ 1 - 0
app/router.js

@@ -241,6 +241,7 @@ module.exports = app => {
     app.post('/tender/:id/ledger/get-children', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.getChildren');
     app.post('/tender/:id/ledger/update', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.update');
     app.post('/tender/:id/ledger/upload-excel/:ueType', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.uploadExcel');
+    app.post('/tender/:id/ledger/ybp', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.uploadYbp');
     app.get('/tender/:id/ledger/download/:file', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.download');
     app.post('/tender/:id/anc-gcl/update', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.ancGclUpdate');
     app.post('/tender/:id/pos/update', sessionAuth, tenderCheck, uncheckTenderCheck, 'ledgerController.posUpdate');

+ 101 - 0
app/service/ledger.js

@@ -707,6 +707,107 @@ module.exports = app => {
             }
         }
 
+        async importYbp(tender, ybp) {
+            const YBP = require('../lib/ybp');
+            const YbpTrees = require('../lib/ybp_tree');
+            const gatherTreeSetting = {
+                id: 'ledger_id', pid: 'ledger_pid', order: 'order', full_path: 'full_path', level: 'level', rootId: -1,
+                calcFields: [ 'total_price' ],
+                calc(node, helper, decimal) {
+                    if (!node.children || node.children.length === 0) {
+                        node.total_price = helper.mul(node.quantity, node.unit_price, decimal.tp);
+                    }
+                },
+            };
+            const ybpTreeSetting = { id: 'ID', pid: 'parentID', order: 'seq', rootId: '-1' };
+            const helper = this.ctx.helper;
+
+            const ybpAnalysis = new YBP(this.ctx);
+            const ybpData = ybpAnalysis.decryptBuffer(ybp);
+            const gatherTree = new YbpTrees.YbpImportTree(gatherTreeSetting, YbpTrees.YbpImportType.flow, helper, tender.info.decimal);
+            for (const subject of ybpData.subjects) {
+                if (!subject.bills || subject.bills.length === 0) continue;
+
+                const ybpTree = new YbpTrees.YbpTree(ybpTreeSetting);
+                ybpTree.loadDatas(subject.bills);
+                const loadRelaFun = function (cur, node) {
+                    if (node.children && node.children.length > 0) return;
+
+                    const rations = subject.rations.filter(x => { return x.parentID === node.ID; });
+                    if (rations.length === 0) return;
+
+                    if (!cur.glj) cur.glj = [];
+                    for (const r of rations) {
+                        const gljs = r.gljList || r.rationGljList;
+                        if (!gljs || gljs.length === 0) continue;
+                        for (const g of gljs) {
+                            let curGlj = cur.glj.find(x => {
+                                if (x.projectGljID && g.projectGljID) return x.projectGljID === g.projectGljID;
+                                return x.code === g.code && x.name === g.name && x.unit === g.unit && x.unit_price === g.unit_price;
+                            });
+                            if (!curGlj) {
+                                curGlj = { code: g.code, name: g.name, unit: g.unit, unit_price: g.unit_price, spec: g.specs, projectGljID: g.projectGljID };
+                                cur.glj.push(curGlj);
+                            }
+                            curGlj.quantity = helper.add(curGlj.quantity, g.quantity);
+                        }
+                    }
+                };
+                gatherTree.importTree(ybpTree, subject.project.name, loadRelaFun);
+            }
+            gatherTree.sort();
+            gatherTree.calculateAll();
+
+            const bills = [], glj = [];
+            for (const n of gatherTree.nodes) {
+                const billsId = this.uuid.v4();
+                bills.push({
+                    id: billsId,
+                    tender_id: tender.id,
+                    ledger_id: n.ledger_id,
+                    ledger_pid: n.ledger_pid,
+                    level: n.level,
+                    order: n.order,
+                    full_path: n.full_path,
+                    is_leaf: !n.children || n.children.length === 0,
+                    code: n.code,
+                    b_code: n.b_code,
+                    name: n.name,
+                    unit: n.unit,
+                    unit_price: n.unit_price || 0,
+                    sgfh_qty: n.quantity || 0,
+                    sgfh_tp: n.total_price || 0,
+                    quantity: n.quantity || 0,
+                    total_price: n.total_price || 0,
+                    memo: n.source.join(';'),
+                });
+                if (!n.glj) continue;
+                for (const g of n.glj) {
+                    glj.push({
+                        tid: tender.id,
+                        lid: billsId,
+                        code: g.code || '',
+                        name: g.name || '',
+                        unit: g.unit || '',
+                        spec: g.spec || '',
+                        quantity: helper.div(g.quantity, n.quantity, 4),
+                    })
+                }
+            }
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.ctx.service.ledger.tableName, { tender_id: tender.id });
+                await conn.delete(this.ctx.service.pos.tableName, { tid: tender.id });
+                await conn.insert(this.ctx.service.ledger.tableName, bills);
+                // if (glj.length > 0) await conn.insert(this.ctx.service.ledgerGlj.tableName, glj);
+                await conn.commit();
+                return bills;
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+
         async sumLoad(lid, tenders) {
             const maxId = await this._getMaxLid(this.ctx.tender.id);
             const select = await this.getDataById(lid);

+ 3 - 0
app/view/ledger/explode.ejs

@@ -42,6 +42,9 @@
                 <div class="d-inline-block ml-3">
                     <a id="exportLedger" class="btn btn-primary btn-sm" href="javascript: void(0)">导出台账Excel</a>
                 </div>
+                <div class="d-inline-block ml-1">
+                    <a href="#upload-ybp" data-toggle="modal" data-target="#upload-ybp" class="btn btn-sm btn-primary">导入计价文件</a>
+                </div>
                 <% if (syncLedgerUrl) { %>
                 <div class="d-inline-block ml-1">
                     <a id="sync-ledger" class="btn btn-primary btn-sm" href="javascript: void(0)">同步台账</a>

+ 20 - 0
app/view/ledger/explode_modal.ejs

@@ -55,6 +55,26 @@
         </div>
     </div>
 </div>
+<!--上传计价文件-->
+<div class="modal fade" id="upload-ybp" data-backdrop="static" enctype="multipart/form-data">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入计价文件</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="exampleFormControlFile1">导入计价YBPX/YUP文件</label>
+                    <input type="file" class="form-control-file" id="ybp-file">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary btn-sm" id="upload-ybp-file">确认上传</button>
+            </div>
+        </div>
+    </div>
+</div>
 <!--批量添加清单部位-->
 <div class="modal fade" id="batch" data-backdrop="static">
     <div class="modal-dialog modal-xl" role="document">

+ 1 - 1
config/config.default.js

@@ -124,7 +124,7 @@ module.exports = appInfo => {
 
     // 上传设置
     config.multipart = {
-        whitelist: ['.cpd',
+        whitelist: ['.cpd', '.yup', '.ybpx',
             '.json', '.txt',
             '.xls', '.xlsx',
             '.doc', '.docx',

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
         "gt3-sdk": "^2.0.0",
         "gulp": "^4.0.0",
         "hpack.js": "^2.1.6",
+        "js-base64": "^3.7.7",
         "js-xlsx": "^0.8.22",
         "jsonwebtoken": "^8.5.1",
         "jszip": "^3.1.3",