浏览代码

Merge branch 'master' of http://192.168.1.41:3000/maixinrong/Calculation

TonyKang 5 年之前
父节点
当前提交
6cd1920b8d
共有 43 个文件被更改,包括 4129 次插入2983 次删除
  1. 73 1
      app/base/base_tree_service.js
  2. 33 10
      app/controller/change_controller.js
  3. 24 2
      app/controller/deal_bills_controller.js
  4. 1 1
      app/controller/ledger_controller.js
  5. 4 0
      app/controller/report_controller.js
  6. 15 1
      app/controller/revise_controller.js
  7. 1 1
      app/controller/tender_controller.js
  8. 54 11
      app/extend/helper.js
  9. 1 1
      app/public/css/main.css
  10. 1 1
      app/public/js/change_approval.js
  11. 14 14
      app/public/js/change_set.js
  12. 182 0
      app/public/js/file-saver/FileSaver.js
  13. 3 0
      app/public/js/global.js
  14. 121 8
      app/public/js/ledger.js
  15. 2 2
      app/public/js/ledger_search.js
  16. 1 1
      app/public/js/number-precision.js
  17. 345 24
      app/public/js/revise.js
  18. 158 0
      app/public/js/shares/merge_peg.js
  19. 1 1
      app/public/js/spreadjs_rela/spreadjs_zh.js
  20. 30 8
      app/public/js/stage.js
  21. 1 1
      app/public/js/stage_compare.js
  22. 1 0
      app/router.js
  23. 7 3
      app/service/change.js
  24. 14 0
      app/service/change_audit_list.js
  25. 48 0
      app/service/report_memory.js
  26. 6 6
      app/service/revise_bills.js
  27. 3 3
      app/view/change/index.ejs
  28. 8 8
      app/view/change/info.ejs
  29. 9 7
      app/view/change/info_modal.ejs
  30. 3 0
      app/view/layout/layout.ejs
  31. 4 1
      app/view/ledger/audit.ejs
  32. 1 0
      app/view/ledger/explode.ejs
  33. 1 0
      app/view/ledger/explode_modal.ejs
  34. 4 18
      app/view/measure/stage_modal.ejs
  35. 21 7
      app/view/revise/info.ejs
  36. 1 0
      app/view/revise/info_modal.ejs
  37. 45 0
      app/view/shares/merge_peg_modal.ejs
  38. 1 0
      app/view/stage/modal.ejs
  39. 1 0
      app/view/tender/detail_modal.ejs
  40. 2 0
      config/config.qa.js
  41. 5 0
      config/web.js
  42. 2867 2840
      package-lock.json
  43. 12 2
      test/app/service/report_memory.test.js

+ 73 - 1
app/base/base_tree_service.js

@@ -160,6 +160,32 @@ class TreeService extends Service {
         return order instanceof Array ? result : (result.length > 0 ? result[0] : null);
     }
 
