MaiXinRong пре 3 недеља
родитељ
комит
af05f6acf9

+ 5 - 0
app/const/spread.js

@@ -141,6 +141,7 @@ const BaseSetCol = {
         { key: 'ex_memo3', name: '备注3', fixed: [], bills: 1, pos: 1, },
         { key: 'ex_calc1', name: '计算1', fixed: [], bills: 1, pos: 1, },
         { key: 'from', name: '来源', fixed: ['alias'], bills: 1, pos: 1, },
+        { key: 'calc_template', name: '计算模板', fixed: [], bills: 1, pos: 0 },
     ],
     tz_stage_set: [
         { key: 'code', name: '项目节编号', fixed: ['move', 'valid', 'alias'], bills: 1, pos: 0 },
@@ -253,6 +254,7 @@ const glSpreadTemplate = {
         { key: 'ex_memo3', valid: 0 },
         { key: 'ex_calc1', valid: 0 },
         { key: 'from', valid: 0 },
+        { key: 'calc_template', valid: 0 },
     ],
     tz_stage_set: [
         { key: 'code', valid: 1 },
@@ -364,6 +366,7 @@ const szSpreadTemplate = {
         { key: 'ex_memo3', valid: 0 },
         { key: 'ex_calc1', valid: 0 },
         { key: 'from', valid: 0 },
+        { key: 'calc_template', valid: 0},
     ],
     tz_stage_set: [
         { key: 'code', valid: 1 },
@@ -475,6 +478,7 @@ const fjSpreadTemplate = {
         { key: 'ex_memo3', valid: 0 },
         { key: 'ex_calc1', valid: 0 },
         { key: 'from', valid: 0 },
+        { key: 'calc_template', valid: 0},
     ],
     tz_stage_set: [
         { key: 'code', valid: 1 },
@@ -611,6 +615,7 @@ const BaseSpreadColSetting = {
             ],
             from: [{title: '来源', colSpan: '1', rowSpan: '2', field: 'from', hAlign: 1, width: 50, formatter: '@'}],
             is_new_price: [{ title: '新增单价', colSpan: '1', rowSpan: '2', field: 'is_new_price', hAlign: 1, width: 60, cellType: 'checkbox' }],
+            calc_template: [{ title: '计算模板', colSpan: '1', rowSpan: '2', field: 'calc_template', hAlign: 1, width: 100, cellType: 'customizeCombo' }],
         },
         pos: {
             name: [{title: '计量单元', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@'}],

+ 27 - 1
app/controller/ledger_controller.js

@@ -126,6 +126,7 @@ module.exports = app => {
                 tender.data.hasRevise = !!revise;
                 const stage = await this.ctx.service.stage.getDataByCondition({ tid: tender.id });
                 tender.data.hasStage = !!stage;
+                const posCalcTemplate = await this.ctx.service.calcTmpl.getAllTemplateDetail(tender.id, 'posCalc');
                 const renderData = {
                     tender: tender.data,
                     tenderInfo: tender.info,
@@ -152,6 +153,7 @@ module.exports = app => {
                     nodeType: stdConst.nodeType,
                     authMobile: pa.auth_mobile,
                     deleteFilePermission: PermissionCheck.delFile(this.ctx.session.sessionUser.permission),
+                    posCalcTemplate,
                 };
                 if ((tender.data.ledger_status !== auditConst.status.checked && ctx.session.sessionUser.is_admin) ||
                     ((tender.data.ledger_status === auditConst.status.uncheck || tender.data.ledger_status === auditConst.status.checkNo) && tender.data.user_id === ctx.session.sessionUser.accountId)) {
@@ -327,6 +329,9 @@ module.exports = app => {
                         ctx.helper.checkDgnQtyPrecision(data.postData);
                         responseData.data = await ctx.service.ledger.updateCalc(ctx.tender.id, data.postData);
                         break;
+                    case 'extra':
+                        responseData.data = await ctx.service.ledger.updateCalcTemplate(ctx.tender.id, data.postData);
+                        break;
                     case 'paste-block':
                         responseData.data = await this._pasteBlock(ctx, data.postData);
                         break;
@@ -437,8 +442,17 @@ module.exports = app => {
                 const ancillaryGclData = this.ctx.tender.data.measure_type === measureType.tz.value
                     ? await ctx.service.ancillaryGcl.getAllDataByCondition({ where: { tid: ctx.tender.id } })
                     : [];
+                if (this.ctx.tender.data.measure_type === measureType.tz.value) {
+                    const calcTemplate = await ctx.service.ledgerExtra.getCalcTemplateData(ctx.tender.id);
+                    this.ctx.helper.assignRelaData(ledgerData, [
+                        { data: calcTemplate, fields: ['calc_template'], prefix: '', relaId: 'id' },
+                    ]);
+                }
+                const posCalcDetailData = this.ctx.tender.data.measure_type === measureType.tz.value
+                    ? await ctx.service.posCalcDetail.getAllDataByCondition({ where: { tid: ctx.tender.id } })
+                    : [];
                 const ledgerTags = await this.ctx.service.ledgerTag.getDatas(ctx.tender.id);
-                ctx.body = { err: 0, msg: '', data: { bills: ledgerData, pos: posData, ancGcl: ancillaryGclData, tags: ledgerTags } };
+                ctx.body = { err: 0, msg: '', data: { bills: ledgerData, pos: posData, ancGcl: ancillaryGclData, posCalcDetail: posCalcDetailData, tags: ledgerTags } };
             } catch (err) {
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: [] };
@@ -482,6 +496,18 @@ module.exports = app => {
             }
         }
 
+        async posCalcUpdate(ctx) {
+            try {
+                await this.checkMeasureType(measureType.tz.value);
+                const data = JSON.parse(ctx.request.body.data);
+                const responseData = await ctx.service.posCalcDetail.updateDatas(data);
+                ctx.body = { err: 0, msg: '', data: responseData };
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
         /**
          * 更新 部位明细数据
          *

+ 26 - 22
app/controller/spss_controller.js

@@ -539,15 +539,19 @@ module.exports = app => {
             if (!stage) return [];
 
             const data = await this.ctx.service.stageJgcl.getStageData(stage);
+            const preData = stage.preCheckedStage ? await this.ctx.service.stageJgcl.getEndStageData(stage.tid, stage.preCheckedStage.order) : [];
             for (const d of data) {
-                d.pre_arrive_qty = d.arrive_qty;
-                d.pre_arrive_tp = d.arrive_tp;
-                d.pre_deduct_qty = d.deduct_qty;
-                d.pre_deduct_tp = d.deduct_tp;
-                d.end_arrive_qty = d.arrive_qty;
-                d.end_arrive_tp = d.arrive_tp;
-                d.end_deduct_qty = d.deduct_qty;
-                d.end_deduct_tp = d.deduct_tp;
+                const pd = this.ctx.helper._.find(preData, {uuid: d.uuid});
+                if (pd) {
+                    d.pre_arrive_qty = this.ctx.helper.add(pd.arrive_qty, d.arrive_qty);
+                    d.pre_arrive_tp = this.ctx.helper.add(pd.arrive_tp, d.arrive_tp);
+                    d.pre_deduct_qty = this.ctx.helper.add(pd.deduct_qty, d.deduct_qty);
+                    d.pre_deduct_tp = this.ctx.helper.add(pd.deduct_tp, d.deduct_tp);
+                }
+                d.end_arrive_qty = d.pre_arrive_qty;
+                d.end_arrive_tp = d.pre_arrive_tp;
+                d.end_deduct_qty = d.pre_deduct_qty;
+                d.end_deduct_tp = d.pre_deduct_tp;
                 d.arrive_qty = 0;
                 d.arrive_tp = 0;
                 d.deduct_qty = 0;
@@ -580,10 +584,10 @@ module.exports = app => {
 
             const data = await this.ctx.service.stageYjcl.getStageData(stage);
             for (const d of data) {
-                d.end_qty = this.ctx.helper.add(d.pre_qty, d.qty);
-                d.end_tp = this.ctx.helper.add(d.pre_tp, d.tp);
-                d.pre_qty = d.end_qty;
-                d.pre_tp = d.pre_tp;
+                d.pre_qty = this.ctx.helper.add(d.pre_qty, d.qty);
+                d.pre_tp = this.ctx.helper.add(d.pre_tp, d.tp);
+                d.end_qty = d.pre_qty;
+                d.end_tp = d.pre_tp;
                 d.qty = 0;
                 d.tp = 0;
             }
@@ -636,10 +640,10 @@ module.exports = app => {
 
             const data = await this.ctx.service.stageSafeProd.getStageData(stage);
             for (const d of data) {
-                d.pre_qty = d.qty;
-                d.pre_tp = d.tp;
-                d.end_qty = d.qty;
-                d.end_tp = d.tp;
+                d.pre_qty = this.ctx.helper.add(d.qty, d.pre_qty);
+                d.pre_tp = this.ctx.helper.add(d.tp, d.pre_tp);
+                d.end_qty = d.pre_qty;
+                d.end_tp = d.pre_tp;
                 d.qty = 0;
                 d.tp = 0;
             }
@@ -676,10 +680,10 @@ module.exports = app => {
 
             const data = await this.ctx.service.stageTempLand.getStageData(stage);
             for (const d of data) {
-                d.pre_qty = d.qty;
-                d.pre_tp = d.tp;
-                d.end_qty = d.qty;
-                d.end_tp = d.tp;
+                d.pre_qty = this.ctx.helper.add(d.qty, d.pre_qty);
+                d.pre_tp = this.ctx.helper.add(d.tp, d.pre_tp);
+                d.end_qty = d.pre_qty;
+                d.end_tp = d.pre_tp;
                 d.qty = 0;
                 d.tp = 0;
             }
@@ -714,8 +718,8 @@ module.exports = app => {
 
             const data = await this.ctx.service.stageOther.getStageData(stage);
             for (const d of data) {
-                d.pre_tp = d.tp;
-                d.end_tp = d.tp;
+                d.pre_tp = this.ctx.helper.add(d.pre_tp, d.tp);
+                d.end_tp = d.pre_tp;
                 d.tp = 0;
             }
             return data;

+ 60 - 0
app/controller/template_controller.js

@@ -53,6 +53,66 @@ module.exports = app => {
                 ctx.body = '参数错误';
             }
         }
+
+        async posCalc(ctx) {
+            try {
+                // if (!ctx.subProject.page_show.posCalc) throw '该功能已关闭';
+                const renderData = {
+                    validColInfo: ctx.service.calcTmpl.TemplateRela.posCalc.ValidColInfo,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.template.posCalc),
+                };
+                renderData.templateList = await ctx.service.calcTmpl.getAllTemplate(ctx.tender.id, 'posCalc');
+                await this.layout('template/pos_calc.ejs', renderData, 'template/preview_modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '查看模板数据错误');
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+
+        //  ------------ 以下方法为所有模板共用 --------------
+        async load(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const filter = data.filter ? data.filter.split(';') : [];
+                const result = {};
+                for (const f of filter) {
+                    switch(f) {
+                        case 'detail':
+                            result[f] = await this.ctx.service.calcTmpl.getTemplate(data.id, data.type);
+                            break;
+                        default:
+                            throw '未知数据类型';
+                    }
+                }
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (error) {
+                ctx.log(error);
+                ctx.ajaxErrorBody(error, '加载数据失败');
+            }
+        }
+        async saveTemplate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const result = await ctx.service.calcTmpl.saveTemplate(data);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '修改数据失败');
+            }
+        }
+        async preview(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const spreadSetting = this.ctx.service.calcTmpl.calcSpreadCache(data.col_set, data.type);
+                const testData = this.ctx.service.calcTmpl.getCalcTestData(data.col_set, data.type);
+                ctx.body = { err: 0, msg: '', data: { spreadSetting, testData} };
+            } catch (error) {
+                ctx.log(error);
+                ctx.ajaxErrorBody(error, '预览失败');
+            }
+        }
+        // ---------------------------------------------------
     }
 
     return TemplateController;

+ 404 - 64
app/public/js/ledger.js

@@ -131,6 +131,321 @@ $(document).ready(function() {
     const ancGclSpread = SpreadJsObj.createNewSpread($('#anc-gcl-spread')[0]);
     const ancGclSheet = ancGclSpread.getActiveSheet();
 
+    // 设计量明细相关
+    const posCalcDetail = (function() {
+        const detail = createPosCalcDetail({ id: 'id', masterId: 'pid', topId: 'lid', sort: [['pcd_order', 'asc']]});
+        const spread = SpreadJsObj.createNewSpread($('#pos-detail-spread')[0]);
+        const sheet = spread.getActiveSheet();
+        const emptySetting = {
+            cols: [],
+            emptyRows: 3,
+            headRows: 1,
+            headRowHeight: [32],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            frozenLineColor: '#93b5e4',
+        };
+        const refresh = function() {
+            if (spread) spread.refresh();
+        };
+        let template;
+        const reloadCurDetailData = function() {
+            const curPos = SpreadJsObj.getSelectObject(posSheet);
+            const detailData = curPos ? detail.getPartData(curPos.id) || [] : [];
+            SpreadJsObj.loadSheetData(sheet, SpreadJsObj.DataType.Data, detailData);
+        };
+        const loadCurDetailData = function() {
+            const node = treeOperationObj.getSelectNode(ledgerSheet);
+            template = posCalcTemplate.find(x => { return x.id === node.calc_template });
+            SpreadJsObj.initSheet(sheet, template ? template.spread_cache : emptySetting);
+            reloadCurDetailData();
+        };
+        const ctrlObj = {
+            afterPostData: function(result) {
+                detail.updateDatas(result.detail);
+                reloadCurDetailData();
+                const updateRst = pos.updateDatas(result.pos);
+                billsTag.refreshPosTagView(updateRst.update);
+                const loadResult = ledgerTree.loadPostData({ update: result.bills });
+                treeOperationObj.refreshTree(ledgerSpread.getActiveSheet(), loadResult);
+                posOperationObj.loadCurPosData();
+                treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet());
+            },
+            baseOpr: function (type) {
+                const data = {};
+
+                const detailRange = sheet.zh_data;
+                if (type !== 'insert' && (!detailRange || detailRange.length === 0)) return;
+
+                const sel = sheet.getSelections();
+                if (!sel[0]) return;
+
+                const row = sel[0].row, count = sel[0].rowCount;
+                const first = detailRange[row];
+                if (type === 'insert') {
+                    const node = SpreadJsObj.getSelectObject(ledgerSheet);
+                    data.add = [{ lid: node.id, pcd_order: detailRange.length + 1 }];
+                } else if (type === 'delete') {
+                    data.del = [];
+                    for (let iRow = 0; iRow < count; iRow++) {
+                        const detailData = detailRange[row + iRow];
+                        if (!detailData) continue;
+                        data.del.push(detailData.id);
+                    }
+
+                    if (data.del.length === 0) return;
+                } else if (type === 'up-move') {
+                    data.update = [];
+                    const pre = detailRange[row - 1];
+                    if (!pre) return;
+
+                    const preUpdate = { id: pre.id };
+                    for (let iRow = 0; iRow < count; iRow++) {
+                        const detailData = detailRange[iRow + row];
+                        if (!detailData) continue;
+                        data.update.push({ id: detailData.id, pcd_order: detailRange[iRow + row - 1].pcd_order });
+                        preUpdate.pcd_order = detailData.pcd_order;
+                    }
+                    data.update.push(preUpdate);
+
+                    if (data.update <= 1) return;
+                } else if (type === 'down-move') {
+                    data.update = [];
+                    const next = detailRange[row + count];
+                    if (!next) return;
+
+                    const nextUpdate = { id: next.id };
+                    for (let iRow = count - 1; iRow >= 0; iRow--) {
+                        const detailData = detailRange[iRow + row];
+                        if (!detailData) continue;
+
+                        data.update.push({ id: detailData.id, pcd_order: detailRange[iRow + row + 1].g_order});
+                        nextUpdate.g_order = detailData.g_order;
+                    }
+                    data.update.push(nextUpdate);
+
+                    if (data.update <= 1) return;
+                }
+
+                postData('/tender/' + getTenderId() + '/pos-calc/update', data, function(result) {
+                    ctrlObj.afterPostData(result);
+                    if (type !== 'delete') SpreadJsObj.locateData(sheet, first);
+                });
+            },
+            editStarting: function (e, info) {
+                ctrlObj.ledgerTreeNode = SpreadJsObj.getSelectObject(ledgerSheet);
+                ctrlObj.posNode = SpreadJsObj.getSelectObject(posSheet);
+            },
+            editEnded: function (e, info) {
+                const setting = info.sheet.zh_setting;
+                if (!setting) return;
+                const detailData = SpreadJsObj.getSelectObject(info.sheet);
+
+                const col = setting.cols[info.col];
+                const orgText = detailData ? detailData[col.field] : '', newText = trimInvalidChar(info.editingText);
+                if (orgText === newText || (!orgText && !newText)) return;
+
+                const pos = ctrlObj.posNode;
+                if (!pos) {
+                    toastr.error('数据错误,请选择计量单元后再试');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+
+                const data = {};
+                if (detailData) {
+                    const updateData = { id: detailData.id };
+                    if (col.type === 'Number') {
+                        const num = _.toNumber(newText);
+                        if (!_.isFinite(num)) {
+                            toastr.error('输入的数字非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                        updateData[col.field] = num;
+                    } else {
+                        updateData[col.field] = newText;
+                    }
+                    data.update = [ updateData ];
+                } else {
+                    const sortData = info.sheet.zh_data;
+                    const order = (!sortData || sortData.length === 0) ? 1 : Math.max(sortData[sortData.length - 1].pcd_order + 1, sortData.length + 1);
+                    const addData = { lid: pos.lid, pid: pos.id, pcd_order: order };
+                    if (col.type === 'Number') {
+                        const num = _.toNumber(newText);
+                        if (!_.isFinite(num)) {
+                            toastr.error('输入的数字非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                        addData[col.field] = num;
+                    } else {
+                        addData[col.field] = newText;
+                    }
+                    data.add = [addData];
+                }
+                postData('/tender/' + getTenderId() + '/pos-calc/update', data, function (result) {
+                    ctrlObj.afterPostData(result);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            },
+            deletePress: function (sheet) {
+                const setting = sheet.zh_setting;
+                if (!setting) return;
+
+                const sortData = sheet.zh_data;
+                if (!sortData || sortData.length === 0) return;
+
+                const sel = sheet.getSelections()[0];
+                const data = { update: [] };
+                for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                    let bDel = false;
+                    const node = sortData[iRow];
+                    if (!node) continue;
+
+                    const updateData = { id: node.id };
+                    for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                        const style = sheet.getStyle(iRow, iCol);
+                        if (style.locked) continue;
+
+                        const col = setting.cols[iCol];
+                        updateData[col.field] = col.type === 'Number' ? 0 : '';
+                        bDel = true;
+                    }
+                    if (bDel) data.update.push(updateData);
+                }
+                if (data.update.length === 0) return;
+
+                postData('/tender/' + getTenderId() + '/pos-calc/update', data, function (result) {
+                    ctrlObj.afterPostData(result);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            },
+            clipboardPasting: function(e, info) {
+                info.cancel = true;
+                const relaPos = ctrlObj.posNode;
+                if (!relaPos) {
+                    toastr.error('数据错误,请选择计量单元后再试');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+
+                const hint = {
+                    num: {type: 'warning', msg: '输入的 数字 非法,已过滤'},
+                };
+                const setting = info.sheet.zh_setting;
+                const sortData = info.sheet.zh_data || [];
+                const pasteData = SpreadJsObj.analysisPasteText(info.pasteData.text);
+                const data = {};
+                const analysisData = function(pasteRow, targetData) {
+                    pasteRow.forEach((value, iCol) => {
+                        const col = setting.cols[info.cellRange.col + iCol];
+                        if (col.type === 'Number') {
+                            const num = _.toNumber(value);
+                            if (!_.isFinite(num)) {
+                                toastMessageUniq(hint.num);
+                                return;
+                            }
+                            targetData[col.field] = num;
+                        } else {
+                            targetData[col.field] = value;
+                        }
+                    });
+                };
+                for (let iRow = 0; iRow < pasteData.length; iRow++) {
+                    const curRow = iRow + info.cellRange.row;
+                    const detailData = sortData[curRow];
+                    if (detailData) {
+                        if (!data.update) data.update = [];
+                        const updateData = { id: detailData.id };
+                        analysisData(pasteData[iRow], updateData);
+                        data.update.push(updateData);
+                    } else {
+                        if (!data.add) data.add = [];
+                        const addData = { lid: relaPos.lid, pid: relaPos.id, g_order: curRow + 1};
+                        analysisData(pasteData[iRow], addData);
+                        data.add.push(addData);
+                    }
+                }
+                if ((!data.update || data.update.length === 0) && (!data.add || data.add.length === 0)) return;
+
+                postData('/tender/' + getTenderId() + '/pos-calc/update', data, function (result) {
+                    ctrlObj.afterPostData(result);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            },
+        };
+        SpreadJsObj.addDeleteBind(spread, ctrlObj.deletePress);
+        spread.bind(spreadNS.Events.EditStarting, ctrlObj.editStarting);
+        spread.bind(spreadNS.Events.EditEnded, ctrlObj.editEnded);
+        spread.bind(spreadNS.Events.ClipboardPasting, ctrlObj.clipboardPasting);
+        $.contextMenu({
+            selector: '#pos-detail-spread',
+            build: function ($trigger, e) {
+                const target = SpreadJsObj.safeRightClickSelection($trigger, e, spread);
+                return target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader;
+            },
+            items: {
+                'insert': {
+                    name: '插入',
+                    icon: 'fa-plus',
+                    disabled: function (key, opt) {
+                        const pos = SpreadJsObj.getSelectObject(posSheet);
+                        return !pos;
+                    },
+                    callback: function (key, opt) {
+                        ctrlObj.baseOpr('insert');
+                    }
+                },
+                'delete': {
+                    name: '删除',
+                    icon: 'fa-remove',
+                    disabled: function (key, opt) {
+                        const detailData = SpreadJsObj.getSelectObject(sheet);
+                        return !detailData;
+                    },
+                    callback: function (key, opt) {
+                        ctrlObj.baseOpr('delete');
+                    }
+                },
+                'down-move': {
+                    name: '下移',
+                    icon: 'fa-arrow-down',
+                    disabled: function(key, opt) {
+                        const sel = sheet.getSelections()[0];
+                        const row = sel ? sel.row : -1;
+                        const first = sheet.zh_data[row];
+                        const next = sheet.zh_data[sel.row + sel.rowCount];
+                        return !first || !next;
+                    },
+                    callback: function(key, opt) {
+                        ctrlObj.baseOpr('down-move');
+                    }
+                },
+                'up-move': {
+                    name: '上移',
+                    icon: 'fa-arrow-up',
+                    disabled: function(key, opt) {
+                        const sel = sheet.getSelections()[0];
+                        const row = sel ? sel.row : -1;
+                        const first = sheet.zh_data[row];
+                        const preNode = sheet.zh_data[row - 1];
+                        return !first || !preNode;
+                    },
+                    callback: function(key, opt) {
+                        ctrlObj.baseOpr('up-move');
+                    }
+                },
+            }
+        });
+
+        return { detail, spread, sheet, refresh, loadCurDetailData }
+    })();
+
     const billsTag = $.billsTag({
         relaTree: ledgerTree,
         selector: '#bills-tag',
@@ -153,6 +468,7 @@ $(document).ready(function() {
             ledgerSpread.refresh();
             if (posSpread) posSpread.refresh();
             if (ancGclSpread) ancGclSpread.refresh();
+            if (posCalcDetail) posCalcDetail.refresh();
         },
     });
     const errorList = $.cs_errorList({
@@ -168,6 +484,7 @@ $(document).ready(function() {
             ledgerSpread.refresh();
             if (posSpread) posSpread.refresh();
             if (ancGclSpread) ancGclSpread.refresh();
+            if (posCalcDetail) posCalcDetail.refresh();
         },
     });
     const checkList = $.ledger_checkList({
@@ -185,6 +502,7 @@ $(document).ready(function() {
             ledgerSpread.refresh();
             if (posSpread) posSpread.refresh();
             if (ancGclSpread) ancGclSpread.refresh();
+            if (posCalcDetail) posCalcDetail.refresh();
         },
     });
 
@@ -211,6 +529,7 @@ $(document).ready(function() {
             ledgerSpread.refresh();
             if (posSpread) posSpread.refresh();
             if (ancGclSpread) ancGclSpread.refresh();
+            if (posCalcDetail) posCalcDetail.refresh();
             if (stdXmj) stdXmj.spread.refresh();
             if (stdGcl) stdGcl.spread.refresh();
             if (dealBills) dealBills.spread.refresh();
@@ -569,88 +888,91 @@ $(document).ready(function() {
          * @param {Object} info
          */
         editEnded: function (e, info) {
-            if (info.sheet.zh_setting) {
-                const col = info.sheet.zh_setting.cols[info.col];
-                const sortData = info.sheet.zh_dataType === 'tree' ? info.sheet.zh_tree.nodes : info.sheet.zh_data;
-                const node = sortData[info.row];
-                const data = {
+            if (!info.sheet.zh_setting) return;
+
+            const col = info.sheet.zh_setting.cols[info.col];
+            const sortData = info.sheet.zh_dataType === 'tree' ? info.sheet.zh_tree.nodes : info.sheet.zh_data;
+            const node = sortData[info.row];
+            const updateData = {postType: 'update', postData: {
                     id: node.id,
                     tender_id: node.tender_id,
                     ledger_id: node.ledger_id
-                };
-                // 未改变值则不提交
-                const orgValue = node[col.field];
-                if (info.editingText === null) info.editingText = '';
-                const newValue = col.wordWrap ? info.editingText : trimInvalidChar(info.editingText);
-                if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) {
-                    return;
-                }
-                // 台账模式,检查计量单元相关
-                if (checkTzMeasureType()) {
-                    if (col.field === 'sgfh_qty' || col.field === 'sgfh_tp' ||
-                        col.field === 'sjcl_qty' || col.field === 'sjcl_tp' ||
-                        col.field === 'qtcl_qty' || col.field === 'qtcl_tp' ||
-                        col.field === 'ex_qty1' || col.field === 'ex_tp1') {
-                        if (!node.children || node.children.length ===0) {
-                            const lPos = pos.getLedgerPos(node.id);
-                            if (lPos && lPos.length > 0) {
-                                toastr.error('清单含有计量单元,请在计量单元输入数量');
-                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
-                                return;
-                            }
-                        }
-                    }
-                    if (col.field === 'b_code' && (info.editingText === '' || !info.editingText)) {
+                }};
+            // 未改变值则不提交
+            const orgValue = node[col.field];
+            if (info.editingText === null) info.editingText = '';
+            const newValue = col.wordWrap ? info.editingText : trimInvalidChar(info.editingText);
+            if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) {
+                return;
+            }
+            // 台账模式,检查计量单元相关
+            if (checkTzMeasureType()) {
+                if (col.field === 'sgfh_qty' || col.field === 'sgfh_tp' ||
+                    col.field === 'sjcl_qty' || col.field === 'sjcl_tp' ||
+                    col.field === 'qtcl_qty' || col.field === 'qtcl_tp' ||
+                    col.field === 'ex_qty1' || col.field === 'ex_tp1') {
+                    if (!node.children || node.children.length ===0) {
                         const lPos = pos.getLedgerPos(node.id);
                         if (lPos && lPos.length > 0) {
-                            toastr.error('清单含有计量单元,请先删除计量单元,再删除清单编号');
+                            toastr.error('清单含有计量单元,请在计量单元输入数量');
                             SpreadJsObj.reLoadRowData(info.sheet, info.row);
                             return;
                         }
                     }
                 }
-                if (col.field === 'node_type' && newValue && newValue !== '0' && newValue !== '19') {
-                    const sameNodeType = sortData.find(x => { return x.node_type == newValue; });
-                    if (sameNodeType) {
-                        toastr.error('已存在该费用类别,请勿重复选择');
+                if (col.field === 'b_code' && (info.editingText === '' || !info.editingText)) {
+                    const lPos = pos.getLedgerPos(node.id);
+                    if (lPos && lPos.length > 0) {
+                        toastr.error('清单含有计量单元,请先删除计量单元,再删除清单编号');
                         SpreadJsObj.reLoadRowData(info.sheet, info.row);
                         return;
                     }
                 }
-                // 获取更新数据
-                if (col.type === 'Number') {
-                    const exprInfo = getExprInfo(col.field);
-                    if (newValue) {
-                        const num = _.toNumber(newValue);
-                        if (_.isFinite(num)) {
-                            data[col.field] = num;
-                        } else {
-                            try {
-                                data[col.field] = ZhCalc.mathCalcExpr(transExpr(newValue));
-                                if (exprInfo) {
-                                    data[exprInfo.expr] = newValue;
-                                }
-                            } catch(err) {
-                                toastr.error('输入的表达式非法');
-                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
-                                return;
-                            }
-                        }
+            }
+            if (col.field === 'node_type' && newValue && newValue !== '0' && newValue !== '19') {
+                const sameNodeType = sortData.find(x => { return x.node_type == newValue; });
+                if (sameNodeType) {
+                    toastr.error('已存在该费用类别,请勿重复选择');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+            }
+            // 获取更新数据
+            if (col.type === 'Number') {
+                const exprInfo = getExprInfo(col.field);
+                if (newValue) {
+                    const num = _.toNumber(newValue);
+                    if (_.isFinite(num)) {
+                        updateData.postData[col.field] = num;
                     } else {
-                        data[col.field] = null;
-                        if (exprInfo) {
-                            data[exprInfo.expr] = '';
+                        try {
+                            updateData.postData[col.field] = ZhCalc.mathCalcExpr(transExpr(newValue));
+                            if (exprInfo) {
+                                updateData.postData[exprInfo.expr] = newValue;
+                            }
+                        } catch(err) {
+                            toastr.error('输入的表达式非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
                         }
                     }
                 } else {
-                    data[col.field] = newValue;
+                    updateData.postData[col.field] = null;
+                    if (exprInfo) {
+                        updateData.postData[exprInfo.expr] = '';
+                    }
                 }
-                // 更新至服务器
-                postData(window.location.pathname + '/update', {postType: 'update', postData: data}, function (result) {
-                    const refreshNode = ledgerTree.loadPostData(result);
-                    treeOperationObj.refreshTree(info.sheet, refreshNode);
-                });
+            } else if (col.field === 'calc_template') {
+                updateData.postType = 'extra';
+                updateData.postData.calc_template = newValue;
+            } else {
+                updateData.postData[col.field] = newValue;
             }
+            // 更新至服务器
+            postData(window.location.pathname + '/update', updateData, function (result) {
+                const refreshNode = ledgerTree.loadPostData(result);
+                treeOperationObj.refreshTree(info.sheet, refreshNode);
+            });
         },
         buttonClicked: function(e, info) {
             if (!info.sheet.zh_setting) return;
@@ -1075,6 +1397,9 @@ $(document).ready(function() {
                         info.cancel = true;
                     }
                     break;
+                case 'calc_template':
+                    info.cancel = node.children && node.children.length > 0 && node.code;
+                    break;
             }
         },
         sortCode: function (sheet) {
@@ -1186,6 +1511,7 @@ $(document).ready(function() {
     //     {title: 'node_type', colSpan: '1', rowSpan: '2', field: 'node_type', hAlign: 2, width: 60, type: 'Number', readOnly: true}
     // );
     sjsSettingObj.setNodeTypeCol(ledgerSpreadSetting.cols, [{ field: 'node_type' }]);
+    sjsSettingObj.setCalcTemplateCol(ledgerSpreadSetting.cols, [{ field: 'calc_template' }]);
     sjsSettingObj.setFromCol(posSpreadSetting.cols, [{field: 'from'}]);
     sjsSettingObj.setFromCol(ledgerSpreadSetting.cols, [{ field: 'from'}]);
     SpreadJsObj.initSheet(ledgerSpread.getActiveSheet(), ledgerSpreadSetting);
@@ -1873,6 +2199,7 @@ $(document).ready(function() {
                 $(".sp-wrap").height(bcontent-30);
                 posSpread.refresh();
                 if (ancGclSpread) ancGclSpread.refresh();
+                if (posCalcDetail) posCalcDetail.refresh();
             }
         });
         sjsSettingObj.setGridSelectStyle(posSpreadSetting);
@@ -2000,6 +2327,7 @@ $(document).ready(function() {
             }
             posOperationObj.loadExprToInput();
             posOperationObj.refreshOperationValid(posSpread.getActiveSheet());
+            posCalcDetail.loadCurDetailData();
         },
         baseOpr: function (sheet, type) {
             const data = {
@@ -2932,6 +3260,10 @@ $(document).ready(function() {
         ancGclObj.loadCurAncillaryGcl();
         SpreadJsObj.resetTopAndSelect(ancGclSheet);
 
+        posCalcDetail.detail.loadDatas(data.posCalcDetail);
+        posCalcDetail.loadCurDetailData();
+        SpreadJsObj.resetTopAndSelect(posCalcDetail.sheet);
+
         treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet());
         treeOperationObj.loadExprToInput(ledgerSpread.getActiveSheet());
 
@@ -2944,6 +3276,7 @@ $(document).ready(function() {
             ledgerSpread.refresh();
             if (posSpread) posSpread.refresh();
             if (ancGclSpread) ancGclSpread.refresh();
+            if (posCalcDetail) posCalcDetail.refresh();
             if (stdXmj) stdXmj.spread.refresh();
             if (stdGcl) stdGcl.spread.refresh();
             if (dealBills) dealBills.spread.refresh();
@@ -3170,6 +3503,7 @@ $(document).ready(function() {
             posSpread.refresh();
         }
         if (ancGclSpread) ancGclSpread.refresh();
+        if (posCalcDetail) posCalcDetail.refresh();
     });
     class DealBills {
         constructor (selector, spreadSetting) {
@@ -4124,9 +4458,9 @@ $(document).ready(function() {
         }
     }
 
-    $('a', '#anc-gcl-nav').bind('click', function(e) {
+    $('a', '#pos-right-btn').bind('click', function(e) {
         e.preventDefault();
-        const tab = $(this);
+        const tab = $(this), tabPanel = $(tab.attr('content'));
         const showSideTab = function (show) {
             const left = $('#pos-spread'), right = $('#pos-right'), parent = left.parent();
             if (show) {
@@ -4150,20 +4484,26 @@ $(document).ready(function() {
 
         };
         if (!tab.hasClass('active')) {
+            $('a', '#pos-right-btn').removeClass('active');
+            $('.tab-pane', '#pos-right').removeClass('active');
             tab.addClass('active');
+            tabPanel.addClass('active');
             showSideTab(tab.hasClass('active'));
         } else {
             tab.removeClass('active');
+            tabPanel.removeClass('active');
             showSideTab(tab.hasClass('active'));
         }
         if (posSpread) posSpread.refresh();
         if (ancGclSpread) ancGclSpread.refresh();
+        if (posCalcDetail) posCalcDetail.refresh();
     });
     $.divResizer({
         select: '#pos-right-spr',
         callback: function () {
             if (posSpread) posSpread.refresh();
             if (ancGclSpread) ancGclSpread.refresh();
+            if (posCalcDetail) posCalcDetail.refresh();
         }
     });
 

+ 163 - 0
app/public/js/path_tree.js

@@ -2287,6 +2287,169 @@ const createAncillaryGcl = function (setting) {
     return new AncillaryGcl(setting);
 };
 
+const createPosCalcDetail = function (setting) {
+    class PosCalcDetail {
+        constructor(setting) {
+            this.itemPre = 'id_';
+            // 无索引
+            this.datas = [];
+            // 以key为索引
+            this.items = {};
+            // 以分类id为索引的有序
+            this.masterIndex = {}; // posCalcDetail ---pid---> pos
+            this.topIndex = {}; // pid ---lid---> bills
+            // 设置
+            this.setting = setting;
+        }
+
+        resortPart(partData) {
+            const sortRule = this.setting.sort || [['pcd_order', 'asc']];
+            if (partData instanceof Array) {
+                partData.sort(function (a, b) {
+                    for (const sr of sortRule) {
+                        const iSort = sr[1] === 'asc' ? a[sr[0]] - b[sr[0]] : b[sr[0]] - a[sr[0]];
+                        if (iSort) return iSort;
+                    }
+                })
+            }
+        }
+
+        /**
+         * 加载数据
+         * @param datas
+         */
+        loadDatas(datas) {
+            this.datas = datas;
+            this.items = {};
+            this.masterIndex = {};
+            this.topIndex = {};
+            for (const data of this.datas) {
+                const key = this.itemPre + data[this.setting.id];
+                this.items[key] = data;
+
+                const masterKey = this.itemPre + data[this.setting.masterId];
+                if (!this.masterIndex[masterKey]) {
+                    this.masterIndex[masterKey] = [];
+                }
+                this.masterIndex[masterKey].push(data);
+                const topKey = this.itemPre + data[this.setting.topId];
+                if (!this.topIndex[topKey]) this.topIndex[topKey] = [];
+                if (this.topIndex[topKey].indexOf(data[this.setting.masterId]) < 0) this.topIndex[topKey].push(data[this.setting.masterId]);
+            }
+            for (const prop in this.masterIndex) {
+                this.resortPart(this.masterIndex[prop]);
+            }
+        }
+
+        _addDatas(data, resort) {
+            const datas = data instanceof Array ? data : [data];
+            for (const d of datas) {
+                const key = this.itemPre + d[this.setting.id];
+                this.items[key] = d;
+
+                const masterKey = this.itemPre + d[this.setting.masterId];
+                if (!this.masterIndex[masterKey]) this.masterIndex[masterKey] = [];
+                this.masterIndex[masterKey].push(d);
+                if (resort.indexOf(masterKey) < 0) resort.push(masterKey);
+
+                const topKey = this.itemPre + data[this.setting.topId];
+                if (!this.topIndex[topKey]) this.topIndex[topKey] = [];
+                if (this.topIndex[topKey].indexOf(data[this.setting.masterId]) < 0) this.topIndex[topKey].push(data[this.setting.masterId]);
+            }
+        }
+
+        /**
+         * 更新数据
+         * @param datas
+         */
+        _updateDatas(data, resort) {
+            const datas = data instanceof Array ? data : [data];
+            for (const d of datas) {
+                const item = this.getItem(d[this.setting.id]);
+                if (!item) continue;
+                for (const prop in d) {
+                    item[prop] = d[prop];
+                }
+                const masterKey = this.itemPre + item[this.setting.masterId];
+                if (resort.indexOf(masterKey) < 0) resort.push(masterKey);
+            }
+
+        }
+
+        /**
+         * 移除数据
+         * @param datas
+         */
+        _removeDatas(data, resort) {
+            if (!data) { return; }
+            const datas = data instanceof Array ? data : [data];
+            for (let i = datas.length - 1; i >= 0; i--) {
+                const id = datas[i];
+                const d = this.getItem(id);
+                this.datas.splice(this.datas.indexOf(d), 1);
+                const key = this.itemPre + d[this.setting.id];
+                delete this.items[key];
+                const range = this.getPartData(d[this.setting.masterId]);
+                range.splice(range.indexOf(d), 1);
+            }
+        }
+
+        updateDatas(data) {
+            const resort = [];
+            if (data.add) this._addDatas(data.add, resort);
+            if (data.del) this._removeDatas(data.del, resort);
+            if (data.update) this._updateDatas(data.update, resort);
+            for (const s of resort) {
+                this.resortPart(this.masterIndex[s]);
+            }
+        }
+
+        /**
+         * 移除数据 - 根据分类id
+         * @param mid
+         */
+        removeDatasByMasterId(mid) {
+            const masterKey = this.itemPre + mid;
+            const range = this.masterIndex[masterKey];
+            if (range) {
+                delete this.masterIndex[masterKey];
+                for (const r of range) {
+                    this.datas.splice(this.datas.indexOf(r), 1);
+                    const key = this.itemPre + r[this.setting.id];
+                    delete this.items[key];
+                }
+            }
+        }
+        removeDatasByTopId(topId) {
+            const topKey = this.itemPre + topId;
+            const topRange = this.topIndex[topKey];
+            if (topRange) {
+                delete this.topIndex[topKey];
+                for (const tr of topRange) {
+                    this.removeDatasByTopId(tr);
+                }
+            }
+        }
+
+        getItem(id) {
+            return this.items[this.itemPre + id];
+        }
+
+        getPartData(mid) {
+            return this.masterIndex[this.itemPre + mid];
+        }
+
+        set sort(sort) {
+            this.setting.sort = sort;
+            for (const key in this.masterIndex) {
+                this.resortPart(this.masterIndex[key]);
+            }
+        }
+    }
+
+    return new PosCalcDetail(setting);
+};
+
 const treeCalc = {
     mapTreeNode: function (tree) {
         const setting = tree.setting;

+ 382 - 0
app/public/js/pos_calc_tmpl.js

@@ -0,0 +1,382 @@
+$(document).ready(() => {
+    autoFlashHeight();
+    class TemplateDetailObj {
+        constructor() {
+            const self = this;
+            this.firstShowDone = false;
+            this.spread = SpreadJsObj.createNewSpread($('#col-set-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            const getTypeValue = function(data) {
+                const typeInfo = validColInfo.find(x => { return x.key === data.type; });
+                return typeInfo.name || '';
+            };
+            this.spreadSetting = {
+                cols: [
+                    { title: '类型', colSpan: '1', rowSpan: '1', field: 'type', hAlign: 1, width: 80, formatter: '@', readOnly: true, getValue: getTypeValue },
+                    { title: '列名', colSpan: '1', rowSpan: '1', field: 'title', hAlign: 0, width: 130, formatter: '@' },
+                    { title: '列宽', colSpan: '1', rowSpan: '1', field: 'width', hAlign: 1, width: 70, type: 'Number' },
+                    { title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 60, cellType: 'unit' },
+                    { title: '计算代号', colSpan: '1', rowSpan: '1', field: 'calc_code', hAlign: 1, width: 80, cellType: 'customizeCombo', comboItems: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J'], },
+                    { title: '小数位数', colSpan: '1', rowSpan: '1', field: 'decimal', hAlign: 1, width: 60, type: 'Number' },
+                    { title: '关联列代号', colSpan: '1', rowSpan: '1', field: 'rela_col', hAlign: 1, width: 80, cellType: 'customizeCombo', comboItems: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J'], },
+                    { title: '计算公式', colSpan: '1', rowSpan: '1', field: 'expr', hAlign: 0, width: 250 },
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                frozenLineColor: '#93b5e4',
+                getColor: function(sheet, data, row, col, defaultColor) {
+                    if (!data) return defaultColor;
+                    const typeInfo = validColInfo.find(x => { return x.key === data.type; });
+                    if (!typeInfo) return defaultColor;
+                    return col && typeInfo.valid.indexOf(col.field) >= 0 ? defaultColor : '#f2f2f2';
+                }
+            };
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.EditStarting, function(e, info){
+                const col = info.sheet.zh_setting.cols[info.col];
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const typeInfo = validColInfo.find(x => { return x.key === select.type; });
+                if (typeInfo) {
+                    info.cancel = col && typeInfo.valid.indexOf(col.field) >= 0 ? false : true;
+                } else {
+                    info.cancel = true;
+                }
+            });
+            this.spread.bind(spreadNS.Events.EditEnded, function(e, info){
+                if (!info.sheet.zh_setting) return;
+
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+
+                const validText = info.editingText ? info.editingText.replace('\n', '') : '';
+                if (col.field === 'calc_code') {
+                    const exist = self.colSetData.find(x => { return x.type === 'num' && x.calc_code === validText && x.field !== select.field; });
+                    if (exist) {
+                        toastr.warning('请勿输入重复的计算代号');
+                    } else {
+                        select.calc_code = validText;
+                    }
+                } else if (col.field === 'width') {
+                    select.width = parseInt(validText);
+                } else if (col.field === 'decimal') {
+                    const num = parseInt(validText);
+                    if (num < 0 || num > 6) {
+                        toastr.warning('小数位数仅可保留0-6位');
+                    } else {
+                        select.decimal = num;
+                    }
+                } else if (col.field === 'rela_col') {
+                    const exist = self.colSetData.find(x => { return x.type === 'num' && x.calc_code === validText });
+                    if (!exist) {
+                        toastr.warning(`暂无计量单号为${validText}的列,请先配置该数值列`);
+                    } else {
+                        select.rela_col = validText;
+                    }
+                } else {
+                    select[col.field] = validText;
+                }
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+            });
+            // 右键菜单
+            $.contextMenu({
+                selector: '#col-set-spread',
+                build: function ($trigger, e) {
+                    const target = SpreadJsObj.safeRightClickSelection($trigger, e, self.spread);
+                    return target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader;
+                },
+                items: {
+                    'add_str': {
+                        name: '新增文本列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('str', select);
+                        },
+                    },
+                    'add_num': {
+                        name: '新增数值列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('num', select);
+                        },
+                    },
+                    'add_spec': {
+                        name: '新增规格列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            toastr.warning('还没想好怎么做,别用!!');
+                            // const select = SpreadJsObj.getSelectObject(self.sheet);
+                            // self.addCol('spec', select);
+                        },
+                    },
+                    'add_qty': {
+                        name: '新增数量列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('qty', select);
+                        },
+                    },
+                    addSpr: '----',
+                    upMove: {
+                        name: '上移',
+                        icon: 'fa-arrow-up',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.move('upMove', select);
+                        },
+                    },
+                    downMove: {
+                        name: '下移',
+                        icon: 'fa-arrow-down',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.move('downMove', select);
+                        },
+                    },
+                }
+            });
+
+            $('#reset').click(function() {
+                self.reset();
+            });
+            $('#save').click(function() {
+                self.save();
+            });
+        }
+        addCol(type, select) {
+            const colInfo = validColInfo.find(x => { return x.key === type; });
+            if (!colInfo) {
+                toastr.error('未知类型');
+                return;
+            }
+            const existCount = this.colSetData.filter(x => { return x.type === type; }).length;
+            if (existCount >= colInfo.count) {
+                toastr.error(`${colInfo.name}列仅支持${colInfo.count}个`);
+                return;
+            }
+            const nData = JSON.parse(JSON.stringify(colInfo.def));
+            for (const f of colInfo.valid) {
+                if (!this.colSetData.find(x => { return x.field === f; })) {
+                    nData.field = f;
+                    break;
+                }
+            }
+            if (select) {
+                const index = this.colSetData.findIndex(x => { return x.field === select.field; });
+                if (index < 0) {
+                    toastr.error('选择的列配置不存在');
+                    return;
+                }
+                this.colSetData.splice(index, 0, nData);
+            } else {
+                this.colSetData.push(nData);
+            }
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
+        }
+        move(type, select) {
+            const index = this.colSetData.findIndex(x => { return x.field === select.field; });
+            if (type === 'upMove' && index === 0) {
+                toastr.error('不可上移');
+                return;
+            }
+            if (type === 'downMove' && index === this.colSetData.length - 1) {
+                toastr.error('不可下移');
+                return;
+            }
+            this.colSetData.splice(index, 1);
+            this.colSetData.splice(type === 'upMove' ? index - 1 : index + 1, 0, select);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
+        }
+        reset() {
+            this.colSetData = JSON.parse(JSON.stringify(this.template.col_set));
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
+        }
+        loadDetail(template) {
+            this.template = template;
+            if (!this.firstShowDone) {
+                this.spread.refresh();
+                this.firstShowDone = true;
+            }
+            this.reset();
+        }
+        save(){
+            const self = this;
+            postData('save', { update: { id: this.template.id, col_set: this.colSetData } }, function(result) {
+                self.template.col_set = result.update.col_set;
+            });
+        }
+        getCurrentColSet() {
+            return this.colSetData;
+        }
+    }
+    const detailObj = new TemplateDetailObj();
+
+    let previewSpreadSetting, previewData, previewSpread;
+    $('#preview-calc-grid').on('shown.bs.modal', function () {
+        if (!previewSpread) {
+            previewSpread = SpreadJsObj.createNewSpread($('#preview-spread')[0]);
+        }
+        const previewSheet = previewSpread.getActiveSheet();
+        SpreadJsObj.initSheet(previewSheet, previewSpreadSetting);
+        const qtyCol = previewSpreadSetting.cols.find(x => { return x.field === 'qty'; });
+        if (qtyCol) {
+            previewData.forEach(x => {
+                let calcExpr = qtyCol.expr;
+                previewSpreadSetting.cols.forEach(c => {
+                    if (c.type !== 'Number' || c.field === 'qty') return;
+                    calcExpr = calcExpr.replace(new RegExp(c.field, 'gm'), x[c.field]);
+                });
+                x.calc_expr = calcExpr;
+                try {
+                    x.qty = ZhCalc.mathCalcExpr(calcExpr.replace(new RegExp('%', 'gm'), '/100'));
+                } catch {
+                    x.qty = 0;
+                }
+            });
+        }
+        SpreadJsObj.loadSheetData(previewSheet, SpreadJsObj.DataType.Data, previewData);
+    });
+    $('#preview').click(function() {
+        const data = { type: 'posCalc', col_set: detailObj.getCurrentColSet() };
+        postData('preview', data, function(result) {
+            result.spreadSetting.readOnly = true;
+            previewSpreadSetting = result.spreadSetting;
+            previewData = result.testData;
+            $('#preview-calc-grid').modal('show');
+        })
+    });
+
+    const templateObj = (function(list){
+        const templates = list;
+        let curTemplate;
+
+        const loadTemplateDetail = async function(template) {
+            const result = await postDataAsync('load', { filter: 'detail', id: template.id, type: 'posCalc' });
+            if (result && result.detail) template.col_set = result.detail.col_set;
+            // template.col_set = [
+            //     { title: '名称', width: 80, type: 'str', field: 'str1' },
+            //     { title: '长度', width: 80, type: 'num', field: 'num1', decimal: 2, unit: 'm', calc_code: 'A' },
+            //     { title: '宽度', width: 80, type: 'num', field: 'num2', decimal: 2, unit: 'm', calc_code: 'B' },
+            //     { title: '高度', width: 80, type: 'num', field: 'num3', decimal: 2, unit: 'm', calc_code: 'C' },
+            //     { title: '数量', width: 80, type: 'qty', field: 'qty', decimal: 2, expr: 'A*B*C' },
+            // ];
+        };
+        const refreshTemplate = async function() {
+            if (!curTemplate) {
+                // todo 隐藏模板详细界面
+            } else {
+                $('dd[templateId]').removeClass('bg-warning');
+                $(`dd[templateId=${curTemplate.id}]`).addClass('bg-warning');
+                if (!curTemplate.col_set) await loadTemplateDetail(curTemplate);
+                detailObj.loadDetail(curTemplate);
+            }
+        };
+        const setCurTemplate = function(template) {
+            curTemplate = template;
+            refreshTemplate();
+        };
+        const getCurTemplate = function() {
+            return curTemplate;
+        };
+        const getTemplateCaptionHtml = function(template) {
+            return `<div class="d-flex justify-content-between align-items-center table-file" templateId="${template.id}"><div>${template.name}</div>` +
+                '    <div class="btn-group-table" style="display: none;">\n' +
+                '    <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameTemplate"><i class="fa fa-pencil fa-fw"></i></a>\n' +
+                '    <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delTemplate"><i class="fa fa-trash-o fa-fw text-danger"></i></a>\n' +
+                '</div></div>';
+        };
+        const getTemplateHtml = function(template) {
+            const html = [];
+            html.push(`<dd class="list-group-item" templateId="${template.id}">`, getTemplateCaptionHtml(template), '</dd>');
+            return html.join('');
+        };
+
+        const addTemplate = function() {
+            postData('save', {add: { name: '' }, type: 'posCalc'}, function(result) {
+                templates.push(result.add);
+                $('#template-list').append(getTemplateHtml(result.add));
+            });
+        };
+        const renameTemplate = function(id, name) {
+            postData('save', { update: { id, name }, type: 'posCalc'}, function(result){
+                const template = templates.find(x => { return x.id === result.update.id; });
+                template.name = result.update.name;
+                $(`dd[templateId=${template.id}]`).html(getTemplateCaptionHtml(template));
+            });
+        };
+        const delTemplate = function(id){
+            postData('save', {del: id, type: 'posCalc'}, function(result) {
+                $(`dd[templateId=${result.del}]`).remove();
+                const tIndex = templates.findIndex(x => { return x.id === id; });
+                templates.splice(tIndex, 1);
+                if (curTemplate.id === id) {
+                    curTemplate = null;
+                    refreshTemplate();
+                }
+            });
+        };
+        if (templates.length > 0) setCurTemplate(templates[0]);
+        return { setCurTemplate, getCurTemplate, addTemplate, delTemplate, renameTemplate }
+    })(templateList);
+
+    $('body').on('click', '.table-file', function(e) {
+        if (this.getAttribute('renaming') === '1') return;
+        if (e.target.tagName === 'A' || e.target.tagName === 'I' || e.target.tagName === 'INPUT') return;
+        const templateId = this.getAttribute('templateId');
+        const template = templateList.find(x => { return x.id === templateId; });
+        templateObj.setCurTemplate(template);
+    });
+    $('body').on('mouseenter', ".table-file", function(){
+        $(this).children(".btn-group-table").css("display","block");
+    });
+    $('body').on('mouseleave', ".table-file", function(){
+        $(this).children(".btn-group-table").css("display","none");
+    });
+
+    $('body').on('click', 'a[name=renameTemplate]', function(e){
+        $(this).parents('.table-file').attr('renaming', '1');
+        $(`#${this.getAttribute('aria-describedby')}`).remove();
+        const templateId = $(this).parents('.table-file').attr('templateId');
+        const template = templateList.find(x => { return x.id === templateId; });
+        if (!template) return;
+
+        const html = [];
+        html.push(`<div><input type="text" class="form-control form-control-sm" style="width: 160px" value="${template.name}"/></div>`);
+        html.push('<div class="btn-group-table" style="display: none;">',
+            `<a href="javascript: void(0)" name="renameOk" class="mr-1"><i class="fa fa-check fa-fw"></i></a>`,
+            `<a href="javascript: void(0)" class="mr-1" name="renameCancel"><i class="fa fa-remove fa-fw text-danger"></i></a>`, '</div>');
+        $(`.table-file[templateId=${templateId}]`).html(html.join(''));
+        e.stopPropagation();
+    });
+    $('body').on('click', 'a[name=renameOk]', function(){
+        const templateId = $(this).parents('.table-file').attr('templateId');
+        const newName = $(this).parents('.table-file').find('input').val();
+        templateObj.renameTemplate(templateId, newName);
+        $(this).parents('.table-file').attr('renaming', '0');
+    });
+    $('body').on('click', 'a[name=renameCancel]', function() {
+        $(this).parents('.table-file').attr('renaming', '0');
+        const templateId = $(this).parents('.table-file').attr('templateId');
+        const template = templateList.find(x => { return x.id === templateId; });
+        if (!template) return;
+
+        const html = [];
+        html.push(`<div>${template.name}</div>`);
+        html.push('<div class="btn-group-table" style="display: none;">',
+            '<a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameTemplate"><i class="fa fa-pencil fa-fw"></i></a>',
+            '<a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameTemplate"><i class="fa fa-pencil fa-fw"></i></a>',
+            '<a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delTemplate"><i class="fa fa-trash-o fa-fw text-danger"></i></a>',
+            '</div>');
+        $(`.table-file[templateId=${templateId}]`).html(html.join(''));
+    });
+
+    $('#addTemplate').click(function() {
+        templateObj.addTemplate();
+    });
+});

+ 16 - 1
app/public/js/shares/sjs_setting.js

@@ -167,6 +167,21 @@ const sjsSettingObj = (function () {
             };
         });
     };
+    const setCalcTemplateCol = function(cols, rela) {
+        setRelaCols(cols, rela, function(col, r) {
+            col.comboItems = (typeof posCalcTemplate === 'undefined') ? [] : posCalcTemplate.map(x => { return { value: x.id, text: x.name }; });
+            col.cellType = 'specCombo';
+            col.cellTypeKey = 'calc_temp';
+            col.comboEdit = function (sheet, data) {
+                if (!data) return false;
+
+                const tree = sheet.zh_tree;
+                if (!tree) return false;
+
+                return data.b_code && (!data.children || data.children.length === 0);
+            };
+        });
+    };
     const setIsTpCol = function(cols, rela) {
         setRelaCols(cols, rela, function(col, r) {
             col.headerCellType = 'tip';
@@ -185,6 +200,6 @@ const sjsSettingObj = (function () {
     return {
         setFxTreeStyle, FxTreeStyle, setGridSelectStyle,
         setTpThousandthFormat, setThousandthFormat, setTpColsThousandthFormat,
-        setPropValue, set3FCols, setQcCols, setOrgPriceCol, setNodeTypeCol, setIsTpCol, setFromCol,
+        setPropValue, set3FCols, setQcCols, setOrgPriceCol, setNodeTypeCol, setIsTpCol, setFromCol, setCalcTemplateCol,
     };
 })();

+ 10 - 5
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -784,11 +784,12 @@ const SpreadJsObj = {
             sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType.stageCombo);
         }
         if (colSetting.cellType === 'specCombo') {
-            if (!sheet.extendCellType.specCombo) {
-                sheet.extendCellType.specCombo = this.CellType.getSpecComboCellType(colSetting.comboItems);
-                SpreadJsObj._addActivePaintEvents(sheet, sheet.extendCellType.specCombo);
+            const cellKey = colSetting.cellTypeKey ? 'specCombo-' + colSetting.cellTypeKey : 'specCombo';
+            if (!sheet.extendCellType[cellKey]) {
+                sheet.extendCellType[cellKey] = this.CellType.getSpecComboCellType(colSetting.comboItems);
+                SpreadJsObj._addActivePaintEvents(sheet, sheet.extendCellType[cellKey]);
             }
-            sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType.specCombo);
+            sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType[cellKey]);
         }
         if (colSetting.cellType === 'datepicker') {
             if (!sheet.extendCellType.datepicker) {
@@ -2526,7 +2527,11 @@ const SpreadJsObj = {
          */
         getCustomizeComboCellType: function (items) {
             let combo = this.getActiveComboCellType();
-            combo.itemHeight(10).editorValueType(spreadNS.CellTypes.EditorValueType.value).items(items);
+            if (typeof items[0] === 'string') {
+                combo.itemHeight(10).items(items);
+            } else {
+                combo.itemHeight(10).editorValueType(spreadNS.CellTypes.EditorValueType.value).items(items);
+            }
             return combo;
         },
         getSpecComboCellType: function (items) {

+ 5 - 0
app/router.js

@@ -620,6 +620,10 @@ module.exports = app => {
     // 标段协作办公
     app.get('/tender/:id/cooperation', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.tenderCooperation');
 
+    app.get('/tender/:id/template/posCalc', sessionAuth, tenderCheck, subProjectCheck, 'templateController.posCalc');
+    app.post('/tender/:id/template/save', sessionAuth, tenderCheck, subProjectCheck, 'templateController.saveTemplate');
+    app.post('/tender/:id/template/load', sessionAuth, tenderCheck, subProjectCheck, 'templateController.load');
+    app.post('/tender/:id/template/preview', sessionAuth, tenderCheck, subProjectCheck, 'templateController.preview');
     // 台账管理相关
     app.get('/tender/:id/ledger', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, ledgerAuditCheck, 'ledgerController.explode');
     app.post('/tender/:id/ledger/load', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.loadExplodeData');
@@ -630,6 +634,7 @@ module.exports = app => {
     app.post('/tender/:id/ledger/dsk', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.importDsk');
     app.get('/tender/:id/ledger/download/:file', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.download');
     app.post('/tender/:id/anc-gcl/update', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.ancGclUpdate');
+    app.post('/tender/:id/pos-calc/update', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.posCalcUpdate');
     app.post('/tender/:id/pos/update', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.posUpdate');
     app.post('/tender/:id/pos/paste', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.posPaste');
     app.post('/tender/:id/ledger/deal2sgfh', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.deal2sgfh');

+ 241 - 0
app/service/calc_tmpl.js

@@ -0,0 +1,241 @@
+'use strict';
+
+/**
+ * 计量单元计算模板
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const ValidTemplateType = ['posCalc'];
+const PosCalc = (function(){
+    const EmptySpreadCache = {
+        cols: [],
+        emptyRows: 3,
+        headRows: 1,
+        headRowHeight: [32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        frozenLineColor: '#93b5e4',
+    };
+    const BaseSpreadColSetting = {
+        str: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
+        num: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
+        spec: { title: '规格', colSpan: '1', rowSpan: '1', field: 'spec', hAlign: 1, width: 80, formatter: '@', cellType: 'spec', relaCol: '' },
+        qty: {  title: '数量', colSpan: '1', rowSpan: '1', field: 'qty', hAlign: 2, width: 80, type: 'Number', readOnly: true },
+    };
+    const ValidColInfo = [
+        { key: 'str', name: '文本', fields: ['str1', 'str2', 'str3', 'str4'], valid: ['title', 'width'], def: { title: '文字', width: 80, type: 'str'} },
+        { key: 'num', name: '数值', fields: ['num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8', 'num9'], valid: ['title', 'width', 'calc_code', 'decimal', 'unit'], def: { title: '数值', width: 80, decimal: 2, type: 'num', unit: ''} },
+        { key: 'spec', name: '规格', fields: ['spec'], valid: ['title', 'width', 'rela_col'], def: { title: '规格', width: 80, rela_col: '', type: 'spec'} },
+        { key: 'qty', name: '数量', fields: ['qty'], valid: ['title', 'width', 'expr', 'decimal'], def: { title: '数量', width: 80, decimal: 2, expr: '', type: 'qty' } },
+    ];
+    ValidColInfo.forEach(vci => { return vci.count = vci.fields.length; });
+    const TestData = {
+        col_set: [
+            { title: '名称', width: 80, type: 'str', field: 'str1' },
+            { title: '长度', width: 80, type: 'num', field: 'num1', decimal: 2, unit: 'm', calc_code: 'A' },
+            { title: '宽度', width: 80, type: 'num', field: 'num2', decimal: 2, unit: 'm', calc_code: 'B' },
+            { title: '高度', width: 80, type: 'num', field: 'num3', decimal: 2, unit: 'm', calc_code: 'C' },
+            { title: '数量', width: 80, type: 'num', field: 'num3', decimal: 2, },
+        ]
+    };
+    return { EmptySpreadCache, BaseSpreadColSetting, ValidColInfo, TestData };
+})();
+
+function randomWord(randomFlag, min, max){
+    let str = "",
+        range = min,
+        arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
+        pos;
+
+    // 随机产生
+    if(randomFlag){
+        range = Math.round(Math.random() * (max-min)) + min;
+    }
+    for(var i=0; i<range; i++){
+        pos = Math.round(Math.random() * (arr.length-1));
+        str += arr[pos];
+    }
+    return str;
+}
+
+module.exports = app => {
+    class CalcTmpl extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'calc_tmpl';
+            this.TemplateRela = { posCalc: PosCalc };
+        }
+
+        async getAllTemplate(tid, type, sort = 'asc') {
+            return this.getAllDataByCondition({
+                columns: ['id', 'name', 'create_user_id'],
+                where: { tid, type },
+                orders: [['create_time', sort]]
+            });
+        }
+        analysisTemplate(data) {
+            const datas = data instanceof Array ? data : [data];
+            datas.forEach(x => {
+                x.col_set = x.col_set ? JSON.parse(x.col_set) : [];
+                x.spread_cache = x.spread_cache ? JSON.parse(x.spread_cache) : {};
+                x.field_cache = x.field_cache ? JSON.parse(x.field_cache) : {};
+                x.decimal = x.decimal ? JSON.parse(x.decimal) : {};
+            });
+        }
+        async getAllTemplateDetail(tid, type) {
+            const result = await this.getAllDataByCondition({
+                where: { tid, type },
+                orders: [['create_time', 'desc']]
+            });
+            this.analysisTemplate(result);
+            return result;
+        }
+        async getTemplate(id) {
+            const result = await this.getDataById(id);
+            this.analysisTemplate(result);
+            return result;
+        }
+        async getLedgerCalcTemplate(tid, lid) {
+            const sql = `SELECT ct.*, l.id FROM ${this.ctx.service.ledgerExtra.departTableName(tid)} l LEFT JOIN ${this.tableName} ct ON l.calc_template = l.id WHERE l.tid = ? and lid = ?`;
+            const result = await this.db.query(sql, [tid, lid]);
+            this.analysisTemplate(result);
+            return result;
+        }
+
+        // 根据列设置计算spreadjs配置相关,方法命名须严格按照【方法_类型】命名
+        transExpr_posCalc(expr, colSet) {
+            let newExpr = expr;
+            colSet.forEach(x => {
+                if (x.type === 'num' && x.calc_code) newExpr = newExpr.replace(new RegExp(x.calc_code, 'gm'), x.field);
+            });
+            return newExpr;
+        }
+        calcSpreadCache_posCalc(colSet) {
+            const relaConst = this.TemplateRela.posCalc;
+            const result = JSON.parse(JSON.stringify(relaConst.EmptySpreadCache));
+            let specCol;
+            for (const col of colSet) {
+                const info = relaConst.ValidColInfo.find(x => { return x.key === col.type });
+                const spreadCol = JSON.parse(JSON.stringify(relaConst.BaseSpreadColSetting[col.type]));
+                spreadCol.field = col.field;
+                info.valid.forEach(v => {
+                    if (v === 'unit' && col.unit) {
+                        spreadCol.title = col.title + '(' + col.unit + ')';
+                    } else if (v === 'expr') {
+                        spreadCol.expr = this.transExpr_posCalc(col.expr, colSet);
+                    } else if (v === 'spec') {
+                        // todo
+                        specCol = spreadCol;
+                    } else {
+                        if (col[v] !== undefined) spreadCol[v] = col[v];
+                    }
+                });
+                if (col.calc_code) spreadCol.title = spreadCol.title + '\n' + col.calc_code;
+                if (col.expr) spreadCol.title = spreadCol.title + '\n' + col.expr;
+                result.cols.push(spreadCol);
+            }
+            if (specCol) {
+                const relaSet = colSet.find(x => { return x.calc_code === specCol.rela_col; });
+                const relaCol = result.cols.find(x => { return x.field === relaSet.field; });
+                relaCol.readOnly = true;
+            }
+            return result;
+        }
+        calcSpreadCache(colSet, type) {
+            const funName = 'calcSpreadCache_' + type;
+            if (this[funName]) return this[funName](colSet);
+            return null;
+        }
+
+        getCalcTestData_posCalc(colSet, count) {
+            const result = [];
+            const testData = {};
+            colSet.forEach(x => {
+                if (x.type === 'num') testData[x.field] = Math.max(Math.floor(Math.random()*10), 1);
+                if (x.type === 'str') testData[x.field] = randomWord(true, 3, 6);
+            });
+            result.push(testData);
+            return result;
+        }
+        getCalcTestData(colSet, type, count = 1) {
+            const funName = 'getCalcTestData_' + type;
+            if (this[funName]) return this[funName](colSet);
+            return null;
+        }
+
+        async _addTemplate(name, type) {
+            if (ValidTemplateType.indexOf(type) < 0) throw '新增的模板类型非法';
+
+            const relaConst = this.TemplateRela[type];
+            const insertData = {
+                id: this.uuid.v4(),
+                pid: this.ctx.session.sessionProject.id,
+                tid: this.ctx.tender.id,
+                name: name || '新增计算模板',
+                type,
+                create_user_id: this.ctx.session.sessionUser.accountId,
+                update_user_id: this.ctx.session.sessionUser.accountId,
+                // 关键默认值
+                col_set: '[]',
+                spread_cache: JSON.stringify(relaConst.EmptySpreadCache),
+                field_cache: '{}',
+                decimal: JSON.stringify([{ def: 2 }]),
+                calc_expr: '',
+            };
+            await this.db.insert(this.tableName, insertData);
+            return await this.getTemplate(insertData.id);
+        }
+        async checkTemplateEdit(id) {
+            const template = await this.getDataById(id);
+            if (!template) throw '编辑的模板不存在';
+            if (template.tid !== this.ctx.tender.id) throw '模板不属于当前标段,请刷新后重试';
+            if (template.create_user_id !== this.ctx.session.sessionUser.accountId) throw '非您创建的模板';
+            return template;
+        }
+        async _saveTemplate(id, data) {
+            const org = await this.checkTemplateEdit(id);
+            const updateData = { id };
+            if (data.name !== undefined) updateData.name = data.name;
+            if (data.col_set !== undefined) {
+                updateData.col_set = JSON.stringify(data.col_set);
+                const spread_cache = this.calcSpreadCache(data.col_set, org.type);
+                updateData.spread_cache = JSON.stringify(spread_cache);
+                const field_cache = {};
+                data.col_set.forEach(x => { field_cache[x.field] = x.title; });
+                updateData.field_cache = JSON.stringify(field_cache);
+                const decimal = { def: 2 };
+                spread_cache.cols.forEach(x => { if (x.decimal !== null && x.decimal !== undefined) decimal[x.field] = x.decimal; });
+                updateData.decimal = JSON.stringify(decimal);
+                const qtyCol = spread_cache.cols.find(x => { return x.field === 'qty'; });
+                updateData.calc_expr = qtyCol ? qtyCol.expr : '';
+            }
+            await this.db.update(this.tableName, updateData);
+            return await this.getTemplate(id);
+        }
+        async _delTemplate(id) {
+            const template = await this.checkTemplateEdit(id);
+            await this.db.delete(this.tableName, { id });
+            return template;
+        }
+
+        async saveTemplate(data) {
+            const result = {};
+            if (data.add) result.add = await this._addTemplate(data.add.name, data.type);
+            if (data.del) result.del = await this._delTemplate(data.del);
+            if (data.update) result.update = await this._saveTemplate(data.update.id, data.update);
+            return result;
+        }
+    }
+
+    return CalcTmpl;
+};

+ 18 - 0
app/service/ledger.js

@@ -869,6 +869,24 @@ module.exports = app => {
                 columns: ['id', ...columns],
             });
         }
+
+        async updateCalcTemplate(tid, data) {
+            const orgExtra = await this.ctx.service.ledgerExtra.getDataById(data.id);
+            const conn = await this.db.beginTransaction();
+            try {
+                if (orgExtra) {
+                    await conn.update(this.service.ledgerExtra.tableName, { id: data.id, calc_template: data.calc_template });
+                } else {
+                    await conn.insert(this.service.ledgerExtra.tableName, { id: data.id, tid, calc_template: data.calc_template });
+                }
+                // todo 清理计算数据 / 重算所有数据
+                await conn.commit();
+                return { update: data };
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
     }
 
     return Ledger;

+ 5 - 0
app/service/ledger_extra.js

@@ -32,6 +32,11 @@ module.exports = app => {
             });
         }
 
+        async getCalcTemplateData(tid) {
+            const sql = `SELECT id, calc_template FROM ${this.departTableName(tid)} WHERE tid = ? and calc_template <> ''`;
+            return await this.db.query(sql, [tid]);
+        }
+
         async updateMultiLimit(id, multi_limit) {
             const exist = await this.getDataById(id);
             if (exist) {

+ 281 - 0
app/service/pos_calc_detail.js

@@ -0,0 +1,281 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+const strField = ['str1', 'str2', 'str3', 'str4'];
+const numField = ['num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8', 'num9'];
+const specialField = ['spec', 'qty', 'expr'];
+const math = require('mathjs');
+math.config({
+    number: 'BigNumber',
+});
+
+module.exports = app => {
+
+    class PosCalcDetail extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'pos_calc_detail';
+        }
+
+        // ---------- 校验&计算数据相关 ------------------
+        getDecimal(decimal, field) {
+            return decimal[field] !== undefined ? decimal[field] || 0 : decimal.def;
+        }
+        _calcExpr(formula) {
+            const percentReg = /((\d+)|((\d+)(\.\d+)))%/g;
+            const percent = formula.match(percentReg);
+            if (percent) {
+                for (const p of percent) {
+                    const v = math.eval(p.replace(new RegExp('%', 'gm'), '/100'));
+                    formula = formula.replace(p, v);
+                }
+            }
+            try {
+                // 使用mathjs计算 math.eval('17259401.95*0.9') = 15533461.754999999,正确应为 15533461.755
+                // const value = math.eval(formula);
+                // 使用逆波兰法四则运算,可防止出现误差,但是只支持四则运算,不支持科学运算
+                // const value = this.ctx.helper.calcExprStrRpn(formula);
+                // 使用mathjs的大数运算,可支持所有
+                const value = parseFloat(math.eval(formula));
+                return Number.isFinite(value) ? value : 0;
+            } catch(err) {
+                return 0;
+            }
+        }
+        // 后端计算
+        _loadDataAndCalc(data, updateData, orgData, template) {
+            let calc = false;
+            for (const sf of strField) {
+                if (updateData[sf] !== undefined) data[sf] = updateData[sf] || '';
+            }
+            if (updateData.spec !== undefined) data.spec = updateData.spec;
+            data.expr = template.calc_expr;
+            for (const nf of numField) {
+                if (updateData[nf] !== undefined) {
+                    data[nf] = this.ctx.helper.round(updateData[nf] || 0, this.getDecimal(template.decimal, nf));
+                    calc = true;
+                }
+                data.expr = data.expr.replace(new RegExp(nf, 'gm'), updateData[nf] || orgData[nf] || 0);
+            }
+            data.qty = this.ctx.helper.round(this._calcExpr(data.expr), this.getDecimal(template.decimal, 'qty'));
+            return calc;
+        }
+        // 依赖前端计算
+        _loadDataResult(data, updateData) {
+            for (const sf of strField) {
+                if (updateData[sf] !== undefined) data[sf] = updateData[sf] || '';
+            }
+            if (updateData.spec !== undefined) data.spec = updateData.spec;
+            if (updateData.qty !== undefined) data.qty = updateData.qty;
+            if (updateData.expr !== undefined) data.expr = updateData.expr;
+            for (const nf of numField) {
+                if (updateData[nf] !== undefined) data[nf] = updateData[nf];
+            }
+        }
+        // --------------------------------------------
+
+        async _getBillsPosUpdateData(data, pid, lid, calc = true) {
+            if (!calc) return [null, null];
+
+            try {
+                const ledgerData = await this.ctx.service.ledger.getDataById(lid);
+                const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, ledgerData.unit);
+                const posDatas = await this.ctx.service.pos.getAllDataByCondition({ where: { lid } });
+                const detailDatas = await this.getAllDataByCondition({ columns: ['id', 'lid', 'pid', 'qty'], where: { pid } });
+
+                let sumPosQty = 0;
+                for (const d of data) {
+                    sumPosQty = this.ctx.helper.add(d.qty, sumPosQty);
+                }
+                for (const d of detailDatas) {
+                    if (data.findIndex(x => { return x.id === d.id; }) >= 0) continue;
+                    sumPosQty = this.ctx.helper.add(d.qty, sumPosQty);
+                }
+                const op = posDatas.find(x => { return x.id === pid; });
+                if (!op) throw '所属计量单元不存在,请刷新页面后再试';
+                const ppd = { id: op.id, sgfh_qty: this.ctx.helper.round(sumPosQty, precision.value) };
+                ppd.quantity = this.ctx.helper.sum([ppd.sgfh_qty, op.sjcl_qty, op.qtcl_qty]);
+
+                let sumBillsQty = 0;
+                for (const pd of posDatas) {
+                    if (pd.id === pid) {
+                        sumBillsQty = this.ctx.helper.add(sumBillsQty, ppd.sgfh_qty);
+                    } else {
+                        sumBillsQty = this.ctx.helper.add(sumBillsQty, pd.sgfh_qty);
+                    }
+                }
+                const bpd = { id: ledgerData.id, sgfh_qty: sumBillsQty };
+                bpd.sgfh_tp = this.ctx.helper.mul(bpd.sgfh_qty, ledgerData.unit_price, this.ctx.tender.info.decimal.tp);
+                bpd.quantity = this.ctx.helper.sum([bpd.sgfh_qty, ledgerData.sjcl_qty, ledgerData.qtcl_qty]);
+                bpd.total_price = this.ctx.helper.mul(bpd.quantity, ledgerData.unit_price, this.ctx.tender.info.decimal.tp);
+                return [ppd, bpd];
+            } catch (err) {
+                console.log(err);
+                return [null, null];
+            }
+        }
+
+        async _addDatas(data, calcMaster) {
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const datas = data instanceof Array ? data : [data];
+            const le = await this.ctx.service.ledgerExtra.getDataById(datas[0].lid);
+            if (!le.calc_template) throw '未定义计算模板,请先在清单处选择计算模板';
+            const calcTemplate = await this.ctx.service.calcTmpl.getTemplate(le.calc_template);
+            if (!calcTemplate) throw '计算模板不存在';
+
+            const insertData = [];
+            let isCalc = false;
+            for (const d of datas) {
+                if (!d.lid || !d.pid || !d.pcd_order) throw '新增其他数据,提交的数据错误';
+                const nd = {
+                    id: this.uuid.v4(), tid: this.ctx.tender.id,
+                    create_user_id: user_id, update_user_id: user_id,
+                    lid: d.lid, pid: d.pid, pcd_order: d.pcd_order,
+                };
+                if (this._loadDataAndCalc(nd, d, {}, calcTemplate)) isCalc = true;
+                insertData.push(nd);
+            }
+            const [posUpdate, billsUpdate] = await this._getBillsPosUpdateData(insertData, insertData[0].pid, insertData[0].lid, isCalc);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.insert(this.tableName, insertData);
+                if (posUpdate) await conn.update(this.ctx.service.pos.tableName, posUpdate);
+                if (billsUpdate) await conn.update(this.ctx.service.ledger.tableName, billsUpdate);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
+            const addData = await this.getAllDataByCondition({
+                where: { id: this.ctx.helper._.map(insertData, 'id') }
+            });
+            const posData = isCalc ? await this.ctx.service.pos.getDataById(addData[0].pid) : null;
+            const ledgerData = isCalc ? await this.ctx.service.ledger.getDataById(addData[0].lid) : null;
+            return [addData, posData, ledgerData]
+        }
+        async _delDatas (data) {
+            if (!data || data.length === 0) throw '提交数据错误';
+            const orgDatas = await this.getAllDataByCondition({ where: { id: data } });
+
+            if (!orgDatas || orgDatas.length === 0) throw '删除的设计量明细不存在';
+
+            let posDetail = await this.getAllDataByCondition({ where: { tid: orgDatas[0].tid, pid: orgDatas[0].pid } });
+            posDetail = posDetail.filter(pa => {
+                return data.indexOf(pa.id) < 0;
+            });
+            posDetail.sort((x, y) => { return x.pcd_order - y.pcd_order; });
+
+            const updateData = [];
+            posDetail.forEach((x, i) => {
+                if (x.pcd_order !== i + 1) updateData.push({ id: x.id, pcd_order: i + 1});
+            });
+            const [posUpdate, billsUpdate] = await this._getBillsPosUpdateData(data.map(x => { return { id: x, qty: 0 }; }), orgDatas[0].pid, orgDatas[0].lid);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { id: data });
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                if (posUpdate) await conn.update(this.ctx.service.pos.tableName, posUpdate);
+                if (billsUpdate) await conn.update(this.ctx.service.ledger.tableName, billsUpdate);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            const posData = await this.ctx.service.pos.getDataById(orgDatas[0].pid);
+            const ledgerData = await this.ctx.service.ledger.getDataById(orgDatas[0].lid);
+            return [data, updateData, posData, ledgerData];
+        }
+        async _updateDatas (data) {
+            if (!data || data.length === 0) throw '提交数据错误';
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const datas = data instanceof Array ? data : [data];
+            const orgDatas = await this.getAllDataByCondition({
+                where: { id: this.ctx.helper._.map(datas, 'id') }
+            });
+            if (!orgDatas || orgDatas.length === 0) throw '修改的设计量明细不存在';
+
+            const le = await this.ctx.service.ledgerExtra.getDataById(orgDatas[0].lid);
+            if (!le.calc_template) throw '未定义计算模板,请先在清单处选择计算模板';
+            const calcTemplate = await this.ctx.service.calcTmpl.getTemplate(le.calc_template);
+            if (!calcTemplate) throw '计算模板不存在';
+
+            const uDatas = [];
+            let isCalc = false;
+            for (const d of datas) {
+                const od = orgDatas.find(x => { return x.id === d.id; });
+                if (!od) continue;
+
+                const nd = { id: od.id, update_user_id: user_id };
+                if (this._loadDataAndCalc(nd, d, od, calcTemplate)) isCalc = true;
+                uDatas.push(nd);
+            }
+            const [posUpdate, billsUpdate] = await this._getBillsPosUpdateData(uDatas, orgDatas[0].pid, orgDatas[0].lid, isCalc);
+
+            if (uDatas.length > 0) {
+                const conn = await this.db.beginTransaction();
+                try {
+                    await conn.updateRows(this.tableName, uDatas);
+                    if (posUpdate) await conn.update(this.ctx.service.pos.tableName, posUpdate);
+                    if (billsUpdate) await conn.update(this.ctx.service.ledger.tableName, billsUpdate);
+                    await conn.commit();
+                } catch (err) {
+                    await conn.rollback();
+                    throw err;
+                }
+                const posData = isCalc ? await this.ctx.service.pos.getDataById(orgDatas[0].pid) : null;
+                const ledgerData = isCalc ? await this.ctx.service.ledger.getDataById(orgDatas[0].lid) : null;
+                return [uDatas, posData, ledgerData];
+            } else {
+                return [];
+            }
+        }
+
+        async updateDatas(data) {
+            const result = { detail: {add: [], del: [], update: []}, pos: null, bills: null };
+            try {
+                if (data.add) {
+                    [result.detail.add, result.pos, result.bills] = await this._addDatas(data.add);
+                }
+                if (data.update) {
+                    [result.detail.update, result.pos, result.bills] = await this._updateDatas(data.update);
+                }
+                if (data.del) {
+                    [result.detail.del, result.detail.update, result.pos, result.bills] = await this._delDatas(data.del);
+                }
+                return result;
+            } catch (err) {
+                if (err.stack) {
+                    throw err;
+                } else {
+                    result.err = err.toString();
+                    return result;
+                }
+            }
+        }
+
+        async deleteBillsPartData(transaction, tid, lid) {
+            await transaction.delete(this.tableName, { tid: tid, lid: lid });
+        }
+        async deletePosPartData(transaction, tid, pid) {
+            await transaction.delete(this.tableName, { tid: tid, lid: pid });
+        }
+    }
+
+    return PosCalcDetail;
+};

+ 2 - 2
app/service/pos_extra.js

@@ -10,7 +10,7 @@
 
 module.exports = app => {
 
-    class LedgerExtra extends app.BaseService {
+    class PosExtra extends app.BaseService {
         /**
          * 构造函数
          *
@@ -33,5 +33,5 @@ module.exports = app => {
         }
     }
 
-    return LedgerExtra;
+    return PosExtra;
 };

+ 17 - 6
app/view/ledger/explode.ejs

@@ -123,9 +123,8 @@
                             </ul>
                         </div>
                         <div class="ml-auto">
-                            <!--<a class="btn btn-sm btn-primary mr-1 mt-1" id="anc-gcl-btn" href="javascript: void(0);">附属工程量</a>-->
-                            <% if (tenderInfo.display.ledger.ancillaryGcl) {%>
-                            <ul class="nav nav-tabs" id="anc-gcl-nav" >
+                            <ul class="nav nav-tabs" id="pos-right-btn" >
+                                <% if (tenderInfo.display.ledger.ancillaryGcl) {%>
                                 <li class="nav-item">
                                     <div class="form-check form-check-inline mt-1">
                                         <input class="form-check-input pt-1" type="checkbox" id="filter-anc-gcl" value="0" name="is_aux">
@@ -133,10 +132,13 @@
                                     </div>
                                 </li>
                                 <li class="nav-item">
-                                    <a class="nav-link" href="javascript:void(0)">附属工程量</a>
+                                    <a class="nav-link" content="#anc-gcl" href="javascript:void(0)">附属工程量</a>
+                                </li>
+                                <% } %>
+                                <li class="nav-item">
+                                    <a class="nav-link" content="#pos-detail" href="javascript:void(0)">设计量明细</a>
                                 </li>
                             </ul>
-                            <% } %>
                         </div>
                     </div>
                     <div class="sp-wrap" style="display: flex; flex-wrap: wrap;">
@@ -144,7 +146,15 @@
                         </div>
                         <div class="c-body" id="pos-right" style="display: none; width: 40%;">
                             <div class="resize-x" id="pos-right-spr" r-Type="width" div1="#pos-spread" div2="#pos-right" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
-                            <div class="sp-wrap" id="anc-gcl-spread">
+                            <div class="tab-content sp-wrap">
+                                <div id="anc-gcl" class="tab-pane table-select-show">
+                                    <div class="sp-wrap" id="anc-gcl-spread">
+                                    </div>
+                                </div>
+                                <div id="pos-detail" class="tab-pane table-select-show">
+                                    <div class="sp-wrap" id="pos-detail-spread">
+                                    </div>
+                                </div>
                             </div>
                         </div>
                     </div>
@@ -403,6 +413,7 @@
     });
     const syncLedgerUrl = '<%- syncLedgerUrl %>';
     const nodeType = JSON.parse('<%- JSON.stringify(nodeType) %>');
+    const posCalcTemplate = JSON.parse(unescape('<%- escape(JSON.stringify(posCalcTemplate)) %>'));
 </script>
 <% if ((tender.ledger_status !== auditConst.status.checked && ctx.session.sessionUser.is_admin) || ((tender.ledger_status === auditConst.status.uncheck || tender.ledger_status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === tender.user_id)) { %>
 <script>

+ 46 - 0
app/view/template/pos_calc.ejs

@@ -0,0 +1,46 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main row">
+            <div class="col-3">
+                <button class="btn btn-sm btn-light text-primary" id="addTemplate"><i class="fa fa-plus" aria-hidden="true"></i> 新增计算模板</button>
+            </div>
+            <div class="col-9" id="detail-ctrl">
+                <a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="reset"> 重置</a>
+                <a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="preview"> 预览</a>
+                <a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="save"> 保存</a>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="row">
+                <div class="col-3">
+                    <div class="sjs-height-0">
+                        <dl class="list-group mt-2 ml-2" id="template-list">
+                            <% for (const template of templateList) { %>
+                            <dd class="list-group-item" templateId="<%- template.id %>">
+                                <div class="d-flex justify-content-between align-items-center table-file" templateId="<%- template.id %>">
+                                    <div><%- template.name %>%></div>
+                                    <div class="btn-group-table" style="display: none;">
+                                        <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameTemplate"><i class="fa fa-pencil fa-fw"></i></a>
+                                        <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delTemplate"><i class="fa fa-trash-o fa-fw text-danger"></i></a>
+                                    </div>
+                                </div>
+                            </dd>
+                            <% } %>
+                        </dl>
+                    </div>
+                </div>
+                <div class="col-9" style="height: 100%">
+                    <div id="col-set-spread" class="sjs-height-0">
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const templateList = JSON.parse('<%- JSON.stringify(templateList) %>');
+    const validColInfo = JSON.parse('<%- JSON.stringify(validColInfo) %>');
+</script>

+ 15 - 0
app/view/template/preview_modal.ejs

@@ -0,0 +1,15 @@
+<div class="modal fade" id="preview-calc-grid" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">预览</h5>
+            </div>
+            <div class="modal-body">
+                <div style="height: 300px" id="preview-spread"></div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 14 - 0
app/view/template/sub_menu.ejs

@@ -0,0 +1,14 @@
+<div class="panel-sidebar">
+    <div class="sidebar-title">
+        模板设置
+    </div>
+    <div class="scrollbar-auto">
+        <div class="nav-box">
+            <ul class="nav-list list-unstyled">
+                <li <% if (ctx.url.indexOf('pos-calc') >= 0) { %>class="active"<% } %>>
+                    <a href="/tender/<%- ctx.tender.id %>/template/pos-calc"><span>计量单元计算</span></a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>

+ 16 - 0
config/web.js

@@ -2465,6 +2465,22 @@ const JsFiles = {
                 mergeFile: 'safe_inspection_information',
             },
         },
+        template: {
+            posCalc: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/zh_calc.js',
+                    '/public/js/sub_menu.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/pos_calc_tmpl.js',
+                ],
+                mergeFile: 'pos_calc_tmpl',
+            }
+        }
     },
 };
 

+ 229 - 0
sql/update.sql

@@ -66,6 +66,235 @@ ADD COLUMN `zb_tp_unit` varchar(10) CHARACTER SET utf8 COLLATE utf8_unicode_ci N
 ALTER TABLE `zh_tender_permission`
 ADD COLUMN `schedule` varchar(255) NOT NULL DEFAULT '' COMMENT '标段进度权限(,分隔,具体见代码定义)' AFTER `safe_payment`;
 
+ALTER TABLE `zh_ledger_extra_0`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_1`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_2`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_3`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_4`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_5`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_6`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_7`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_8`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_9`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_10`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_11`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_12`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_13`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_14`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_15`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_16`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_17`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_18`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_19`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_20`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_21`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_22`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_23`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_24`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_25`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_26`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_27`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_28`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_29`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_30`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_31`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_32`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_33`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_34`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_35`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_36`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_37`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_38`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_39`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_40`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_41`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_42`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_43`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_44`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_45`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_46`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_47`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_48`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_49`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_50`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_51`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_52`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_53`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_54`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_55`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_56`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_57`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_58`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_59`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_60`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_61`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_62`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_63`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_64`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_65`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_66`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_67`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_68`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_69`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_70`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_71`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_72`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_73`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_74`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_75`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_76`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_77`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_78`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_79`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_80`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_81`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_82`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_83`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_84`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_85`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_86`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_87`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_88`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_89`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_90`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_91`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_92`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_93`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_94`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_95`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_96`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_97`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_98`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+ALTER TABLE `zh_ledger_extra_99`
+ADD COLUMN `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板' AFTER `multi_limit`;
+
+CREATE TABLE `zh_pos_calc_detail`  (
+  `id` varchar(36) NOT NULL COMMENT 'uuid',
+  `tid` int(11) NOT NULL COMMENT '标段id(zh_tender.id)',
+  `lid` varchar(36) NOT NULL COMMENT '台账id(zh_ledger.id)',
+  `pid` varchar(36) NOT NULL COMMENT '计量单元id(zh_pos.id)',
+  `pcd_order` int(11) NOT NULL COMMENT '排序',
+  `create_user_id` int(11) NOT NULL COMMENT '新增用户id(zh_project_account.id)',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '新增时间',
+  `update_user_id` int(11) NOT NULL COMMENT '最后修改用户id(zh_project_account.id)',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
+  `qty` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数量',
+  `expr` varchar(255) NOT NULL DEFAULT '' COMMENT '数量计算式',
+  `str1` varchar(255) NOT NULL DEFAULT '' COMMENT '文本1',
+  `str2` varchar(255) NOT NULL DEFAULT '' COMMENT '文本2',
+  `str3` varchar(255) NOT NULL DEFAULT '' COMMENT '文本3',
+  `str4` varchar(255) NOT NULL DEFAULT '' COMMENT '文本4',
+  `spec` varchar(50) NOT NULL DEFAULT '' COMMENT '规格',
+  `num1` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值1',
+  `num2` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值2',
+  `num3` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值3',
+  `num4` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值4',
+  `num5` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值5',
+  `num6` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值6',
+  `num7` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值7',
+  `num8` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值8',
+  `num9` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值9',
+  PRIMARY KEY (`id`)
+);
 
 ------------------------------------
 -- 表数据