+    async getChildBetween(mid, pid, order1, order2) {
+        this.initSqlBuilder();
+        this.sqlBuilder.setAndWhere(this.setting.mid, {
+            value: mid,
+            operate: '=',
+        });
+        this.sqlBuilder.setAndWhere(this.setting.pid, {
+            value: pid,
+            operate: '=',
+        });
+        this.sqlBuilder.setAndWhere(this.setting.order, {
+            value: order1,
+            operate: '>',
+        });
+        this.sqlBuilder.setAndWhere(this.setting.order, {
+            value: order2,
+            operate: '<',
+        });
+        this.sqlBuilder.orderBy = [['order', 'ASC']];
+
+        const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+        const data = await this.db.query(sql, sqlParam);
+
+        return data;
+    }
+
     /**
      * 根据 父节点ID 和 节点排序order 获取全部后节点数据
      * @param {Number} mid - master id
@@ -398,7 +424,7 @@ class TreeService extends Service {
         try {
             if (select) await this._updateChildrenOrder(mid, select[this.setting.pid], select[this.setting.order]+1);
             const newNode = await this._addNodeData(mid, select, data);
-            if (newNode.affectedRows !== 1) throw '新增节点数据错误';
+            if (newNode.affectedRows !== 1) throw '新增节点数据错误';
             await this.transaction.commit();
             this.transaction = null;
         } catch (err) {
@@ -417,6 +443,52 @@ class TreeService extends Service {
         }
     }
 
+    async addNodeBatch(mid, kid, data, count = 1) {
+        if (!mid) return null;
+        const select = kid ? await this.getDataByKid(mid, kid) : null;
+        if (kid && !select) throw '新增节点数据错误';
+
+        this.transaction = await this.db.beginTransaction();
+        try {
+            if (select) await this._updateChildrenOrder(mid, select[this.setting.pid], select[this.setting.order] + 1, count);
+            const newDatas = [];
+            const maxId = await this._getMaxLid(mid);
+            for (let i = 1; i < count + 1; i++) {
+                const newData = Object.assign({}, data);
+                if (this.setting.uuid) newData.id = this.uuid.v4();
+                newData[this.setting.kid] = maxId + i;
+                newData[this.setting.pid] = select ? select[this.setting.pid] : rootId;
+                newData[this.setting.mid] = mid;
+                newData[this.setting.level] = select ? select[this.setting.level] : 1;
+                newData[this.setting.order] = select ? select[this.setting.order] + i : i;
+                newData[this.setting.fullPath] = newData[this.setting.level] > 1
+                    ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
+                    : newData[this.setting.kid] + '';
+                newData[this.setting.isLeaf] = true;
+                newDatas.push(newData);
+            }
+            const insertResult = await this.transaction.insert(this.tableName, newDatas);
+            this._cacheMaxLid(mid, maxId + count);
+
+            if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+            await this.transaction.commit();
+            this.transaction = null;
+        } catch (err) {
+            await this.transaction.rollback();
+            this.transaction = null;
+            throw err;
+        }
+
+        if (select) {
+            const createData = await this.getChildBetween(mid, select[this.setting.pid], select[this.setting.order], select[this.setting.order] + count + 1);
+            const updateData = await this.getNextsData(mid, select[this.setting.pid], select[this.setting.order] + count);
+            return {create: createData, update: updateData};
+        } else {
+            const createData = await this.getChildBetween(mid, -1, 0, count + 1);
+            return {create: createData};
+        }
+    }
+
     /**
      * 删除相关数据 用于继承
      * @param mid

+ 33 - 10
app/controller/change_controller.js

@@ -342,8 +342,8 @@ module.exports = app => {
                             cl.detail,
                             cl.lid,
                         ];
-                        ototalCost += ctx.helper.mul(cl.unit_price, cl.oamount, ctx.tender.info.decimal.tp);
-                        ctotalCost += ctx.helper.mul(cl.unit_price, cl.camount, ctx.tender.info.decimal.tp);
+                        ototalCost += cl.unit_price === null ? 0 : ctx.helper.mul(cl.unit_price, cl.oamount, ctx.tender.info.decimal.tp);
+                        ctotalCost += cl.unit_price === null ? 0 : ctx.helper.mul(cl.unit_price, cl.camount, ctx.tender.info.decimal.tp);
                         if (cl.lid !== 0) {
                             changeListData.push(cLArray.join(';'));
                         } else {
@@ -388,9 +388,9 @@ module.exports = app => {
                     let stotalCost = 0;
                     const auditTotalCost = [];
                     for (const cl of changeList) {
-                        ototalCost += parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.oamount), renderData.tpUnit));
-                        ctotalCost += parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.camount), renderData.tpUnit));
-                        stotalCost += cl.samount !== '' ? parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.samount), renderData.tpUnit)) : 0;
+                        ototalCost += cl.unit_price === null ? 0 : parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.oamount), renderData.tpUnit));
+                        ctotalCost += cl.unit_price === null ? 0 : parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.camount), renderData.tpUnit));
+                        stotalCost += cl.samount !== '' && cl.unit_price !== null ? parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.samount), renderData.tpUnit)) : 0;
                         const audit_amount = cl.audit_amount !== null && cl.audit_amount !== '' ? cl.audit_amount.split(',') : '';
                         auditTotalCost.push(audit_amount);
                     }
@@ -405,7 +405,7 @@ module.exports = app => {
                             au.totalCost = 0;
                             for (const [auindex, at] of auditTotalCost.entries()) {
                                 au.list_amount.push(at[index - 1]);
-                                au.totalCost += at[index - 1] !== undefined ? parseFloat(ctx.helper.roundNum(ctx.helper.accMul(changeList[auindex].unit_price, at[index - 1]), renderData.tpUnit)) : 0;
+                                au.totalCost += at[index - 1] !== undefined && changeList[auindex].unit_price !== null ? parseFloat(ctx.helper.roundNum(ctx.helper.accMul(changeList[auindex].unit_price, at[index - 1]), renderData.tpUnit)) : 0;
                             }
                         }
                     }
@@ -425,8 +425,8 @@ module.exports = app => {
                     const auditTotalCost = [];
                     const auditUnit = [];
                     for (const cl of changeList) {
-                        ototalCost += parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.oamount), renderData.tpUnit));
-                        ctotalCost += parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.camount), renderData.tpUnit));
+                        ototalCost += cl.unit_price === null ? 0 : parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.oamount), renderData.tpUnit));
+                        ctotalCost += cl.unit_price === null ? 0 : parseFloat(ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.camount), renderData.tpUnit));
                         const audit_amount = cl.audit_amount !== null && cl.audit_amount !== '' ? cl.audit_amount.split(',') : '';
                         auditTotalCost.push(audit_amount);
                     }
@@ -448,12 +448,12 @@ module.exports = app => {
                                     //     au.totalCost += parseFloat(ctx.helper.roundNum(ctx.helper.accMul(changeList[auindex].unit_price, changeList[auindex].camount), renderData.tpUnit));
                                     // }
                                     au.list_amount.push(changeList[auindex].spamount);
-                                    au.totalCost += parseFloat(ctx.helper.roundNum(ctx.helper.accMul(changeList[auindex].unit_price, changeList[auindex].spamount), renderData.tpUnit));
+                                    au.totalCost += changeList[auindex].unit_price === null ? 0 : parseFloat(ctx.helper.roundNum(ctx.helper.accMul(changeList[auindex].unit_price, changeList[auindex].spamount), renderData.tpUnit));
                                 }
                             } else {
                                 for (const [auindex, at] of auditTotalCost.entries()) {
                                     au.list_amount.push(at[index - 1]);
-                                    au.totalCost += at[index - 1] !== undefined ? parseFloat(ctx.helper.roundNum(ctx.helper.accMul(changeList[auindex].unit_price, at[index - 1]), renderData.tpUnit)) : 0;
+                                    au.totalCost += at[index - 1] !== undefined && changeList[auindex].unit_price !== null ? parseFloat(ctx.helper.roundNum(ctx.helper.accMul(changeList[auindex].unit_price, at[index - 1]), renderData.tpUnit)) : 0;
                                 }
                             }
                         }
@@ -746,6 +746,29 @@ module.exports = app => {
                 ctx.redirect(ctx.request.header.referer);
             }
         }
+
+        /**
+         * 获取变更清单
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async bills(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const responseData = {err: 0, msg: '', data: []};
+                switch (data.type) {
+                    case 'gather':
+                        responseData.data = await ctx.service.changeAuditList.gatherBgBills(ctx.tender.id);
+                        break;
+                    default:
+                        throw '查询的数据不存在';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                this.ajaxErrorBody(err, '获取变更清单失败');
+            }
+        }
     }
 
     return ChangeController;

+ 24 - 2
app/controller/deal_bills_controller.js

@@ -105,12 +105,34 @@ module.exports = app => {
                     let fileName;
                     if (file === '签约清单导入格式.xls') {
                         fileName = this.app.baseDir + '/app/public/deal_bills/template.xls';
-                    } else {
+                        ctx.body = await fs.readFileSync(fileName);
+                    } else if (file === '签约清单.xlsx') {
                         const create_time = Date.parse(new Date()) / 1000;
                         fileName = this.app.baseDir + '/app/public/deal_bills/downloads/' + ctx.tender.id + '-' + create_time + '.xlsx';
+                        await this.ctx.helper.recursiveMkdirSync(this.app.baseDir + '/app/public/deal_bills/downloads');
                         // todo 导出签约清单Excel
+                        const setting = {
+                            header: ['清单编号', '名称', '单位', '数量', '单价', '金额'],
+                            width: [80, 150, 60, 80, 80, 80],
+                            hAlign: ['left', 'left', 'center', 'right', 'right', 'right'],
+                        };
+                        const dealBills = await ctx.service.dealBills.getAllDataByCondition({ where: { tender_id: ctx.tender.id } });
+                        const data = [];
+                        for (const db of dealBills) {
+                            data.push([db.code, db.name, db.unit, db.quantity, db.unit_price, db.total_price]);
+                        }
+                        const arraySheetData = this.ctx.helper.simpleXlsxSheetData(setting, data);
+                        console.log(arraySheetData);
+                        xlsx.writeFile({
+                            SheetNames: ['Sheet1'],
+                            Sheets: {
+                                'Sheet1': arraySheetData,
+                            },
+                        }, fileName);
+                        ctx.body = await fs.readFileSync(fileName);
+                        // 输出文件后删除
+                        fs.unlinkSync(fileName);
                     }
-                    ctx.body = await fs.readFileSync(fileName);
                 } catch (err) {
                     this.log(err);
                 }

+ 1 - 1
app/controller/ledger_controller.js

@@ -194,7 +194,7 @@ module.exports = app => {
             }
             switch (type) {
                 case 'add':
-                    return await ctx.service.ledger.addNode(ctx.tender.id, data.id);
+                    return await ctx.service.ledger.addNodeBatch(ctx.tender.id, data.id, {}, data.count);
                 case 'delete':
                     return await ctx.service.ledger.delete(ctx.tender.id, data.id, data.count);
                 case 'up-move':

+ 4 - 0
app/controller/report_controller.js

@@ -355,6 +355,10 @@ async function getReportData(ctx, params, filters) {
                     runnableRst.push(ctx.service.reportMemory.getStageImZlData(params.tender_id, params.stage_id));
                     runnableKey.push('mem_stage_im_zl');
                     break;
+                case 'mem_month_progress':
+                    runnableRst.push(ctx.service.reportMemory.getMonthProgress(params.tender_id));
+                    runnableKey.push('mem_month_progress');
+                    break;
                 default:
                     break;
             }

+ 15 - 1
app/controller/revise_controller.js

@@ -386,7 +386,7 @@ module.exports = app => {
             }
             switch (type) {
                 case 'add':
-                    return await this.ctx.service.reviseBills.addReviseNode(revise.tid, revise.id, data.id);
+                    return await this.ctx.service.reviseBills.addReviseNode(revise.tid, revise.id, data.id, data.count);
                 case 'delete':
                     return await this.ctx.service.reviseBills.delete(revise.tid, data.id, data.count);
                 case 'up-move':
@@ -464,6 +464,17 @@ module.exports = app => {
                 throw '数据错误';
             }
         }
+        async _addBg(revise, data) {
+            if (!data.type || !data.bgBills) throw '数据错误';
+            data.bgBills.unit_price = this.ctx.helper.round(data.bgBills.unit_price, this.ctx.tender.info.decimal.up);
+            if (data.type === 'child') {
+                return await this.ctx.service.reviseBills.addChild(revise.tid, data.id, data.bgBills, revise.id);
+            } else if (data.type === 'next') {
+                return await this.ctx.service.reviseBills.addBillsNode(revise.tid, data.id, data.bgBills, revise.id);
+            } else {
+                throw '数据错误';
+            }
+        }
         async _updatePos(revise, data) {
             await this.checkMeasureType(measureType.tz.value);
             if (!data.posPostType) throw '数据错误';
@@ -517,6 +528,9 @@ module.exports = app => {
                     case 'add-deal':
                         responseData.data = await this._addDeal(revise, data.postData);
                         break;
+                    case 'add-bg':
+                        responseData.data = await this._addDeal(revise, data.postData);
+                        break;
                     case 'add-std':
                         responseData.data = await this._addStd(revise, data.postData);
                         break;

+ 1 - 1
app/controller/tender_controller.js

@@ -284,7 +284,7 @@ module.exports = app => {
                     tender.end_gather_tp = ctx.helper.add(tender.end_contract_tp, tender.end_qc_tp);
                     tender.pre_gather_tp = ctx.helper.add(lastStage.pre_contract_tp, lastStage.pre_qc_tp);
                     tender.yf_tp = lastStage.yf_tp;
-                    tender.qc_ratio = ctx.helper.mul(ctx.helper.div(tender.end_qc_tp, tender.gather_tp, 2), 100);
+                    tender.qc_ratio = ctx.helper.mul(ctx.helper.div(tender.end_qc_tp, ctx.tender.info.deal_param.contractPrice, 2), 100);
                     tender.sum = ctx.helper.add(tender.total_price, tender.end_qc_tp);
                     tender.pre_ratio = ctx.helper.mul(ctx.helper.div(tender.pre_gather_tp, tender.sum, 2), 100);
                     tender.cur_ratio = ctx.helper.mul(ctx.helper.div(tender.gather_tp, tender.sum, 2), 100);

+ 54 - 11
app/extend/helper.js

@@ -82,6 +82,9 @@ module.exports = {
      * @return {string} - 结果
      */
     accMul(arg1, arg2) {
+        if (!arg1 || !arg2) {
+            return '';
+        }
         let m = 0;
         const s1 = arg1.toString();
         const s2 = arg2.toString();
@@ -131,6 +134,9 @@ module.exports = {
 
     // 四舍五入或末尾加零,实现类似php的 sprintf("%.".decimal."f", val);
     roundNum(val, decimals) {
+        if (val === '' || val === null) {
+            return '';
+        }
         if (val !== '') {
             val = parseFloat(val);
             if (decimals < 1) {
@@ -183,12 +189,15 @@ module.exports = {
 
     // 根据单位获取小数位数
     findDecimal(unit) {
-        let value = this.ctx.tender.info.precision.other.value;
-        const changeUnits = this.ctx.tender.info.precision;
-        for (const d in changeUnits) {
-            if (changeUnits[d].unit !== undefined && changeUnits[d].unit === unit) {
-                value = changeUnits[d].value;
-                break;
+        let value = 3;
+        if (unit !== '') {
+            value = this.ctx.tender.info.precision.other.value;
+            const changeUnits = this.ctx.tender.info.precision;
+            for (const d in changeUnits) {
+                if (changeUnits[d].unit !== undefined && changeUnits[d].unit === unit) {
+                    value = changeUnits[d].value;
+                    break;
+                }
             }
         }
         return value;
@@ -490,11 +499,13 @@ module.exports = {
      * @returns {Promise<void>}
      */
     async recursiveMkdirSync(pathName) {
-        const upperPath = path.dirname(pathName);
-        if (!fs.existsSync(upperPath)) {
-            await this.recursiveMkdirSync(upperPath);
+        if (!fs.existsSync(pathName)) {
+            const upperPath = path.dirname(pathName);
+            if (!fs.existsSync(upperPath)) {
+                await this.recursiveMkdirSync(upperPath);
+            }
+            await fs.mkdirSync(pathName);
         }
-        await fs.mkdirSync(pathName);
     },
 
     /**
@@ -630,6 +641,9 @@ module.exports = {
      * @returns {*}
      */
     mul(num1, num2, digit = 6) {
+        if (num1 === '' || num1 === null || num2 === '' || num2 === null) {
+            return 0;
+        }
         return Decimal.mul(num1 ? num1 : 0, num2 ? num2 : 0).toDecimalPlaces(digit).toNumber();
     },
     /**
@@ -774,7 +788,7 @@ module.exports = {
             for (var i = 0; i < strArr.length; i++) {
                 newNum = (i == 0 && strArr[i] == 0 ? "" : (i > 0 && strArr[i] == 0 && strArr[i - 1] == 0 ? "" : changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i]))) + newNum;
             }
-            return newNum;
+            return strArr.length === 2 && newNum.indexOf("一十") !== -1 ? newNum.replace('一十', '十') : newNum;
         }
         let overWan = Math.floor(num / 10000);
         let noWan = num % 10000;
@@ -803,4 +817,33 @@ module.exports = {
             sms.send(mobiles, content);
         }
     },
+
+    /**
+     *
+     * @param setting
+     * @param data
+     * @returns {{} & any & {"!ref": string} & {"!cols"}}
+     */
+    simpleXlsxSheetData (setting, data) {
+        const headerStyle = {
+            font: {bold: true},
+            alignment: {horizontal: 'center'},
+        };
+        const sHeader = setting.header
+            .map((v, i) => Object.assign({}, {v: v, s: headerStyle, position: String.fromCharCode(65+i) + 1 }))
+            .reduce((prev, next) => Object.assign({}, prev, {[next.position]: {v: next.v, s: next.s}}), {});
+        const sData = data
+            .map((v, i) => v.map((k, j) => Object.assign({}, {
+                v: k,
+                s: {alignment: {horizontal: setting.hAlign[j]}},
+                position: String.fromCharCode(65+j) + (i+2) })))
+            .reduce((prev, next) => prev.concat(next))
+            .reduce((prev, next) => Object.assign({}, prev, {[next.position]: {v: next.v, s: next.s}}), {});
+        const output = Object.assign({}, sHeader, sData);
+        const outputPos = Object.keys(output);
+        const result = Object.assign({}, output,
+            {'!ref': outputPos[0] + ':' + outputPos[outputPos.length - 1]},
+            {'!cols': setting.width.map((w) => Object.assign({}, {wpx: w}))});
+        return result;
+    }
 };

+ 1 - 1
app/public/css/main.css

@@ -103,7 +103,7 @@ input.nospin[type="number"]{-moz-appearance:textfield;}
 	background-color: #ced4da;
 	-webkit-border-radius: 0;
 }
-.sjs-height-1,.sjs-height-2,.sjs-sh-1,.sjs-sh-2,.sjs-sh-3,.sjs-sh-4,.sjs-sh-5{
+.sjs-height-1,.sjs-height-2,.sjs-sh,.sjs-sh-1,.sjs-sh-2,.sjs-sh-3,.sjs-sh-4,.sjs-sh-5{
   overflow: hidden;
 }
 .sjs-height-4,.sjs-height-5,.sjs-height-6,.sjs-option-height{

+ 1 - 1
app/public/js/change_approval.js

@@ -28,7 +28,7 @@ $(document).ready(() => {
         const lid = $(this).parents('tr').data('lid');
         const tr = $('#list tr[data-lid="' + lid + '"]').eq(0);
         const unitprice = tr.children('td[data-site="5"]').text();
-        tr.children('.amount_cost').text(amount != '' ?
+        tr.children('.amount_cost').text(amount != '' && unitprice ?
             roundnum(parseFloat(unitprice).mul(parseFloat(amount)),totalPriceUnit) : '');
 
         // 统计总金额

+ 14 - 14
app/public/js/change_set.js

@@ -470,7 +470,7 @@ $(document).ready(() => {
         const code = $.trim(tr.children('td[data-site="0"]').children('input').val()) || $.trim(tr.children('td[data-site="0"]').text());
         const name = $.trim(tr.children('td[data-site="1"]').children('input').val()) || $.trim(tr.children('td[data-site="1"]').text());
         const bwmx = $.trim(tr.children('td[data-site="2"]').children('input').val()) || $.trim(tr.children('td[data-site="2"]').text());
-        const unit = $.trim(tr.children('td[data-site="4"]').children('select').val()) || $.trim(tr.children('td[data-site="4"]').text());
+        const unit = tr.children('td[data-site="4"]').children('select').val() === '' ? tr.children('td[data-site="4"]').children('select').val() : $.trim(tr.children('td[data-site="4"]').children('select').val()) || $.trim(tr.children('td[data-site="4"]').text());
         const price = (tr.children('td[data-site="5"]').children('input').val() != '-' ? tr.children('td[data-site="5"]').children('input').val() : '') || tr.children('td[data-site="5"]').text();
         const oamount = (tr.children('td[data-site="6"]').children('input').val() != '-' ? tr.children('td[data-site="6"]').children('input').val() : '') || tr.children('td[data-site="6"]').text();
         const scnum = tr.children('td[data-site="8"]').children('input').val() != '-' ? tr.children('td[data-site="8"]').children('input').val() : '';
@@ -500,6 +500,7 @@ $(document).ready(() => {
         const name = $.trim(tr.children('td[data-site="1"]').children('input').val());
         const bwmx = $.trim(tr.children('td[data-site="2"]').children('input').val());
         const unit = $(this).val();
+        tr.children('td[data-site="4"]').children('select').val($(this).val());
         const price = tr.children('td[data-site="5"]').children('input').val() != '-' ? tr.children('td[data-site="5"]').children('input').val() : '';
         let oamount = tr.children('td[data-site="6"]').children('input').val() != '-' ? tr.children('td[data-site="6"]').children('input').val() : '';
         let scnum = tr.children('td[data-site="8"]').children('input').val() != '-' ? tr.children('td[data-site="8"]').children('input').val() : '';
@@ -614,12 +615,12 @@ function maketablelist(status){
             '<td data-site="0">'+ code +'</td>' +
             '<td data-site="1">'+ name +'</td>' +
             '<td data-site="2">'+ bwmx +'</td>' +
-            '<td data-site="3"><input class="form-control input-sm" type="text" placeholder="变更详情" value="' + detail + '"></td>' +
+            '<td data-site="3"><input class="form-control form-control-sm" type="text" placeholder="变更详情" value="' + detail + '"></td>' +
             '<td data-site="4">'+ unit +'</td>' +
             '<td data-site="5">'+ roundnum(price, updecimal) +'</td>' +
             '<td data-site="6">'+ roundnum(oamount, numdecimal) +'</td>' +
             '<td data-site="7">'+ roundnum(parseFloat(price).mul(parseFloat(oamount)),decimal) +'</td>' +
-            '<td data-site="8"><input class="form-control input-sm" type="text" onkeyup="RegNum(this,event,'+ numdecimal +')" placeholder="请输入变更数量" value="'+ (scnum != '-' ? roundnum(scnum, numdecimal) : '') +'"></td>' +
+            '<td data-site="8"><input class="form-control form-control-sm" type="text" onkeyup="RegNum(this,event,'+ numdecimal +')" placeholder="请输入变更数量" value="'+ (scnum != '-' ? roundnum(scnum, numdecimal) : '') +'"></td>' +
             '<td data-site="9">'+ sctotal +'</td>'+
             deteletr +'</tr>';
         index ++;
@@ -630,7 +631,7 @@ function maketablelist(status){
     let radionWhiteList = $('#change-whitelist').val() !== '' ? $('#change-whitelist').val().split('^_^') : [];
     //判断是否添加空白清单
     if(status == 'addwhite'){
-        let trlist = ['','','',changeUnits.m.unit,makedecimalzero(decimal),makedecimalzero(findDecimal(changeUnits.m.unit)),makedecimalzero(findDecimal(changeUnits.m.unit)),'',0];
+        let trlist = ['','','','','',makedecimalzero(findDecimal(3)),makedecimalzero(findDecimal(3)),'',0];
         radionWhiteList.push(trlist.join(';'));
     }
 
@@ -646,8 +647,7 @@ function maketablelist(status){
         let detail = radionArray[7];
         let ototal = price != '' && oamount != '' ? roundnum(parseFloat(price).mul(parseFloat(oamount)),decimal) : '';
         let sctotal = price != '' && scnum != '' ? roundnum(parseFloat(price).mul(parseFloat(scnum)),decimal) : '';
-
-        let optionlist = '';
+        let optionlist = '<option></option>';
         for (const j in changeUnits) {
             if (changeUnits[j].unit !== undefined && changeUnits[j].unit === unit) {
                 optionlist += '<option selected="selected">'+ changeUnits[j].unit +'</option>';
@@ -660,15 +660,15 @@ function maketablelist(status){
         let numdecimal = findDecimal(unit);
 
         html += '<tr class="clist" data-lid="' + whiteIndex + '" data-index="' + whiteIndex + '">' +
-            '<td data-site="0"><input class="form-control input-sm" type="text" value="'+ code +'" placeholder="清单编号"></td>' +
-            '<td data-site="1"><input class="form-control input-sm" type="text" value="'+ name +'" placeholder="名称"></td>' +
-            '<td data-site="2"><input class="form-control input-sm" type="text" value="'+ bwmx +'" placeholder="变更部位"></td>' +
-            '<td data-site="3"><input class="form-control input-sm" type="text" value="'+ detail +'" placeholder="变更详情"></td>' +
-            '<td data-site="4"><select class="form-control input-sm">'+ optionlist +'</select></td>' +
-            '<td data-site="5"><input class="form-control input-sm" type="text" onkeyup="RegNum(this,event,'+ updecimal +')" value="'+ roundnum(price, updecimal) +'" placeholder="请输入单价"></td>' +
-            '<td data-site="6"><input class="form-control input-sm" type="text" onkeyup="RegNum(this,event,'+ numdecimal +')" value="'+ roundnum(oamount, numdecimal) +'" placeholder="请输入数量"></td>' +
+            '<td data-site="0"><input class="form-control form-control-sm" type="text" value="'+ code +'" placeholder="清单编号"></td>' +
+            '<td data-site="1"><input class="form-control form-control-sm" type="text" value="'+ name +'" placeholder="名称"></td>' +
+            '<td data-site="2"><input class="form-control form-control-sm" type="text" value="'+ bwmx +'" placeholder="变更部位"></td>' +
+            '<td data-site="3"><input class="form-control form-control-sm" type="text" value="'+ detail +'" placeholder="变更详情"></td>' +
+            '<td data-site="4"><select class="form-control form-control-sm">'+ optionlist +'</select></td>' +
+            '<td data-site="5"><input class="form-control form-control-sm" type="text" onkeyup="RegNum(this,event,'+ updecimal +')" value="'+ roundnum(price, updecimal) +'" placeholder="请输入单价"></td>' +
+            '<td data-site="6"><input class="form-control form-control-sm" type="text" onkeyup="RegNum(this,event,'+ numdecimal +')" value="'+ roundnum(oamount, numdecimal) +'" placeholder="请输入数量"></td>' +
             '<td data-site="7">'+ ototal +'</td>' +
-            '<td data-site="8"><input class="form-control input-sm" type="text" onkeyup="RegNum(this,event,'+ numdecimal +')" value="'+ roundnum(scnum, numdecimal) +'" placeholder="请输入变更数量"></td>' +
+            '<td data-site="8"><input class="form-control form-control-sm" type="text" onkeyup="RegNum(this,event,'+ numdecimal +')" value="'+ roundnum(scnum, numdecimal) +'" placeholder="请输入变更数量"></td>' +
             '<td data-site="9">'+ sctotal +'</td>'+
             deteletr +'</tr>';
         whiteIndex ++;

+ 182 - 0
app/public/js/file-saver/FileSaver.js

@@ -0,0 +1,182 @@
+/* FileSaver.js
+ * A saveAs() FileSaver implementation.
+ * 1.3.8
+ * 2018-03-22 14:03:47
+ *
+ * By Eli Grey, https://eligrey.com
+ * License: MIT
+ *   See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
+ */
+
+/*global self */
+/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */
+
+var saveAs = saveAs || (function(view) {
+	"use strict";
+	// IE <10 is explicitly unsupported
+	if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
+		return;
+	}
+	var
+		  doc = view.document
+		  // only get URL when necessary in case Blob.js hasn't overridden it yet
+		, get_URL = function() {
+			return view.URL || view.webkitURL || view;
+		}
+		, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+		, can_use_save_link = "download" in save_link
+		, click = function(node) {
+			var event = new MouseEvent("click");
+			node.dispatchEvent(event);
+		}
+		, is_safari = /constructor/i.test(view.HTMLElement) || view.safari
+		, is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent)
+		, setImmediate = view.setImmediate || view.setTimeout
+		, throw_outside = function(ex) {
+			setImmediate(function() {
+				throw ex;
+			}, 0);
+		}
+		, force_saveable_type = "application/octet-stream"
+		// the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
+		, arbitrary_revoke_timeout = 1000 * 40 // in ms
+		, revoke = function(file) {
+			var revoker = function() {
+				if (typeof file === "string") { // file is an object URL
+					get_URL().revokeObjectURL(file);
+				} else { // file is a File
+					file.remove();
+				}
+			};
+			setTimeout(revoker, arbitrary_revoke_timeout);
+		}
+		, dispatch = function(filesaver, event_types, event) {
+			event_types = [].concat(event_types);
+			var i = event_types.length;
+			while (i--) {
+				var listener = filesaver["on" + event_types[i]];
+				if (typeof listener === "function") {
+					try {
+						listener.call(filesaver, event || filesaver);
+					} catch (ex) {
+						throw_outside(ex);
+					}
+				}
+			}
+		}
+		, auto_bom = function(blob) {
+			// prepend BOM for UTF-8 XML and text/* types (including HTML)
+			// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
+			if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
+				return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type});
+			}
+			return blob;
+		}
+		, FileSaver = function(blob, name, no_auto_bom) {
+			if (!no_auto_bom) {
+				blob = auto_bom(blob);
+			}
+			// First try a.download, then web filesystem, then object URLs
+			var
+				  filesaver = this
+				, type = blob.type
+				, force = type === force_saveable_type
+				, object_url
+				, dispatch_all = function() {
+					dispatch(filesaver, "writestart progress write writeend".split(" "));
+				}
+				// on any filesys errors revert to saving with object URLs
+				, fs_error = function() {
+					if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
+						// Safari doesn't allow downloading of blob urls
+						var reader = new FileReader();
+						reader.onloadend = function() {
+							var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
+							var popup = view.open(url, '_blank');
+							if(!popup) view.location.href = url;
+							url=undefined; // release reference before dispatching
+							filesaver.readyState = filesaver.DONE;
+							dispatch_all();
+						};
+						reader.readAsDataURL(blob);
+						filesaver.readyState = filesaver.INIT;
+						return;
+					}
+					// don't create more object URLs than needed
+					if (!object_url) {
+						object_url = get_URL().createObjectURL(blob);
+					}
+					if (force) {
+						view.location.href = object_url;
+					} else {
+						var opened = view.open(object_url, "_blank");
+						if (!opened) {
+							// Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
+							view.location.href = object_url;
+						}
+					}
+					filesaver.readyState = filesaver.DONE;
+					dispatch_all();
+					revoke(object_url);
+				}
+			;
+			filesaver.readyState = filesaver.INIT;
+
+			if (can_use_save_link) {
+				object_url = get_URL().createObjectURL(blob);
+				setImmediate(function() {
+					save_link.href = object_url;
+					save_link.download = name;
+					click(save_link);
+					dispatch_all();
+					revoke(object_url);
+					filesaver.readyState = filesaver.DONE;
+				}, 0);
+				return;
+			}
+
+			fs_error();
+		}
+		, FS_proto = FileSaver.prototype
+		, saveAs = function(blob, name, no_auto_bom) {
+			return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
+		}
+	;
+
+	// IE 10+ (native saveAs)
+	if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
+		return function(blob, name, no_auto_bom) {
+			name = name || blob.name || "download";
+
+			if (!no_auto_bom) {
+				blob = auto_bom(blob);
+			}
+			return navigator.msSaveOrOpenBlob(blob, name);
+		};
+	}
+
+	// todo: detect chrome extensions & packaged apps
+	//save_link.target = "_blank";
+
+	FS_proto.abort = function(){};
+	FS_proto.readyState = FS_proto.INIT = 0;
+	FS_proto.WRITING = 1;
+	FS_proto.DONE = 2;
+
+	FS_proto.error =
+	FS_proto.onwritestart =
+	FS_proto.onprogress =
+	FS_proto.onwrite =
+	FS_proto.onabort =
+	FS_proto.onerror =
+	FS_proto.onwriteend =
+		null;
+
+	return saveAs;
+}(
+	   typeof self !== "undefined" && self
+	|| typeof window !== "undefined" && window
+	|| this
+));

+ 3 - 0
app/public/js/global.js

@@ -31,6 +31,9 @@ function autoFlashHeight(){
     $(".sjs-sh-3").height($(window).height()-cHeader-sBar3-92+55);
     $(".sjs-sh-4").height($(window).height()-cHeader-sBar4-92+55);
     $(".sjs-sh-5").height($(window).height()-cHeader-sBar5-92+55);
+    for (const sh of $('.sjs-sh')) {
+        $(sh).height($(window).height()-cHeader-getObjHeight($('.sjs-bar', sh.parent))-92+55);
+    }
 };
 $(window).resize(autoFlashHeight);
 /*全局自适应高度结束*/

+ 121 - 8
app/public/js/ledger.js

@@ -94,7 +94,7 @@ $(document).ready(function() {
         loadExprToInput(sheet) {
             const sel = sheet.getSelections()[0];
             const col = sheet.zh_setting.cols[sel.col], cell = sheet.getCell(sel.row, sel.col);
-            if (col.type === 'Number') {
+            if (col && col.type === 'Number') {
                 const data = SpreadJsObj.getSelectObject(sheet);
                 if (data) {
                     $('#bills-expr').val(data[col.field]).attr('field', col.field).attr('org', data[col.field]);
@@ -260,7 +260,7 @@ $(document).ready(function() {
          * 新增节点
          * @param spread
          */
-        addNode: function (sheet) {
+        addNode: function (sheet, count = 1) {
             const self = this;
             const sel = sheet.getSelections()[0];
             const row = sheet.getSelections()[0].row;
@@ -275,6 +275,7 @@ $(document).ready(function() {
                 postType: 'add',
                 postData: {
                     id: tree.getNodeKey(node),
+                    count: count,
                 }
             }, function (result) {
                 const refreshNode = tree.loadPostData(result);
@@ -559,8 +560,7 @@ $(document).ready(function() {
             }
         },
         clipboardPasting: function (e, info) {
-            const sheet = info.sheet, tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
-            console.log(info);
+            const tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
             info.cancel = true;
             if (!setting || !tree) return;
             const pasteData = info.pasteData.html
@@ -912,6 +912,31 @@ $(document).ready(function() {
         });
 
         let batchInsertObj;
+        $.contextMenu.types.batchInsert = function (item, opt, root) {
+            const self = this;
+            if ($.isFunction(item.icon)) {
+                item._icon = item.icon.call(this, this, $t, key, item);
+            } else {
+                if (typeof(item.icon) === 'string' && item.icon.substring(0, 3) === 'fa-') {
+                    // to enable font awesome
+                    item._icon = root.classNames.icon + ' ' + root.classNames.icon + '--fa fa ' + item.icon;
+                } else {
+                    item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon;
+                }
+            }
+            this.addClass(item._icon);
+            const $obj = $('<div>' + item.name + '<input class="text-right ml-1 mr-1" type="tel" max="20" min="1" value="' + item.value + '" style="width: 30px; height: 18px; padding-right: 4px;">行</div>')
+                .appendTo(this);
+            const $input = $obj.find('input');
+            const event = () => {
+                if (self.hasClass('context-menu-disabled')) return;
+                item.batchInsert($input[0], root);
+            };
+            $obj.on('click', event).keypress(function (e) {if (e.keyCode === 13) { event(); }});
+            $input.click((e) => {e.stopPropagation();})
+                .keyup((e) => {if (e.keyCode === 13) item.batchInsert($input[0], root);})
+                .on('input', function () {this.value = this.value.replace(/[^\d]/g, '');});
+        };
         // 右键菜单
         $.contextMenu({
             selector: '#ledger-spread',
@@ -1033,6 +1058,47 @@ $(document).ready(function() {
                         }
                     }
                 },
+                'batchInsert': {
+                    name: '批量插入',
+                    type: 'batchInsert',
+                    value: '2',
+                    icon: 'fa-sign-in',
+                    batchInsert: function (obj, root) {
+                        if (_.toNumber(obj.value) > _.toNumber(obj.max)) {
+                            obj.value = obj.max;
+                            toastr.warning('批量插入不可多于' + obj.max);
+                        } else if(_.toNumber(obj.value) < _.toNumber(obj.min)) {
+                            obj.value = obj.min;
+                            toastr.warning('批量插入不可少于' + obj.min);
+                        } else {
+                            treeOperationObj.addNode(ledgerSpread.getActiveSheet(), parseInt(obj.value));
+                            root.$menu.trigger('contextmenu:hide');
+                        }
+                    },
+                    disabled: function (key, opt) {
+                        const sheet = ledgerSpread.getActiveSheet();
+                        const selection = sheet.getSelections();
+                        const sel = selection ? selection[0] : sheet.getSelections()[0];
+                        const row = sel ? sel.row : -1;
+                        const tree = sheet.zh_tree;
+                        if (!tree) return true;
+                        const first = sheet.zh_tree.nodes[row];
+                        let last = first, sameParent = true;
+                        if (sel.rowCount > 1) {
+                            for (let r = 1; r < sel.rowCount; r++) {
+                                const rNode = tree.nodes[sel.row + r];
+                                if (rNode.level > first.level) continue;
+                                if ((rNode.level < first.level) || (rNode.level === first.level && rNode.pid !== first.pid)) {
+                                    sameParent = false;
+                                    break;
+                                }
+                                last = rNode;
+                            }
+                        }
+                        const valid = !sheet.zh_setting.readOnly;
+                        return !(valid && first && first.level > 1);
+                    }
+                },
                 'batchInsertBillsPos': {
                     name: '批量插入清单-计量单元',
                     icon: 'fa-sign-in',
@@ -1064,15 +1130,35 @@ $(document).ready(function() {
                 },
                 'importExcel': {
                     name: '导入分项清单Excel',
-                    icon: 'file-excel-o',
+                    icon: 'fa-file-excel-o',
                     disabled: function (key, opt) {
                         return readOnly;
                     },
                     callback: function (key, opt) {
                         $('#upload-ledger').modal('show');
                     }
+                },
+                'exportExcel': {
+                    name: '导出表格数据',
+                    icon: 'fa-file-excel-o',
+                    callback: function (key, opt) {
+                        const excelIo = new GC.Spread.Excel.IO();
+                        const date = new Date();
+                        const fileName = $('.text-truncate').attr('data-original-title') + '.xlsx';
+                        const sJson = JSON.stringify(ledgerSpread.toJSON({columnHeadersAsFrozenRows: true, rowHeadersAsFrozenColumns: true}));
+                        excelIo.save(sJson, function(blob) {
+                            saveAs(blob, fileName);
+                        });
+                    },
+                    visible: function (key, opt) {
+                        try {
+                            return is_debug;
+                        } catch (err) {
+                            return false;
+                        }
+                    }
                 }
-            }
+            },
         });
     } else {
         SpreadJsObj.forbiddenSpreadContextMenu('#ledger-spread', ledgerSpread);
@@ -1358,7 +1444,7 @@ $(document).ready(function() {
         selectionChanged: function (e, info) {
             const col = info.sheet.zh_setting.cols[info.newSelections[0].col];
             const cell = info.sheet.getCell(info.newSelections[0].row, info.newSelections[0].col);
-            if (col.type === 'Number') {
+            if (col && col.type === 'Number') {
                 const data = SpreadJsObj.getSelectObject(info.sheet);
                 if (data) {
                     $('#pos-expr').val(data[col.field]).attr('field', col.field).attr('org', data[col.field])
@@ -1370,8 +1456,24 @@ $(document).ready(function() {
                 $('#pos-expr').val('').attr('readOnly', true);
             }
         },
+        addPegs: function (pegs) {
+            if (!pegs || pegs.length <= 0) return;
+            const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+            if (!node) return;
+
+            const sheet = posSpread.getActiveSheet();
+            const sortData = sheet.zh_data || [];
+            let order = sortData.length > 0 ? sortData[sortData.length - 1].porder + 1 : 1;
+            pegs.forEach(function (p) {p.porder = ++order; p.lid = node.id});
+
+            postData('/tender/' + getTenderId() + '/pos/paste', pegs, function (result) {
+                pos.updateDatas(result.pos);
+                posOperationObj.loadCurPosData();
+                treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet());
+            });
+        }
     };
-    posSpread.bind(spreadNS.Events.SelectionChanged, posOperationObj.selectionChanged)
+    posSpread.bind(spreadNS.Events.SelectionChanged, posOperationObj.selectionChanged);
     if (!posSpreadSetting.readOnly) {
         $('#pos-expr').bind('change mouseleave', function () {
             const expr = $(this);
@@ -1414,6 +1516,7 @@ $(document).ready(function() {
         posSpread.bind(spreadNS.Events.EditStarting, posOperationObj.editStarting);
         posSpread.bind(spreadNS.Events.EditEnded, posOperationObj.editEnded);
         posSpread.bind(spreadNS.Events.ClipboardPasted, posOperationObj.clipboardPasted);
+        const mergePeg = NewMergePeg({ callback: posOperationObj.addPegs });
         // 右键菜单
         $.contextMenu({
             selector: '#pos-spread',
@@ -1438,6 +1541,16 @@ $(document).ready(function() {
                         posOperationObj.deletePos(posSpread.getActiveSheet());
                     }
                 },
+                'merge-peg': {
+                    name: '合并起讫桩号',
+                    disabled: function (key, opt) {
+                        const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                        return _.isNil(node) || _.isNil(node.b_code) || node.b_code === '';
+                    },
+                    callback: function (key, opt) {
+                        mergePeg.show();
+                    }
+                }
             }
         });
     } else {

+ 2 - 2
app/public/js/ledger_search.js

@@ -111,7 +111,7 @@
         const resultId = setting.id + '-search-result';
         const obj = $(setting.selector);
         obj.html(
-            '                        <div class="sjs-bar-1">\n' +
+            '                        <div class="sjs-bar">\n' +
             '                            <div class="input-group input-group-sm pb-1">\n' +
             '                                <input id="searchKeyword" type="text" class="form-control" placeholder="可查找 项目节编号/清单编号/名称" aria-label="Recipient\'s username" aria-describedby="button-addon2">\n' +
             '                                <div class="input-group-append">\n' +
@@ -119,7 +119,7 @@
             '                                </div>\n' +
             '                            </div>\n' +
             '                        </div>\n' +
-            '                        <div id="' + resultId + '" class="sjs-sh-1">\n' +
+            '                        <div id="' + resultId + '" class="sjs-sh">\n' +
             '                        </div>'
         );
         autoFlashHeight();

+ 1 - 1
app/public/js/number-precision.js

@@ -160,7 +160,7 @@ function transFormToChinese(num) {
         for (var i = 0; i < strArr.length; i++) {
             newNum = (i == 0 && strArr[i] == 0 ? "" : (i > 0 && strArr[i] == 0 && strArr[i - 1] == 0 ? "" : changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i]))) + newNum;
         }
-        return newNum;
+        return strArr.length === 2 && newNum.indexOf("一十") !== -1 ? newNum.replace('一十', '十') : newNum;
     }
     let overWan = Math.floor(num / 10000);
     let noWan = num % 10000;

+ 345 - 24
app/public/js/revise.js

@@ -9,6 +9,12 @@
  */
 
 const ckBillsSpread = window.location.pathname + '-billsSelect';
+const invalidFields = {
+    parent: ['sgfh_qty', 'sgfh_tp', 'sjcl_qty', 'sjcl_tp', 'qtcl_qty', 'qtcl_tp', 'deal_qty', 'deal_tp', 'unit_price'],
+    gcl: ['dgn_qty1', 'dgn_qty2'],
+    posCode: ['b_code'],
+    posCalc: ['sgfh_qty', 'sgfh_tp', 'sjcl_qty', 'sjcl_tp', 'qtcl_qty', 'qtcl_tp'],
+};
 function transExpr(expr) {
     return $.trim(expr).replace('\t', '').replace('=', '').replace('%', '/100');
 }
@@ -56,7 +62,7 @@ $(document).ready(() => {
         loadExprToInput(sheet) {
             const sel = sheet.getSelections()[0];
             const col = sheet.zh_setting.cols[sel.col], cell = sheet.getCell(sel.row, sel.col);
-            if (col.type === 'Number') {
+            if (col && col.type === 'Number') {
                 const data = SpreadJsObj.getSelectObject(sheet);
                 if (data) {
                     $('#bills-expr').val(data[col.field]).attr('field', col.field).attr('org', data[col.field]);
@@ -233,7 +239,7 @@ $(document).ready(() => {
          * 新增节点
          * @param spread
          */
-        baseOpr: function (sheet, type) {
+        baseOpr: function (sheet, type, addCount = 1) {
             const self = this;
             const [tree, node, count] = this.getDefaultSelectInfo(sheet);
             if (!tree || !node || !count) return;
@@ -281,7 +287,7 @@ $(document).ready(() => {
                 postType: type,
                 postData: {
                     id: node.ledger_id,
-                    count: count,
+                    count: type === 'add' ? addCount : count,
                 }
             }, function (result) {
                 const refreshData = tree.loadPostData(result);
@@ -389,17 +395,113 @@ $(document).ready(() => {
             }
         },
         clipboardPasting: function (e, info) {
-            if (info.sheet.zh_setting) {
-                const range = info.cellRange;
-                for (let iRow = range.row; iRow < range.row + range.rowCount; iRow++) {
-                    const node = info.sheet.zh_tree.nodes[iRow];
-                    if (info.sheet.zh_tree.checkNodeUsed(node, pos)) {
-                        toastr.warning('"' + node.code + node.b_code + ' ' + node.name +'"已计量,请勿修改');
-                        info.cancel = true;
-                        return;
+            const tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
+            info.cancel = true;
+            if (!setting || !tree) return;
+            // const range = info.cellRange;
+            // for (let iRow = range.row; iRow < range.row + range.rowCount; iRow++) {
+            //     const node = tree.nodes[iRow];
+            //     if (tree.checkNodeUsed(node, pos)) {
+            //         toastr.warning('"' + node.code + node.b_code + ' ' + node.name +'"已计量,请勿修改');
+            //         return;
+            //     }
+            // }
+
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            const hint = {
+                usedUp: {type: 'warning', msg: '节点已计量,不可修改单价'},
+                usedCode: {type: 'warning', msg: '节点已计量,编号不可修改为空值'},
+                invalidExpr: {type: 'warning', msg: '粘贴的表达式非法'},
+                posCode: {type: 'warning', msg: '清单含有计量单元,请先删除计量单元,再修改清单编号为空'},
+                posQty: {type: 'warning', msg: '清单含有计量单元,数量金额根据计量单元汇总计算所得,不可修改'},
+                parent: {type: 'warning', msg: '含有子项的清单,不可粘贴数量、单价、金额'},
+                gcl: {type: 'warning', msg: '工程量清单,不可粘贴项目节数量'},
+            };
+            const datas = [], filterNodes = [];
+
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = tree.nodes[curRow];
+                if (!node) continue;
+
+                let bPaste = false;
+                const data = info.sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    if (node.children && node.children.length > 0 && invalidFields.parent.indexOf(colSetting.field) >= 0) {
+                        toastMessageUniq(hint.parent);
+                        continue;
+                    }
+                    if (!_.isEmpty(node.b_code) && invalidFields.gcl.indexOf(colSetting.field) >= 0) {
+                        toastMessageUniq(hint.gcl);
+                        continue;
+                    }
+
+                    const lPos = pos.getLedgerPos(node.id);
+                    if (lPos && lPos.length > 0) {
+                        if (value === '' && colSetting.field === 'b_code') {
+                            toastMessageUniq(hint.posCode);
+                            continue;
+                        }
+                        if (colSetting.field === 'sgfh_qty' || colSetting.field === 'sgfh_tp' ||
+                            colSetting.field === 'sjcl_qty' || colSetting.field === 'sjcl_tp' ||
+                            colSetting.field === 'qtcl_qty' || colSetting.field === 'qtcl_tp') {
+                            toastMessageUniq(hint.posQty);
+                            continue;
+                        }
+                    }
+
+                    const value = trimInvalidChar(pasteData[iRow][iCol]);
+                    if (tree.checkNodeUsed(node, pos) && col.field === 'unit_price') {
+                        toastMessageUniq (hint.usedUp);
+                        continue;
+                    }
+                    if (colSetting.type === 'Number') {
+                        const num = _.toNumber(value);
+                        if (num) {
+                            data[colSetting.field] = num;
+                        } else {
+                            try {
+                                data[colSetting.field] = math.evaluate(transExpr(value));
+                            } catch (err) {
+                                toastMessageUniq(hint.invalidExpr);
+                                continue;
+                            }
+                        }
+                    } else {
+                        if (node.used && (col.field === 'code' || col.field ==='b_code')
+                            && data[colSetting.field] !== '' && value === '') {
+                            toastMessageUniq(hint.usedCode);
+                            continue;
+                        }
+                        data[colSetting.field] = value;
                     }
+                    bPaste = true;
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
                 }
             }
+            if (datas.length > 0) {
+                postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) {
+                        refreshNode.update = refreshNode.update.concat(filterNodes);
+                    }
+                    billsTreeSpreadObj.refreshTree(info.sheet, refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
         },
         clipboardPasted: function (e, info) {
             const hint = {
@@ -559,7 +661,50 @@ $(document).ready(() => {
                     info.cancel = !_.isEmpty(node.b_code);
                     break;
             }
-        }
+        },
+        cut: function (sheet, sel, callback) {
+            if (!sheet || !sel) return;
+            if (sel.colCount >= sheet.zh_setting.cols.length) {
+                toastr.warning('请勿选中整行剪切');
+                return;
+            }
+
+            const sortData = SpreadJsObj.getSortData(sheet), datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                const node = sortData[iRow];
+                if (node) {
+                    const data = sheet.zh_tree.getNodeKeyData(node);
+                    for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                        const col = sheet.zh_setting.cols[iCol];
+                        if (col.field === 'b_code' || col.field === 'sgfh_qty' || col.field === 'sgfh_tp' ||
+                            col.field === 'sjcl_qty' || col.field === 'sjcl_tp' ||
+                            col.field === 'qtcl_qty' || col.field === 'qtcl_tp') {
+                            const lPos = pos.getLedgerPos(node.id);
+                            if (lPos && lPos.length > 0) {
+                                toastr.error('不可剪切');
+                                return;
+                            }
+                        }
+                        const style = sheet.getStyle(iRow, iCol);
+                        if (style.locked) {
+                            toastr.error('不可剪切');
+                            return;
+                        }
+                        const colSetting = sheet.zh_setting.cols[iCol];
+                        data[colSetting.field] = null;
+                    }
+                    datas.push(data);
+                }
+            }
+
+            if (datas.length > 0) {
+                callback();
+                postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    billsTreeSpreadObj.refreshTree(sheet, refreshNode);
+                });
+            }
+        },
     };
     billsTreeSpreadObj.refreshOperationValid(billsSheet);
     billsTreeSpreadObj.loadExprToInput(billsSheet);
@@ -573,7 +718,7 @@ $(document).ready(() => {
         $('a[name=cpc]').click(function () {
             billsSpread.commandManager().execute({
                 cmd: this.getAttribute('type'),
-                sheetName: billsSpread.getActiveSheet().name()
+                sheetName: billsSheet.name()
             });
         });
 
@@ -617,9 +762,39 @@ $(document).ready(() => {
         billsSpread.bind(spreadNS.Events.EditStarting, billsTreeSpreadObj.editStarting);
         billsSpread.bind(spreadNS.Events.EditEnded, billsTreeSpreadObj.editEnded);
         billsSpread.bind(spreadNS.Events.ClipboardPasting, billsTreeSpreadObj.clipboardPasting);
-        billsSpread.bind(spreadNS.Events.ClipboardPasted, billsTreeSpreadObj.clipboardPasted);
+        billsSpread.bind(spreadNS.Events.ClipboardChanging, function (e, info) {
+            const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
+            SpreadJsObj.Clipboard.setCopyData(copyText);
+        });
         SpreadJsObj.addDeleteBind(billsSpread, billsTreeSpreadObj.deletePress);
+        SpreadJsObj.addCutEvents(billsSpread, billsTreeSpreadObj.cut);
         let batchInsertObj;
+        $.contextMenu.types.batchInsert = function (item, opt, root) {
+            const self = this;
+            if ($.isFunction(item.icon)) {
+                item._icon = item.icon.call(this, this, $t, key, item);
+            } else {
+                if (typeof(item.icon) === 'string' && item.icon.substring(0, 3) === 'fa-') {
+                    // to enable font awesome
+                    item._icon = root.classNames.icon + ' ' + root.classNames.icon + '--fa fa ' + item.icon;
+                } else {
+                    item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon;
+                }
+            }
+            this.addClass(item._icon);
+            const $obj = $('<div>' + item.name + '<input class="text-right ml-1 mr-1" type="tel" max="20" min="1" value="' + item.value + '" style="width: 30px; height: 18px; padding-right: 4px;">行</div>')
+                .appendTo(this);
+            const $input = $obj.find('input');
+
+            const event = () => {
+                if (self.hasClass('context-menu-disabled')) return;
+                item.batchInsert($input[0], root);
+            };
+            $obj.on('click', event).keypress(function (e) {if (e.keyCode === 13) { event(); }});
+            $input.click((e) => {e.stopPropagation();})
+                .keyup((e) => {if (e.keyCode === 13) item.batchInsert($input[0], root);})
+                .on('input', function () {this.value = this.value.replace(/[^\d]/g, '');});
+        };
         // 右键菜单
         $.contextMenu({
             selector: '#bills-spread',
@@ -681,6 +856,35 @@ $(document).ready(() => {
                         return !(valid && first && sameParent && !(first.level === 1 && first.node_type) && !nodeUsed);
                     }
                 },
+                'batchInsert': {
+                    name: '批量插入',
+                    type: 'batchInsert',
+                    value: '2',
+                    icon: 'fa-sign-in',
+                    batchInsert: function (obj, root) {
+                        if (_.toNumber(obj.value) > _.toNumber(obj.max)) {
+                            obj.value = obj.max;
+                            toastr.warning('批量插入不可多于' + obj.max);
+                        } else if (_.toNumber(obj.value) < _.toNumber(obj.min)) {
+                            obj.value = obj.min;
+                            toastr.warning('批量插入不可少于' + obj.min);
+                        } else {
+                            billsTreeSpreadObj.baseOpr(billsSheet, 'add', parseInt(obj.value));
+                            root.$menu.trigger('contextmenu:hide');
+                        }
+                    },
+                    disabled: function (key, opt) {
+                        const sheet = billsSheet;
+                        const selection = sheet.getSelections();
+                        const sel = selection ? selection[0] : sheet.getSelections()[0];
+                        const row = sel ? sel.row : -1;
+                        const tree = sheet.zh_tree;
+                        if (!tree) return true;
+                        const first = sheet.zh_tree.nodes[row];
+                        const valid = !sheet.zh_setting.readOnly;
+                        return !(valid && first && first.level > 1);
+                    }
+                },
                 'batchInsertBillsPos': {
                     name: '批量插入清单-计量单元',
                     icon: 'fa-sign-in',
@@ -992,7 +1196,7 @@ $(document).ready(() => {
         selectionChanged: function (e, info) {
             const col = info.sheet.zh_setting.cols[info.newSelections[0].col];
             const cell = info.sheet.getCell(info.newSelections[0].col, info.newSelections[0].col);
-            if (col.type === 'Number') {
+            if (col && col.type === 'Number') {
                 const data = SpreadJsObj.getSelectObject(info.sheet);
                 if (data) {
                     $('#pos-expr').val(data[col.field]).attr('field', col.field).attr('org', data[col.field])
@@ -1004,6 +1208,22 @@ $(document).ready(() => {
                 $('#pos-expr').val('').attr('readOnly', true);
             }
         },
+        addPegs: function (pegs) {
+            if (!pegs || pegs.length <= 0) return;
+            const node = SpreadJsObj.getSelectObject(billsSheet);
+            if (!node) return;
+
+            const sheet = posSpread.getActiveSheet();
+            const sortData = sheet.zh_data || [];
+            let order = sortData.length > 0 ? sortData[sortData.length - 1].porder + 1 : 1;
+            pegs.forEach(function (p) {p.porder = ++order; p.lid = node.id});
+
+            postData(window.location.pathname + '/update', {postType: 'pos', posPostType: 'paste', postData: pegs}, function (result) {
+                pos.updateDatas(result.pos);
+                posSpreadObj.loadCurPosData();
+                billsTreeSpreadObj.refreshOperationValid(billsSheet);
+            });
+        }
     };
     posSpreadObj.loadCurPosData();
     SpreadJsObj.resetTopAndSelect(posSheet);
@@ -1052,6 +1272,8 @@ $(document).ready(() => {
         posSpread.bind(spreadNS.Events.ClipboardPasting, posSpreadObj.clipboardPasting);
         posSpread.bind(spreadNS.Events.ClipboardPasted, posSpreadObj.clipboardPasted);
         SpreadJsObj.addDeleteBind(posSpread, posSpreadObj.deletePress);
+
+        const mergePeg = NewMergePeg({ callback: posSpreadObj.addPegs });
         $.contextMenu({
             selector: '#pos-spread',
             build: function ($trigger, e) {
@@ -1073,6 +1295,16 @@ $(document).ready(() => {
                     callback: function (key, opt) {
                         posSpreadObj.deletePos(posSheet);
                     }
+                },
+                'merge-peg': {
+                    name: '合并起讫桩号',
+                    disabled: function (key, opt) {
+                        const node = SpreadJsObj.getSelectObject(billsSheet);
+                        return _.isNil(node) || _.isNil(node.b_code) || node.b_code === '';
+                    },
+                    callback: function (key, opt) {
+                        mergePeg.show();
+                    }
                 }
             }
 
@@ -1138,13 +1370,17 @@ $(document).ready(() => {
             }
             SpreadJsObj.forbiddenSpreadContextMenu(selector, this.spread);
         }
-        loadData () {
-            if (this.loaded) return;
+        loadData (callback) {
+            if (this.loaded) {
+                if (callback) callback();
+                return;
+            }
             const self = this;
             postData(this.url+'/get-data', {}, function (data) {
                 self.data = data;
                 SpreadJsObj.loadSheetData(self.spread.getActiveSheet(), 'data', data);
                 self.loaded = true;
+                if (callback) callback();
             });
         }
         calculateData () {
@@ -1155,6 +1391,65 @@ $(document).ready(() => {
             }
         }
     }
+    class BgBills {
+        constructor (selector, spreadSetting) {
+            this.loaded = false;
+            this.obj = $(selector)[0];
+            this.spreadSetting = spreadSetting;
+            this.spread = SpreadJsObj.createNewSpread(this.obj);
+            this.sheet = this.spread.getActiveSheet();
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            if (!readOnly) {
+                this.spread.bind(spreadNS.Events.CellDoubleClick, function (e, info) {
+                    const dealSheet = info.sheet;
+                    const mainSheet = billsSheet;
+
+                    const bgBills = SpreadJsObj.getSelectObject(dealSheet);
+                    if (!bgBills) { return; }
+                    const mainTree = mainSheet.zh_tree;
+                    const mainNode = SpreadJsObj.getSelectObject(mainSheet);
+                    if (!mainNode || !mainTree) { return; }
+
+                    if (mainNode.code && mainNode.code !== '' && !mainTree.isLeafXmj(mainNode)) {
+                        toastr.warning('非最底层项目下,不应添加变更清单');
+                        return;
+                    }
+
+                    postData(window.location.pathname + '/update', {
+                        postType: 'add-bg',
+                        postData: {
+                            id: mainNode.ledger_id,
+                            type: mainNode.code ? 'child' : 'next',
+                            dealBills: {
+                                b_code: bgBills.code, name: bgBills.name, unit: bgBills.unit,
+                                unit_price: bgBills.unit_price,
+                            }
+                        },
+                    }, function (result) {
+                        const refreshData = mainTree.loadPostData(result);
+                        billsTreeSpreadObj.refreshTree(mainSheet, refreshData);
+                        const sel = mainSheet.getSelections()[0];
+                        if (sel && refreshData.create[0]) {
+                            mainSheet.setSelection(mainTree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                        }
+                        billsTreeSpreadObj.refreshOperationValid(mainSheet);
+                        billsSpread.focus();
+                        posSpreadObj.loadCurPosData();
+                    });
+                });
+            }
+            SpreadJsObj.forbiddenSpreadContextMenu(selector, this.spread);
+        }
+        loadData () {
+            if (this.loaded) return;
+            const self = this;
+            postData('/tender/' + window.location.pathname.split('/')[2] +'/change/bills', {type: 'gather'}, function (data) {
+                self.data = data;
+                SpreadJsObj.loadSheetData(self.spread.getActiveSheet(), 'data', data);
+                self.loaded = true;
+            });
+        }
+    }
     class BatchInsertBillsPosObj {
         constructor (obj) {
             const self = this;
@@ -1191,7 +1486,7 @@ $(document).ready(() => {
             };
             for (let iNum = 1; iNum <= this.billsCount; iNum++) {
                 this.posSpreadSetting.cols.push(
-                    {title: '节点' + iNum, field: 'bills' + iNum, hAlign: 2, width: 50}
+                    {title: '清单' + iNum, field: 'bills' + iNum, hAlign: 2, width: 50}
                 )
             }
             this.posSpread = SpreadJsObj.createNewSpread($('.batch-l-b', this.obj)[0]);
@@ -1215,8 +1510,9 @@ $(document).ready(() => {
             SpreadJsObj.initSheet(this.dealSpread.getActiveSheet(), this.dealSpreadSetting);
             SpreadJsObj.refreshColumnAlign(this.dealSpread.getActiveSheet());
             // 拉取签约节点数据
-            dealBills.loadData();
-            SpreadJsObj.loadSheetData(this.dealSpread.getActiveSheet(), 'data', dealBills.data);
+            dealBills.loadData(() => {
+                SpreadJsObj.loadSheetData(this.dealSpread.getActiveSheet(), 'data', dealBills.data);
+            });
             // 双击签约节点,自动添加到清单编号窗口
             this.dealSpread.bind(GC.Spread.Sheets.Events.CellDoubleClick, function (e, info) {
                 const deal = info.sheet.zh_data[info.row];
@@ -1229,7 +1525,7 @@ $(document).ready(() => {
                 if (sel.row + 1 === qdSheet.getRowCount()) {
                     const count = sel.row + 2;
                     qdSheet.setRowCount(count);
-                    qdSheet.getCell(sel.row + 1, 0, GC.Spread.Sheets.SheetArea.rowHeader).text('节点' + count);
+                    qdSheet.getCell(sel.row + 1, 0, GC.Spread.Sheets.SheetArea.rowHeader).text('清单' + count);
 
                     const colCount = posSheet.getColumnCount() + 1;
                     posSheet.setColumnCount(colCount);
@@ -1258,8 +1554,8 @@ $(document).ready(() => {
                     postData(window.location.pathname + '/update', {postType: 'batch-insert', postData: insertData}, function (data) {
                         pos.updateDatas(data.pos);
                         const result = billsTree.loadPostData(data.ledger);
-                        billsTreeSpreadObj.refreshTree(sheet, result);
-                        billsTreeSpreadObj.refreshOperationValid(sheet, selection);
+                        billsTreeSpreadObj.refreshTree(billsSheet, result);
+                        billsTreeSpreadObj.refreshOperationValid(billsSheet, selection);
                         self.obj.modal('hide');
                     }, null, true);
                 }
@@ -1275,7 +1571,7 @@ $(document).ready(() => {
             SpreadJsObj.beginMassOperation(qdSheet);
             qdSheet.clear(0, 0, qdSheet.getRowCount(), qdSheet.getColumnCount(), GC.Spread.Sheets.SheetArea.viewport, GC.Spread.Sheets.StorageType.data);
             for (let iRow = 1; iRow <= this.billsCount; iRow++) {
-                qdSheet.getCell(iRow - 1, 0, GC.Spread.Sheets.SheetArea.rowHeader).text('节点' + iRow);
+                qdSheet.getCell(iRow - 1, 0, GC.Spread.Sheets.SheetArea.rowHeader).text('清单' + iRow);
             }
             qdSheet.setSelection(0, 0, 1 ,1);
             SpreadJsObj.endMassOperation(qdSheet);
@@ -1339,6 +1635,22 @@ $(document).ready(() => {
         headerFont: '12px 微软雅黑',
         font: '12px 微软雅黑',
     });
+    const bgBills = new BgBills('#bg-bills-spread', {
+        cols: [
+            {title: '清单编号', field: 'code', hAlign: 0, width: 85, formatter: '@', readOnly: true},
+            {title: '名称', field: 'name', hAlign: 0, width: 150, formatter: '@', readOnly: true},
+            {title: '单位', field: 'unit', hAlign: 1, width: 50, formatter: '@', readOnly: true},
+            {title: '单价', field: 'unit_price', hAlign: 2, width: 50, readOnly: true},
+            {title: '数量', field: 'quantity', hAlign: 2, width: 50, readOnly: true},
+            {title: '金额', field: 'total_price', hAlign: 2, width: 50, readOnly: true},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+    });
 
     $.divResizer({
         select: '#revise-right-spr',
@@ -1356,6 +1668,9 @@ $(document).ready(() => {
             if (dealBills) {
                 dealBills.spread.refresh();
             }
+            if (bgBills) {
+                bgBills.spread.refresh();
+            }
             if (searchLedger) {
                 searchLedger.spread.refresh();
             }
@@ -1388,6 +1703,9 @@ $(document).ready(() => {
             if (dealBills) {
                 dealBills.spread.refresh();
             }
+            if (bgBills) {
+                bgBills.spread.refresh();
+            }
             if (searchLedger) {
                 searchLedger.spread.refresh();
             }
@@ -1556,6 +1874,9 @@ $(document).ready(() => {
             } else if (tab.attr('content') === '#deal-bills') {
                 dealBills.loadData();
                 dealBills.spread.refresh();
+            } else if (tab.attr('content') === '#bg-bills') {
+                bgBills.loadData();
+                bgBills.spread.refresh();
             } else if (tab.attr('content') === '#search' && !searchLedger) {
                 if (!searchLedger) {
                     searchLedger = $.billsSearch({

+ 158 - 0
app/public/js/shares/merge_peg.js

@@ -0,0 +1,158 @@
+'use strict';
+
+/**
+ * 配合/view/shares/merge_peg_modal.ejs
+ * 依赖jQuery, lodash, spreadjs,请放在spreadjs_zh.js之后
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const NewMergePeg = function (setting) {
+    const spread = SpreadJsObj.createNewSpread($('#mp-spread')[0]);
+    const sheet = spread.getActiveSheet();
+    const spreadSetting = {
+        cols: [
+            {title: '起始桩号', field: 'start-peg', hAlign: 0, width: 80, formatter: '@'},
+            {title: '终止桩号', field: 'end-peg', hAlign: 0, width: 80, formatter: '@'},
+            {title: '位置', field: 'peg-pos', hAlign: 0, width: 60, formatter: '@'},
+            {title: '合并结果', field: 'peg', hAlign: 0, width: 180, formatter: '@'},
+        ],
+        emptyRows: 3,
+        headRows: 1,
+        headRowHeight: [32],
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+    };
+    const findCol = function (field) {
+        return _.findIndex(spreadSetting.cols, function (c) {return c.field === field});
+    };
+    const spCol = findCol('start-peg'), epCol = findCol('end-peg'), posCol = findCol('peg-pos'), pegCol = findCol('peg');
+    SpreadJsObj.initSheet(sheet, spreadSetting);
+    sheet.setColumnWidth(0, 30, spreadNS.SheetArea.rowHeader);
+    const spreadObj = {
+        mergePeg(row) {
+            const withPos = $('#mp-with-pos')[0].checked;
+            const withSprChar = $('#mp-with-spr-char')[0].checked, sprChar = $('#mp-spr-char').val();
+            const mergeRow = function (row) {
+                const startPeg = _.trim(sheet.getText(row, spCol));
+                const endPeg = _.trim(sheet.getText(row, epCol));
+                let peg;
+                if (startPeg !== '') {
+                    peg = endPeg !== ''
+                        ? (withSprChar && endPeg.indexOf(sprChar) !== 0 ? startPeg + sprChar + endPeg : startPeg + endPeg)
+                        : startPeg;
+                } else {
+                    peg = endPeg !== '' ? endPeg : '';
+                }
+                if (withPos) {
+                    peg = _.trim(sheet.getText(row, posCol)) + peg;
+                }
+                sheet.setText(row, pegCol, peg);
+            };
+            if (row) {
+                const rows = row instanceof Array ? row : [row];
+                if (row instanceof Array) {
+                    for (const r of rows) {
+                        mergeRow(r)
+                    }
+                }
+            } else {
+                for (let iRow = 0, iLen = sheet.getRowCount(); iRow < iLen; iRow++) {
+                    mergeRow(iRow);
+                }
+            }
+        },
+        getPegs() {
+            const result = [];
+            for (let iRow = 0, iLen = sheet.getRowCount(); iRow < iLen; iRow++) {
+                const peg = sheet.getText(iRow, pegCol);
+                if (peg !== '') {
+                    result.push({name: peg});
+                }
+            }
+            return result;
+        },
+        editStarting(e, info) {
+            if (!info.sheet.zh_setting) return;
+            const col = info.sheet.zh_setting.cols[info.col];
+            switch (col.field) {
+                case 'peg':
+                    info.cancel = true;
+                    break;
+            }
+        },
+        clipboardPasted: function (e, info) {
+            spreadObj.mergePeg();
+        },
+        deletePress: function (sheet) {
+            if (!sheet.zh_setting) return;
+
+            const sel = sheet.getSelections()[0], row = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    if (iCol !== pegCol) {
+                        sheet.setText(iRow, iCol, '');
+                    }
+                }
+                row.push(iRow);
+            }
+            spreadObj.mergePeg(row);
+        },
+        editEnded: function (e, info) {
+            spreadObj.mergePeg(info.row);
+        },
+        cut: function (sheet, sel, callback) {
+            if (!sheet || !sel) return;
+
+            callback();
+            const rows = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    sheet.setText(iRow, iCol, '');
+                }
+                rows.push(iRow);
+            }
+            spreadObj.mergePeg(rows);
+        }
+    };
+    spread.bind(spreadNS.Events.EditStarting, spreadObj.editStarting);
+    spread.bind(spreadNS.Events.ClipboardPasted, spreadObj.clipboardPasted);
+    spread.bind(spreadNS.Events.EditEnded, spreadObj.editEnded);
+    SpreadJsObj.addDeleteBind(spread, spreadObj.deletePress);
+    SpreadJsObj.addCutEvents(spread, spreadObj.cut);
+
+    // 勾选位置
+    $('#mp-with-pos').click(() => {spreadObj.mergePeg();});
+    // 勾选桩号连接符
+    $('#mp-with-spr-char').click(function () {
+        if (this.checked) {
+            $('#mp-spr-char').show();
+        } else {
+            $('#mp-spr-char').hide();
+        }
+        spreadObj.mergePeg();
+    });
+    // 选择连接符
+    $('#mp-spr-char').change(() => {spreadObj.mergePeg()});
+    // 初始化窗口
+    $('#merge-peg').on('show.bs.modal', function () {
+        sheet.clear(0, 0, sheet.getRowCount(), sheet.getColumnCount(),
+            spreadNS.SheetArea.viewport, spreadNS.StorageType.data);
+    });
+    $('#merge-peg').on('shown.bs.modal', function () {
+        spread.refresh();
+    });
+    $('#mp-ok').click(() => {
+        if (setting.callback) {
+            setting.callback(spreadObj.getPegs());
+        }
+        $('#merge-peg').modal('hide');
+    });
+
+    const showModal = function () {
+        $('#merge-peg').modal('show');
+    };
+    return {show: showModal};
+};

+ 1 - 1
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -88,7 +88,7 @@ const SpreadJsObj = {
         return spread;
     },
     addCutEvents: function (spread, fun) {
-        const cut = spreadNS.Commands.cut.execute;
+        const cut = spreadNS.Commands.copy.execute;
         spreadNS.Commands.cut.execute = function (context, options, isUndo) {
             const sheet = context.getActiveSheet();
             const sels = sheet.getSelections();

+ 30 - 8
app/public/js/stage.js

@@ -972,7 +972,7 @@ $(document).ready(() => {
         loadExprToInput(sheet) {
             const sel = sheet.getSelections()[0];
             const col = sheet.zh_setting.cols[sel.col], cell = sheet.getCell(sel.row, sel.col);
-            if (col.type === 'Number') {
+            if (col && col.type === 'Number') {
                 const data = SpreadJsObj.getSelectObject(sheet);
                 if (data) {
                     $('#pos-expr').val(data[col.field]).attr('field', col.field).attr('org', data[col.field])
@@ -1293,6 +1293,23 @@ $(document).ready(() => {
         },
         selectionChanged: function (e, info) {
             stagePosSpreadObj.loadExprToInput(info.sheet);
+        },
+        addPegs: function (pegs) {
+            if (!pegs || pegs.length <= 0) return;
+            const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+            if (!node) return;
+
+            const sheet = spSpread.getActiveSheet();
+            const sortData = sheet.zh_data || [];
+            let order = sortData.length > 0 ? sortData[sortData.length - 1].porder + 1 : 1;
+            pegs.forEach(function (p) {p.porder = ++order; p.lid = node.id});
+
+            postData(window.location.pathname + '/update', {pos: {updateType: 'add', updateData: pegs} }, function (result) {
+                if (result.pos) {
+                    stagePos.updateDatas(result.pos.pos);
+                }
+                stagePosSpreadObj.loadCurPosData();
+            });
         }
     };
     // 加载上下窗口resizer
@@ -1367,6 +1384,7 @@ $(document).ready(() => {
         });
     }
     if (!checkTzMeasureType()) {
+        const mergePeg = NewMergePeg({ callback: stagePosSpreadObj.addPegs});
         $.contextMenu({
             selector: '#stage-pos',
             build: function ($trigger, e) {
@@ -1406,17 +1424,21 @@ $(document).ready(() => {
                     callback: function (key, opt) {
                         stagePosSpreadObj.deletePos(spSpread.getActiveSheet());
                     }
+                },
+                'merge-peg': {
+                    name: '合并起讫桩号',
+                    disabled: function (key, opt) {
+                        const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+                        return _.isNil(node) || _.isNil(node.b_code) || node.b_code === '';
+                    },
+                    callback: function (key, opt) {
+                        mergePeg.show();
+                    }
                 }
             }
         })
     } else {
-        $.contextMenu({
-            selector: '#stage-pos',
-            build: function ($trigger, e) {
-                const target = SpreadJsObj.safeRightClickSelection($trigger, e, spSpread);
-                return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
-            }
-        })
+        SpreadJsObj.forbiddenSpreadContextMenu('#stage-pos', spSpread);
     }
     $.subMenu({
         menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',

+ 1 - 1
app/public/js/stage_compare.js

@@ -61,7 +61,7 @@ $(document).ready(function () {
         function checkDiffer(data) {
             const fieldSufs = sheet.zh_setting.fieldSufs;
             if (fieldSufs.length <= 1) return false;
-            const field = data.quantity ? 'gather_qty' : 'gather_tp';
+            const field = !data.is_tp ? 'gather_qty' : 'gather_tp';
             const base = data[field + fieldSufs[0]];
             for (let i = 1; i< fieldSufs.length; i++) {
                 const compare = data[field + fieldSufs[i]];

+ 1 - 0
app/router.js

@@ -209,6 +209,7 @@ module.exports = app => {
     app.get('/change/download/file/:id', sessionAuth, 'changeController.downloadFile');
     app.post('/change/delete/file', sessionAuth, 'changeController.deleteFile');
     app.post('/tender/:id/change/delete', sessionAuth, tenderCheck, 'changeController.delete');
+    app.post('/tender/:id/change/bills', sessionAuth, tenderCheck, 'changeController.bills');
 
     app.post('/change/save', sessionAuth, 'changeController.save');
 

+ 7 - 3
app/service/change.js

@@ -279,7 +279,7 @@ module.exports = app => {
          * @return {void}
          */
         async save(postData, tenderId) {
-            const tenderInfo  = await this.ctx.service.tenderInfo.getTenderInfo(tenderId);
+            const tenderInfo = await this.ctx.service.tenderInfo.getTenderInfo(tenderId);
             // 初始化事务
             this.transaction = await this.db.beginTransaction();
             let result = false;
@@ -369,6 +369,9 @@ module.exports = app => {
                             detail: clInfo[7],
                             spamount: clInfo[6],
                         };
+                        if (clInfo[4] === '') {
+                            delete clArray.unit_price;
+                        }
                         insertCL.push(clArray);
                         total_price = this.ctx.helper.accAdd(total_price,
                             this.ctx.helper.mul(clArray.unit_price, clArray.spamount, tenderInfo.decimal.tp));
@@ -912,7 +915,7 @@ module.exports = app => {
             let result = false;
             try {
                 const changeInfo = await this.getDataByCondition({ cid });
-                const tenderInfo  = await this.ctx.service.tenderInfo.getTenderInfo(changeInfo.tid);
+                const tenderInfo = await this.ctx.service.tenderInfo.getTenderInfo(changeInfo.tid);
 
                 // 获取终审
                 const auditInfo = (await this.ctx.service.changeAudit.getAllDataByCondition({ where: { cid }, orders: [['usort', 'desc']], limit: 1, offset: 0 }))[0];
@@ -954,6 +957,7 @@ module.exports = app => {
                 const changeList = await this.ctx.service.changeAuditList.getAllDataByCondition({ where: { cid: changeInfo.cid } });
                 for (const cl of changeList) {
                     const audit_amount = cl.audit_amount.split(',');
+                    const last_amount = audit_amount[audit_amount.length - 1];
                     audit_amount.splice(-1, 1);
                     const list_update = {
                         id: cl.id,
@@ -961,7 +965,7 @@ module.exports = app => {
                         samount: '',
                     };
                     total_price = this.ctx.helper.add(total_price,
-                        this.ctx.helper.mul(cl.unit_price, cl.camount, tenderInfo.decimal.tp));
+                        this.ctx.helper.mul(cl.unit_price, parseFloat(last_amount), tenderInfo.decimal.tp));
                     await this.transaction.update(this.ctx.service.changeAuditList.tableName, list_update);
                 }
 

+ 14 - 0
app/service/change_audit_list.js

@@ -22,6 +22,20 @@ module.exports = app => {
             super(ctx);
             this.tableName = 'change_audit_list';
         }
+
+        async gatherBgBills (tid) {
+            const sql = 'SELECT cb.code, cb.name, cb.unit, cb.unit_price, Sum(cb.samount + 0) as quantity' +
+                '  FROM ' + this.tableName + ' cb' +
+                '  LEFT JOIN ' + this.ctx.service.change.tableName + ' c ON cb.cid = c.cid' +
+                '  WHERE cb.tid = ? and c.status = ?' +
+                '  GROUP BY code, name, unit, unit_price';
+            const param = [tid, audit.flow.status.checked];
+            const result = await this.db.query(sql, param);
+            for (const b of result) {
+                b.total_price = this.ctx.helper.mul(b.unit_price, b.quantity, this.ctx.tender.info.decimal.tp);
+            }
+            return result;
+        }
     }
 
     return ChangeAuditList;

+ 48 - 0
app/service/report_memory.js

@@ -187,6 +187,54 @@ module.exports = app => {
             }
             return this.stageImData.main;
         }
+
+        async getMonthProgress(tid) {
+            const helper = this.ctx.helper;
+            await this.ctx.service.tender.checkTender(tid);
+            const tender = this.ctx.tender;
+
+            const stages = await this.ctx.service.stage.getValidStages(tender.id);
+            const lastStage = stages.length > 0 ? stages[0] : null;
+            if (lastStage) {
+                await this.ctx.service.stage.checkStageGatherData(lastStage);
+                tender.gather_tp = helper.add(lastStage.contract_tp, lastStage.qc_tp);
+                tender.end_contract_tp = helper.add(lastStage.contract_tp, lastStage.pre_contract_tp);
+                tender.end_qc_tp = helper.add(lastStage.qc_tp, lastStage.pre_qc_tp);
+                tender.end_gather_tp = helper.add(tender.end_contract_tp, tender.end_qc_tp);
+                tender.pre_gather_tp = helper.add(lastStage.pre_contract_tp, lastStage.pre_qc_tp);
+                tender.yf_tp = lastStage.yf_tp;
+                tender.qc_ratio = helper.mul(helper.div(tender.end_qc_tp, tender.info.deal_param.contractPrice, 2), 100);
+                tender.sum = helper.add(tender.total_price, tender.end_qc_tp);
+                tender.pre_ratio = helper.mul(helper.div(tender.pre_gather_tp, tender.sum, 2), 100);
+                tender.cur_ratio = helper.mul(helper.div(tender.gather_tp, tender.sum, 2), 100);
+                tender.other_tp = helper.sub(helper.sub(tender.sum, tender.pre_gather_tp), tender.gather_tp);
+                tender.other_ratio = Math.max(0, 100 - tender.pre_ratio - tender.cur_ratio);
+            }
+            const monthProgress = [];
+            for (const s of stages) {
+                if (s.s_time) {
+                    let progress = monthProgress.find(function (x) {
+                        return x.month === s.s_time;
+                    });
+                    if (!progress) {
+                        progress = {month: s.s_time};
+                        monthProgress.push(progress);
+                    }
+                    progress.tp = helper.add(helper.add(progress.tp, s.contract_tp), s.qc_tp);
+                }
+            }
+            monthProgress.sort(function (x, y) {
+                return Date.parse(x.month) - Date.parse(y.month);
+            });
+            let sum = 0;
+            for (const p of monthProgress) {
+                p.ratio = helper.mul(helper.div(p.tp, tender.sum, 4), 100);
+                sum = helper.add(sum, p.tp);
+                p.end_tp = sum;
+                p.end_ratio = helper.mul(helper.div(p.end_tp, tender.sum, 4), 100);
+            }
+            return monthProgress;
+        }
     }
 
     return ReportMemory;

+ 6 - 6
app/service/revise_bills.js

@@ -43,9 +43,9 @@ module.exports = app => {
          * @param {Number} lid - 清单节点id
          * @returns {Promise<void>}
          */
-        async addReviseNode(tid, rid, lid) {
+        async addReviseNode(tid, rid, lid, count) {
             if (!rid) return null;
-            return await this.addNode(tid, lid, {crid: rid});
+            return await this.addNodeBatch(tid, lid, {crid: rid}, count);
         }
 
         /**
@@ -59,7 +59,7 @@ module.exports = app => {
             const result = { ledger: {}, pos: null };
             if (!tid || !rid || !lid) return result;
 
-            const select = await this.getDataByLid(tid, lid);
+            const select = await this.getDataByKid(tid, lid);
             if (!select) {
                 throw '位置数据错误';
             }
@@ -136,7 +136,7 @@ module.exports = app => {
             // 查询应返回的结果
             result.ledger.create = await this.getDataById(newIds);
             if (!lastChild) {
-                result.ledger.update = await this.getDataByLid(select.id);
+                result.ledger.update = await this.getDataByKid(select.id);
             }
             result.pos = await this.ctx.service.revisePos.getDataByLid(tid, newIds);
             return result;
@@ -154,11 +154,11 @@ module.exports = app => {
             const result = { ledger: {}, pos: null };
             if (!tid || !rid || !lid) return result;
 
-            const select = await this.getDataByLid(tid, lid);
+            const select = await this.getDataByKid(tid, lid);
             if (!select) {
                 throw '位置数据错误';
             }
-            const parentData = await this.getDataByLid(tid, select.ledger_pid);
+            const parentData = await this.getDataByKid(tid, select.ledger_pid);
             if (!parentData) {
                 throw '位置数据错误';
             }

+ 3 - 3
app/view/change/index.ejs

@@ -30,9 +30,9 @@
                 <table class="table table-bordered">
                     <thead>
                     <tr>
-                        <th width="32%">申请编号/变更令号</th><th width="30%">工程名称</th>
-                        <th width="90">变更类别</th><th width="90">变更金额</th>
-                        <th width="100">审批状态</th><th>审批进度</th><th width="80"></th>
+                        <th>申请编号/变更令号</th><th>工程名称</th>
+                        <th width="10%">变更类别</th><th width="10%">变更金额</th>
+                        <th width="10%">审批状态</th><th width="10%">审批进度</th><th width="10%"></th>
                     </tr>
                     </thead>
                     <tbody id="changeList">

+ 8 - 8
app/view/change/info.ejs

@@ -6,8 +6,8 @@
         font-weight: 600;
     }
     .qd-table .input-sm{
-        height:30px;
-        padding:2px;
+        /*height:calc(1.4125rem + 2px);*/
+        /*padding:2px;*/
     }
     .qd-table>tbody>tr>td, .qd-table>tbody>tr>th, .qd-table>tfoot>tr>td, .qd-table>tfoot>tr>th, .qd-table>thead>tr>td, .qd-table>thead>tr>th {
         padding:5px 2px
@@ -18,10 +18,10 @@
         font-size: 0.9rem;
     }
     #list td select{
-        width: 100%;
-        font-size: 0.9rem;
-        vertical-align: middle;
-        line-height: 30px;
+        /*width: 100%;*/
+        /*font-size: 0.9rem;*/
+        /*vertical-align: middle;*/
+        /*line-height: calc(1.4125rem + 2px);*/
     }
     #list td a {
         cursor: pointer;
@@ -454,6 +454,7 @@
                         <td data-site="3"><input class="form-control form-control-sm" placeholder="变更详情" type="text" value="<%= cl.detail %>"></td>
                         <td data-site="4">
                             <select class="form-control form-control-sm input-sm">
+                                <option></option>
                                 <% for (const j in changeUnits) { %>
                                     <% if (changeUnits[j].unit !== undefined && changeUnits[j].unit === cl.unit) { %>
                                 <option selected="selected"><%= changeUnits[j].unit %></option>
@@ -463,7 +464,7 @@
                                 <% } %>
                             </select>
                         </td>
-                        <td data-site="5"><input class="form-control form-control-sm" placeholder="单价" onkeyup="RegNum(this,event,upUnit)" type="text" value="<%= ctx.helper.roundNum(cl.unit_price, upUnit) %>"></td>
+                        <td data-site="5"><input class="form-control form-control-sm" placeholder="单价" onkeyup="RegNum(this,event, <%= upUnit %>)" type="text" value="<%= ctx.helper.roundNum(cl.unit_price, upUnit) %>"></td>
                         <td data-site="6"><input class="form-control form-control-sm" placeholder="数量" onkeyup="RegNum(this,event,<%= ctx.helper.findDecimal(cl.unit) %>)" type="text" value="<%= ctx.helper.roundNum(cl.oamount, ctx.helper.findDecimal(cl.unit)) %>"></td>
                         <td data-site="7"><%= ctx.helper.roundNum(ctx.helper.accMul(cl.unit_price, cl.oamount), tpUnit) %></td>
                         <td data-site="8"><input class="form-control form-control-sm" placeholder="变更数量" onkeyup="RegNum(this,event,<%= ctx.helper.findDecimal(cl.unit) %>)" type="text" value="<%= ctx.helper.roundNum(cl.camount, ctx.helper.findDecimal(cl.unit)) %>"></td>
@@ -534,7 +535,6 @@
                     </tr>
                     </tfoot>
                 </table>
-
                 <table class="table table-striped table-bordered nowrap qd-table table-list" id="tablelist2" cellspacing="0" style="width:100%;display: none;">
                     <thead>
                     <tr>

+ 9 - 7
app/view/change/info_modal.ejs

@@ -66,6 +66,7 @@
                         <div class="card-header">
                             审批流程
                         </div>
+                        <div class="modal-height-500" style="overflow: auto">
                         <ul class="list-group list-group-flush" id="auditList">
                             <% for (const [index, audit] of auditList.entries()) { %>
                                 <% if (audit.usite !== 0) { %>
@@ -78,6 +79,7 @@
                                 <% } %>
                             <% } %>
                         </ul>
+                        </div>
                         <input type="hidden" id="auditIdList" value="">
                     </div>
                 </div>
@@ -231,7 +233,7 @@
                 </div>
                 <div class="modal-body">
                     <div class="row">
-                        <div class="col-4">
+                        <div class="col-4 modal-height-500" style="overflow: auto">
                             <a href="#sub-ap" data-toggle="modal" data-category="" data-target="#sub-ap" id="hideSp">修改审批流程</a>
                             <div class="card mt-3">
                                 <ul class="list-group list-group-flush" id="shenpi-audit-list">
@@ -355,7 +357,7 @@
             </div>
             <div class="modal-body">
                 <div class="row">
-                    <div class="col-4">
+                    <div class="col-4 modal-height-500" style="overflow: auto">
                         <div class="card mt-3">
                             <ul class="list-group list-group-flush">
                                 <% for (const [index,a] of auditList2.entries()) { %>
@@ -445,7 +447,7 @@
                 </div>
                 <div class="modal-body">
                     <div class="row">
-                        <div class="col-4">
+                        <div class="col-4 modal-height-500" style="overflow: auto">
                             <div class="card mt-3">
                                 <ul class="list-group list-group-flush">
                                     <% for (const [index,a] of auditList2.entries()) { %>
@@ -542,7 +544,7 @@
                 </div>
                 <div class="modal-body">
                     <div class="row">
-                        <div class="col-4">
+                        <div class="col-4 modal-height-500" style="overflow: auto">
                             <div class="card mt-3">
                                 <ul class="list-group list-group-flush">
                                     <% for (const [index,a] of auditList2.entries()) { %>
@@ -603,12 +605,12 @@
                                                     </div>
                                                     <div class="alert alert-warning">
                                                         <div class="form-check form-check-inline">
-                                                            <input class="form-check-input" type="radio" name="status" id="change-back" value="5" <% if (index === 1) { %>checked<% } %>>
+                                                            <input class="form-check-input" type="radio" name="status" id="change-back" value="5" <% if (a.usite === 1) { %>checked<% } %>>
                                                             <label class="form-check-label" for="change-back">退回上报 <%= auditList3[0].name %></label>
                                                         </div>
-                                                        <% if (index !== 1) { %>
+                                                        <% if (a.usite !== 1) { %>
                                                             <div class="form-check form-check-inline">
-                                                                <input class="form-check-input" type="radio" name="status" id="chagne-backnew" value="6" <% if (index !== 1) { %>checked<% } %>>
+                                                                <input class="form-check-input" type="radio" name="status" id="chagne-backnew" value="6" <% if (a.usite !== 1) { %>checked<% } %>>
                                                                 <label class="form-check-label" for="chagne-backnew">退回上一审批人 <%= auditList2[a.usite-1].name %></label>
                                                                 <input type="hidden" name="audit_last_id" value="<%= auditList2[a.usite-1].id %>">
                                                             </div>

+ 3 - 0
app/view/layout/layout.ejs

@@ -69,6 +69,9 @@
         toastr[toastInfo.type](toastInfo.message);
     }
     let user = '<%= ctx.session.sessionUser.name %>';
+    <% if (ctx.app.config.is_debug) { %>
+    const is_debug = true;
+    <% } %>
 </script>
 </body>
 

+ 4 - 1
app/view/ledger/audit.ejs

@@ -67,7 +67,10 @@
                 <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
                 <div class="tab-content">
                     <div id="deal-bills" class="tab-pane">
-                        <div class="sjs-bar-2"></div>
+                        <div class="sjs-bar-2">
+                            签约清单
+                            <a href="/tender/<%- ctx.tender.id %>/deal/download/签约清单.xlsx" class="btn btn-sm btn-primary" style="display: none">下载签约清单</a>
+                        </div>
                         <div class="sjs-sh-2" id="deal-bills-spread">
                         </div>
                     </div>

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

@@ -128,6 +128,7 @@
                         <div class="sjs-bar-4">
                             <div class="pb-1">
                                 <a href="#upload-deal" data-toggle="modal" data-target="#upload-deal" class="btn btn-sm btn-primary">上传签约清单</a>
+                                <a href="/tender/<%- ctx.tender.id %>/deal/download/签约清单.xlsx" class="btn btn-sm btn-primary" style="display: none">下载签约清单</a>
                             </div>
                         </div>
                         <div id="deal-bills-spread" class="sjs-sh-4">

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

@@ -401,3 +401,4 @@
     </div>
 </div>
 <% } %>
+<% include ../shares/merge_peg_modal.ejs %>

+ 4 - 18
app/view/measure/stage_modal.ejs

@@ -14,11 +14,11 @@
                 </div>
                 <div class="form-group">
                     <label>计量年月</label>
-                    <input class="datepicker-here form-control form-control-sm" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text" name="date" autocomplete="off">
+                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text" name="date" autocomplete="off">
                 </div>
                 <div class="form-group">
                     <label>开始-截止日期</label>
-                    <input class="datepicker-here form-control form-control-sm" placeholder="点击选择时间" data-range="true" data-multiple-dates-separator=" ~ " data-language="zh" type="text" name="period" autocomplete="off">
+                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" placeholder="点击选择时间" data-range="true" data-multiple-dates-separator=" ~ " data-language="zh" type="text" name="period" autocomplete="off">
                 </div>
             </div>
             <div class="modal-footer">
@@ -149,11 +149,11 @@
                 </div>
                 <div class="form-group">
                     <label>计量年月</label>
-                    <input class="datepicker-here form-control form-control-sm" id="edit-date" name="date" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
+                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" id="edit-date" name="date" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
                 </div>
                 <div class="form-group">
                     <label>开始-截止日期</label>
-                    <input class="datepicker-here form-control form-control-sm" id="edit-period" name="period" placeholder="点击选择时间" data-range="true" data-multiple-dates-separator=" ~ " data-language="zh" type="text">
+                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" id="edit-period" name="period" placeholder="点击选择时间" data-range="true" data-multiple-dates-separator=" ~ " data-language="zh" type="text">
                 </div>
             </div>
             <div class="modal-footer">
@@ -185,20 +185,6 @@
         editPeriod.selectDate(period);
     }
     <% } %>
-    // $('#edit-ok').click(function () {
-    //     const data = {
-    //         order: parseInt($(this).attr('s-order')),
-    //         date: $('input[name=edit-date]').val(),
-    //         period: $('input[name=edit-period]').val(),
-    //     };
-    //     if (data.date === '' || data.period === '') {
-    //         return;
-    //     }
-    //     const tenderId = window.location.pathname.split('/')[2];
-    //     postData('/tender/' + tenderId + '/measure/stage/save', data, function () {
-    //         $('#edit').modal('hide');
-    //     });
-    // });
     $('.datepicker-here').datepicker({
         autoClose: true,
     });

+ 21 - 7
app/view/revise/info.ejs

@@ -106,11 +106,11 @@
                 <div class="tab-content">
                     <div id="xd-content" class="tab-pane">
                         <% if ((revise.status === audit.status.uncheck || revise.status === audit.status.checkNo) && !readOnly) { %>
-                        <div class="sjs-bar-2">
+                        <div class="sjs-bar">
                             <div class="d-flex"><a href="javascirpt: void(0);" class="btn btn-sm btn-outline-success mb-1 ml-auto" id="save">保存</a></div>
                         </div>
                         <% } %>
-                        <div class="sjs-sh-2" style="overflow:auto">
+                        <div class="sjs-sh" style="overflow:auto">
                             <div class="form-group mt-2">
                                 <label >创建时间</label>
                                 <input type="" class="form-control form-control-sm" value="<%- revise.in_time.toLocaleString() %>" disabled>
@@ -128,7 +128,7 @@
                     <div id="search" class="tab-pane">
                     </div>
                     <div id="std-xmj" class="tab-pane">
-                        <div class="sjs-bar-3">
+                        <div class="sjs-bar">
                             <div class="pb-1">
                                 <select class="form-control form-control-sm">
                                     <% for (const c of stdChapters) { %>
@@ -137,11 +137,11 @@
                                 </select>
                             </div>
                         </div>
-                        <div id="std-xmj-spread" class="sjs-sh-3">
+                        <div id="std-xmj-spread" class="sjs-sh">
                         </div>
                     </div>
                     <div id="std-gcl" class="tab-pane">
-                        <div class="sjs-bar-4">
+                        <div class="sjs-bar">
                             <div class="pb-1">
                                 <select class="form-control form-control-sm">
                                     <% for (const b of stdBills) { %>
@@ -150,11 +150,22 @@
                                 </select>
                             </div>
                         </div>
-                        <div id="std-gcl-spread" class="sjs-sh-4">
+                        <div id="std-gcl-spread" class="sjs-sh">
                         </div>
                     </div>
                     <div id="deal-bills" class="tab-pane">
-                        <div id="deal-bills-spread" class="sjs-sh-5">
+                        <div class="sjs-bar">
+                            签约清单
+                            <a href="/tender/<%- ctx.tender.id %>/deal/download/签约清单.xlsx" class="btn btn-sm btn-primary" style="display: none">下载签约清单</a>
+                        </div>
+                        <div id="deal-bills-spread" class="sjs-sh">
+                        </div>
+                    </div>
+                    <div id="bg-bills" class="tab-pane">
+                        <div class="sjs-bar">
+                            变更清单
+                        </div>
+                        <div id="bg-bills-spread" class="sjs-sh">
                         </div>
                     </div>
                 </div>
@@ -179,6 +190,9 @@
                 <li class="nav-item">
                     <a class="nav-link" content="#deal-bills" href="javascript: void(0);">签约清单</a>
                 </li>
+                <li class="nav-item">
+                    <a class="nav-link" content="#bg-bills" href="javascript: void(0);">变更清单</a>
+                </li>
             </ul>
         </div>
     </div>

+ 1 - 0
app/view/revise/info_modal.ejs

@@ -476,6 +476,7 @@
     </div>
 </div>
 <% } %>
+<% include ../shares/merge_peg_modal.ejs %>
 <script>
     <% if (ctx.session.sessionUser.accountId === revise.uid && (revise.status === audit.status.uncheck || revise.status === audit.status.checkNo)) { %>
     const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');

+ 45 - 0
app/view/shares/merge_peg_modal.ejs

@@ -0,0 +1,45 @@
+<!--合并起讫桩号-->
+<div class="modal fade" id="merge-peg" data-backdrop="static">
+    <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="mb-2" style="height: 25px; line-height: 25px">
+                    <div class="d-inline-block mr-3">
+                        <div class="form-check">
+                            <input class="form-check-input" type="checkbox" value="" id="mp-with-pos">
+                            <label class="form-check-label" for="defaultCheck1">
+                                合并位置
+                            </label>
+                        </div>
+                    </div>
+                    <div class="d-inline-block ">
+                        <div class="form-check">
+                            <input class="form-check-input" type="checkbox" value="" id="mp-with-spr-char">
+                            <label class="form-check-label" for="defaultCheck3">
+                                桩号连接符
+                            </label>
+                        </div>
+                    </div>
+                    <div class="d-inline-block" >
+                        <select class="form-control form-control-sm m-0" style="display: none;" id="mp-spr-char">
+                            <option value="~">~(半角)</option>
+                            <option value="-">-(半角)</option>
+                            <option value="~">~(全角)</option>
+                            <option value="-">-(全角)</option>
+                        </select>
+                    </div>
+                </div>
+                <div class="modal-height-500" id="mp-spread">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <a href="javascript: void(0);" class="btn btn-sm btn-primary" id="mp-ok">确定</a>
+            </div>
+        </div>
+    </div>
+</div>
+<!-- 需引入/common_modal/merge_peg.js-->

+ 1 - 0
app/view/stage/modal.ejs

@@ -382,3 +382,4 @@
     </div>
 </div>
 <% include ./audit_modal.ejs %>
+<% include ../shares/merge_peg_modal.ejs %>

+ 1 - 0
app/view/tender/detail_modal.ejs

@@ -1027,6 +1027,7 @@
         postData('/tender/' + tenderId + '/save', prop, function (data) {
             property.deal_param = data.deal_param;
             $('#bd-set-4').modal('hide');
+            window.location.reload();
         });
     }
 

+ 2 - 0
config/config.qa.js

@@ -64,5 +64,7 @@ module.exports = appInfo => {
         disableConsoleAfterReady: false,
     };
 
+    config.is_debug = true;
+
     return config;
 };

+ 5 - 0
config/web.js

@@ -123,14 +123,17 @@ const JsFiles = {
                     "/public/js/js-xlsx/xlsx.full.min.js",
                     "/public/js/js-xlsx/xlsx.utils.js",
                     "/public/js/spreadjs/sheets/gc.spread.sheets.all.10.0.1.min.js",
+                    "/public/js/spreadjs/sheets/interop/gc.spread.excelio.10.0.1.min.js",
                     "/public/js/decimal.min.js",
                     "/public/js/math.min.js",
+                    "/public/js/file-saver/FileSaver.js",
                 ],
                 mergeFiles: [
                     "/public/js/sub_menu.js",
                     "/public/js/div_resizer.js",
                     "/public/js/spreadjs_rela/spreadjs_zh.js",
                     "/public/js/ledger_search.js",
+                    "/public/js/shares/merge_peg.js",
                     "/public/js/zh_calc.js",
                     "/public/js/path_tree.js",
                     "/public/js/ledger_tree_col.js",
@@ -201,6 +204,7 @@ const JsFiles = {
                     "/public/js/div_resizer.js",
                     "/public/js/spreadjs_rela/spreadjs_zh.js",
                     "/public/js/ledger_search.js",
+                    "/public/js/shares/merge_peg.js",
                     "/public/js/zh_calc.js",
                     "/public/js/path_tree.js",
                     "/public/js/std_lib.js",
@@ -238,6 +242,7 @@ const JsFiles = {
                     "/public/js/div_resizer.js",
                     "/public/js/msg_box.js",
                     "/public/js/spreadjs_rela/spreadjs_zh.js",
+                    "/public/js/shares/merge_peg.js",
                     "/public/js/zh_calc.js",
                     "/public/js/path_tree.js",
                     "/public/js/stage_im.js",

文件差异内容过多而无法显示
+ 2867 - 2840
package-lock.json


+ 12 - 2
test/app/service/report_memory.test.js

@@ -31,7 +31,7 @@ describe('test/app/service/report_memory.test.js', () => {
         assert(loginResult);
         mockData.session = ctx.session;
     });
-    // 生成中间计量表数据 - 台账
+    // 中间计量表数据 - 台账
     it('test getStageImTzData & getStageImTzBillsData', function* () {
         const ctx = app.mockContext(mockData);
 
@@ -48,7 +48,7 @@ describe('test/app/service/report_memory.test.js', () => {
             yield ctx.helper.saveBufferFile(JSON.stringify(billsData,"","\t"), ctx.app.baseDir + '/mem_stage_im_tz_bills.json');
         }
     });
-    // 生成中间计量表数据 - 总量
+    // 中间计量表数据 - 总量
     it('test getStageImZlData', function* () {
         const ctx = app.mockContext(mockData);
 
@@ -59,4 +59,14 @@ describe('test/app/service/report_memory.test.js', () => {
             yield ctx.helper.saveBufferFile(JSON.stringify(mainData,"","\t"), ctx.app.baseDir + '/mem_stage_im_zl.json');
         }
     });
+    // 月进度数据
+    it('test getMonthProgress', function* () {
+        const ctx = app.mockContext(mockData);
+
+        // test12
+        const mainData = yield ctx.service.reportMemory.getMonthProgress(12);
+        if (mainData instanceof Array) {
+            yield ctx.helper.saveBufferFile(JSON.stringify(mainData,"","\t"), ctx.app.baseDir + '/mem_month_progress.json');
+        }
+    });
 });