Explorar o código

Merge branch 'dev' of http://192.168.1.41:3000/maixinrong/Calculation into dev

Tony Kang hai 23 horas
pai
achega
41cf2e24a2

+ 12 - 3
app/base/base_budget_service.js

@@ -38,8 +38,7 @@ class BaseBudget extends TreeService {
         this.tableName = tablename;
     }
 
-    async initByTemplate(conn, budgetId, templateId){
-        if (!conn) throw '内部错误';
+    async initByTemplate(conn, budgetId, templateId) {
         if (budgetId <= 0) throw '概算项目id错误';
         const data = await this.ctx.service.tenderNodeTemplate.getData(templateId);
         if (!data.length) throw '模板数据有误';
@@ -62,10 +61,20 @@ class BaseBudget extends TreeService {
                 node_type: tmp.node_type,
             });
         }
-        const operate = await conn.insert(this.tableName, insertData);
+        const operate = conn ? await conn.insert(this.tableName, insertData) : await this.db.insert(this.tableName, insertData);
         return operate.affectedRows === data.length;
     }
 
+    async reInitByTemplate(budgetId, templateId) {
+        const conn = await this.db.beginTransaction();
+        try {
+            await conn.delete(this.tableName, { bid: budgetId });
+            await this.initByTemplate(conn, budgetId, templateId);
+        } catch {
+            await conn.rollback();
+        }
+    }
+
     async addChild(budgetId, selectId, data) {
         if ((budgetId <= 0) || (selectId <= 0)) return [];
         const selectData = await this.getDataByKid(budgetId, selectId);

+ 14 - 7
app/base/base_tree_service.js

@@ -804,7 +804,7 @@ class TreeService extends Service {
      * @return {Object}
      * @private
      */
-    async _syncUplevelChildren(select) {
+    async _syncUplevelChildren(select, isParentToTop = false) {
         this.initSqlBuilder();
         this.sqlBuilder.setAndWhere(this.setting.mid, {
             value: this.db.escape(select[this.setting.mid]),
@@ -818,10 +818,17 @@ class TreeService extends Service {
             value: 1,
             selfOperate: '-',
         });
-        this.sqlBuilder.setUpdateData(this.setting.fullPath, {
-            value: [this.setting.fullPath, this.db.escape(`-${select[this.setting.pid]}-`), this.db.escape('-')],
-            literal: 'Replace',
-        });
+        if (isParentToTop) {
+            this.sqlBuilder.setUpdateData(this.setting.fullPath, {
+                value: [this.setting.fullPath, this.db.escape(`${select[this.setting.pid]}-`), this.db.escape('')],
+                literal: 'Replace',
+            });
+        } else {
+            this.sqlBuilder.setUpdateData(this.setting.fullPath, {
+                value: [this.setting.fullPath, this.db.escape(`-${select[this.setting.pid]}-`), this.db.escape('-')],
+                literal: 'Replace',
+            });
+        }
         const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
         const data = await this.transaction.query(sql, sqlParam);
 
@@ -924,7 +931,7 @@ class TreeService extends Service {
                 updateData[this.setting.pid] = parent[this.setting.pid];
                 updateData[this.setting.order] = parent[this.setting.order] + i + 1;
                 updateData[this.setting.level] = s[this.setting.level] - 1;
-                updateData[this.setting.fullPath] = s[this.setting.fullPath].replace(`-${s[this.setting.pid]}-`, '-');
+                updateData[this.setting.fullPath] = s[this.setting.level] === 2 ? s[this.setting.kid] + '' : s[this.setting.fullPath].replace(`-${s[this.setting.pid]}-`, '-');
                 newPath.push(updateData[this.setting.fullPath]);
                 if (s[this.setting.isLeaf] && s.id === last.id) {
                     const nexts = await this.getNextsData(mid, parent[this.setting.kid], last[this.setting.order]);
@@ -935,7 +942,7 @@ class TreeService extends Service {
                 }
                 await this.transaction.update(this.tableName, updateData);
                 // 选中节点--全部子节点(含孙) level-1, fullPath变更
-                await this._syncUplevelChildren(s);
+                await this._syncUplevelChildren(s, s[this.setting.level] === 2);
             }
             // 选中节点--全部后兄弟节点 收编为子节点 修改pid, order, fullPath
             await this._syncUpLevelNexts(last);

+ 3 - 0
app/const/source_type.js

@@ -14,6 +14,9 @@ const sourceTypeData = [
     { id: 101, name: '安全生产费', key: 'payment_safe', params: { tender_id: 1, detail_id: 1 } },
     { id: 200, name: '项目进度', key: 'budget', params: { sp_id: 'uuid', budget_id: 1 } },
     { id: 300, name: '合同管理', key: 'contract', params: { tender_id: 1 } },
+    { id: 400, name: '成本报审', key: 'cost_ledger', params: { tender_id: 1, cost_stage_id: 'uuid' } },
+    { id: 401, name: '财务账面', key: 'cost_book', params: { tender_id: 1, cost_stage_id: 'uuid' } },
+    { id: 402, name: '成本分析', key: 'cost_analysis', params: { tender_id: 1, cost_stage_id: 'uuid' } },
     // { id: 301, name: '标段合同', key: 'tender_contract' },
 ];
 

+ 19 - 1
app/controller/budget_controller.js

@@ -21,6 +21,7 @@ const ValidDskProject = {
     gai: [5],
     yu: [1, 18],
     zb: [4, 18, 19],
+    ctrl: [4, 18, 19],
 };
 const DSK = require('../lib/dsk');
 const sendToWormhole = require('stream-wormhole');
@@ -50,6 +51,7 @@ module.exports = app => {
                     bl.gai_tp = await ctx.service.budgetGai.getSumTp(bl.budget_id);
                     bl.yu_tp = await ctx.service.budgetYu.getSumTp(bl.budget_id);
                     bl.zb_tp = await ctx.service.budgetZb.getSumTp(bl.budget_id);
+                    bl.ctrl_tp = await ctx.service.budgetCtrl.getSumTp(bl.budget_id);
                 }
                 renderData.tenderList = await ctx.service.tender.getList4Select('stage');
                 renderData.categoryData = await this.ctx.service.category.getAllCategory(ctx.subProject);
@@ -121,6 +123,7 @@ module.exports = app => {
                     data.gai = await ctx.service.budgetGai.getData(ctx.budget.id);
                     data.yu = await ctx.service.budgetYu.getData(ctx.budget.id);
                     data.zb = await ctx.service.budgetZb.getData(ctx.budget.id);
+                    data.ctrl = await ctx.service.budgetCtrl.getData(ctx.budget.id);
                 }
                 ctx.body = { err: 0, msg: '', data };
             } catch (err) {
@@ -187,13 +190,14 @@ module.exports = app => {
                 case 'gai': return this.ctx.service.budgetGai;
                 case 'yu': return this.ctx.service.budgetYu;
                 case 'zb': return this.ctx.service.budgetZb;
+                case 'ctrl': return this.ctx.service.budgetCtrl;
                 default: return null;
             }
         }
         async _getNeedGcl() {
             if (!this.ctx.params.btype) throw '参数错误';
             const funRela = this.ctx.subProject.fun_rela;
-            return ['yu', 'zb'].indexOf(this.ctx.params.btype) >= 0 && !!funRela.needGcl;
+            return ['yu', 'zb', 'ctrl'].indexOf(this.ctx.params.btype) >= 0 && !!funRela.needGcl;
         }
 
         async detail(ctx) {
@@ -396,6 +400,20 @@ module.exports = app => {
             }
         }
 
+        async init(ctx) {
+            const relaService = this._getRelaService(ctx.params.btype);
+            try {
+                const data = await relaService.getData(ctx.budget.id);
+                if (data.length > 0) throw '已有数据,请勿重复初始化';
+                const budgetStd = await this.ctx.service.budgetStd.getDataById(this.ctx.budget.std_id);
+                await relaService.initByTemplate(null, this.ctx.budget.id, budgetStd[ctx.params.btype + '_template_id']);
+            } catch(err) {
+                ctx.log(err);
+                ctx.postError(err, '初始化数据错误');
+            }
+            ctx.redirect(ctx.request.header.referer);
+        }
+
         async decimal(ctx) {
             try {
                 if (!ctx.budget) throw '项目数据错误';

+ 1 - 1
app/controller/spss_controller.js

@@ -946,7 +946,7 @@ module.exports = app => {
         }
 
         async _testLoadReport(data) {
-            const reportData = await this.ctx.service.report.getReportData(data.source_type, data.params, data.source_filters);
+            const reportData = await this.ctx.service.report.getReportData(data.source_type, data.params, data.source_filters, {});
             this.ctx.body = { err: 0, msg: '', data: reportData };
         }
         async testLoad(ctx) {

+ 2 - 0
app/controller/sub_proj_controller.js

@@ -236,6 +236,8 @@ module.exports = app => {
                 if (info.sgt_tp_unit === '万元') info.sgt_tp = this.ctx.helper.div(info.sgt_tp, 10000, 6);
                 info.zbys_tp = await this.ctx.service.budgetZb.getSumTp(ctx.subProject.budget_id);
                 if (info.zbys_tp_unit === '万元') info.zbys_tp = this.ctx.helper.div(info.zbys_tp, 10000, 6);
+                info.kzmb_tp = await this.ctx.service.budgetCtrl.getSumTp(ctx.subProject.budget_id);
+                if (info.kzmb_tp_unit === '万元') info.kzmb_tp = this.ctx.helper.div(info.kzmb_tp, 10000, 6);
                 const renderData = {
                     info,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.subProject.info),

+ 3 - 1
app/controller/template_controller.js

@@ -190,7 +190,7 @@ module.exports = app => {
             try {
                 const template = await ctx.service.calcTmpl.getTemplate(ctx.query.tid);
                 if (!template) throw '计算模板不存在';
-                if (template.project_id !== ctx.session.sessionProject.id) throw '您无权导出该模板数据';
+                if (template.pid !== ctx.session.sessionProject.id) throw '您无权导出该模板数据';
 
                 const Cpd = require('../lib/crypt').cpd;
                 const cpd = new Cpd();
@@ -213,6 +213,7 @@ module.exports = app => {
                 });
                 ctx.body = await fs.readFileSync(filepath);
             } catch(err) {
+                console.log(err);
                 ctx.log(err);
                 ctx.postError(err, '导出模板数据失败');
             }
@@ -242,6 +243,7 @@ module.exports = app => {
                 const Cpd = require('../lib/crypt').cpd;
                 const cpd = new Cpd();
                 const data = await cpd.decrypt(filepath);
+                if (!data.col_set || !data.multi_header) throw '模板文件格式错误';
                 if (template.type !== data.type) throw '导入的模板文件与当前模板类型不一致,不可导入';
 
                 const result =await ctx.service.calcTmpl.saveTemplate({ update: { id: template.id, col_set: data.col_set, multi_header: data.multi_header } });

+ 20 - 0
app/lib/budget_final.js

@@ -155,6 +155,20 @@ class BudgetFinal {
         });
     }
 
+    async _loadCtrl(budget) {
+        const helper = this.ctx.helper;
+        const ctrl = await this.ctx.service.budgetCtrl.getData(budget.id);
+        const ctrlTree = new BillsTree(this.ctx, { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] });
+        ctrlTree.loadDatas(ctrl);
+        ctrlTree.calculateAll();
+        this.finalTree.loadTree(ctrlTree, function (cur, source) {
+            cur.base = true;
+            cur.ctrl_dgn_qty1 = helper.add(cur.ctrl_dgn_qty1, source.dgn_qty1);
+            cur.ctrl_dgn_qty2 = helper.add(cur.ctrl_dgn_qty2, source.dgn_qty2);
+            cur.ctrl_tp = helper.add(cur.ctrl_tp, source.total_price);
+        });
+    }
+
     async _loadTender(id) {
         const helper = this.ctx.helper;
         const bills = await this.ctx.service.ledger.getFinalData(id, ['id', 'ledger_id', 'ledger_pid', 'level', 'order', 'full_path', 'is_leaf',
@@ -252,6 +266,10 @@ class BudgetFinal {
             node.zb_dgn_qty = node.zb_dgn_qty1
                 ? (node.zb_dgn_qty2 ? node.zb_dgn_qty1 + '/' + node.zb_dgn_qty2 : node.zb_dgn_qty1 + '')
                 : (node.zb_dgn_qty2 ? '/' + node.zb_dgn_qty2 : '');
+            node.ctrl_dgn_price = helper.div(node.ctrl_tp, node.ctrl_dgn_qty1, 2);
+            node.ctrl_dgn_qty = node.ctrl_dgn_qty1
+                ? (node.ctrl_dgn_qty2 ? node.ctrl_dgn_qty1 + '/' + node.ctrl_dgn_qty2 : node.ctrl_dgn_qty1 + '')
+                : (node.ctrl_dgn_qty2 ? '/' + node.ctrl_dgn_qty2 : '');
 
             node.final_dgn_price = helper.div(node.final_tp, node.final_dgn_qty1, 2);
             node.final_dgn_qty = node.final_dgn_qty1
@@ -293,6 +311,7 @@ class BudgetFinal {
                 gai_dgn_qty1: x.gai_dgn_qty1 || 0, gai_dgn_qty2: x.gai_dgn_qty2 || 0, gai_dgn_qty: x.gai_dgn_qty || '', gai_dgn_price: x.gai_dgn_price || 0, gai_tp: x.gai_tp || 0,
                 yu_dgn_qty1: x.yu_dgn_qty1 || 0, yu_dgn_qty2: x.yu_dgn_qty2 || 0, yu_dgn_qty: x.yu_dgn_qty || '', yu_dgn_price: x.yu_dgn_price || 0, yu_tp: x.yu_tp || 0,
                 zb_dgn_qty1: x.zb_dgn_qty1 || 0, zb_dgn_qty2: x.zb_dgn_qty2 || 0, zb_dgn_qty: x.zb_dgn_qty || '', zb_dgn_price: x.zb_dgn_price || 0, zb_tp: x.zb_tp || 0,
+                ctrl_dgn_qty1: x.ctrl_dgn_qty1 || 0, ctrl_dgn_qty2: x.ctrl_dgn_qty2 || 0, ctrl_dgn_qty: x.ctrl_dgn_qty || '', ctrl_dgn_price: x.ctrl_dgn_price || 0, ctrl_tp: x.ctrl_tp || 0,
 
                 dgn_qty1: x.dgn_qty1 || 0, dgn_qty2: x.dgn_qty2 || 0, total_price: x.total_price || 0,
                 dgn_price: x.dgn_price || 0, dgn_qty: x.dgn_qty || '',
@@ -315,6 +334,7 @@ class BudgetFinal {
         for (const t of final.tender) {
             await this._loadTender(t);
         }
+        await this._loadCtrl(budget);
         await this._loadZb(budget);
         await this._loadYu(budget);
         await this._loadGai(budget);

+ 12 - 0
app/lib/rm/budget.js

@@ -84,6 +84,14 @@ class rptMemPaymentSafe extends RptMemBase {
         // return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
         return tree.getDefaultDatas();
     }
+    async budgetCtrl(bid, showLevel = false) {
+        const ctrl = await this.ctx.service.budgetCtrl.getAllDataByCondition({ where: { bid } });
+        const tree = new ledger.billsTree(this.ctx, { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] });
+        tree.loadDatas(ctrl);
+        tree.calculateAll();
+        // return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
+        return tree.getDefaultDatas();
+    }
     async budgetFinalInfo(bid) {
         const budget = this.ctx.budget && this.ctx.budget.id === bid
             ? this.ctx.budget
@@ -118,6 +126,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return this.budgetYu(this.ctx.budget.id);
             case 'mem_budget_zb':
                 return this.budgetZb(this.ctx.budget.id);
+            case 'mem_budget_ctrl':
+                return this.budgetCtrl(this.ctx.budget.id);
             case 'mem_budget_final':
                 return this.budgetFinal(this.ctx.budget.id);
             case 'mem_budget_gu_filter':
@@ -128,6 +138,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return this.budgetYu(this.ctx.budget.id, true);
             case 'mem_budget_zb_filter':
                 return this.budgetZb(this.ctx.budget.id, true);
+            case 'mem_budget_ctrl_filter':
+                return this.budgetCtrl(this.ctx.budget.id, true);
             case 'mem_budget_final_filter':
                 return this.budgetFinal(this.ctx.budget.id, true);
             default:

+ 161 - 0
app/lib/rm/cost_analysis_stage.js

@@ -0,0 +1,161 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const RptMemBase = require('./base');
+const bindData = {};
+const Ledger = require('../ledger');
+const commonCalcFields = ['yf_excl_tax_tp', 'sf_excl_tax_tp', 'in_excl_tax_tp', 'num_a', 'num_b', 'num_c', 'num_d', 'num_e', 'num_f', 'num_g', 'num_h', 'num_i', 'num_j', 'num_k', 'num_l', 'num_m', 'num_n', 'num_o', 'num_p'];
+const specCalcFeilds = ['sf_percent'];
+const calcFields = [...commonCalcFields, ...specCalcFeilds];
+
+class rptMemChange extends RptMemBase {
+    constructor(ctx) {
+        super(ctx, bindData);
+    }
+
+    async doCheckCostStage(stage_id) {
+        if (this.ctx.costStage) return;
+        this.ctx.costStage = await this.ctx.service.costStage.getStage(stage_id);
+        await this.ctx.service.costStage.doCheckStage(this.ctx.costStage);
+    }
+
+    async doCheckTender(tenderId) {
+        if (this.ctx.tender) return;
+        this.ctx.tender = { id: tenderId };
+        this.ctx.tender.data = await this.ctx.service.tender.getTender(tenderId);
+        this.ctx.tender.info = await this.ctx.service.tenderInfo.getTenderInfo(tenderId);
+    }
+
+    async doBeforeLoadReport(params) {
+        await this.doCheckCostStage(params.cost_stage_id);
+        await this.doCheckTender(this.ctx.costStage.tid);
+    }
+
+    async getCostStageAnalysis() {
+        const bills = this.ctx.costStage.readOnly
+            ? await this.ctx.service.costStageAnalysis.getReadData(this.ctx.costStage)
+            : await this.ctx.service.costStageAnalysis.getEditData(this.ctx.costStage);
+        const setting = {
+            id: 'tree_id',
+            pid: 'tree_pid',
+            order: 'tree_order',
+            level: 'tree_level',
+            isLeaf: 'tree_is_leaf',
+            fullPath: 'tree_full_path',
+            rootId: -1,
+            calcFields: commonCalcFields,
+        };
+        const billsTree = new Ledger.billsTree(this.ctx, setting);
+        billsTree.loadDatas(bills);
+        billsTree.calculateAll();
+        return billsTree.getDefaultDatas();
+    }
+    async getCostStageAnalysisDetail() {
+        const detail = this.ctx.costStage.readOnly
+            ? await this.ctx.service.costStageAnalysisDetail.getReadData(this.ctx.costStage)
+            : await this.ctx.service.costStageAnalysisDetail.getEditData(this.ctx.costStage);
+        return detail;
+    }
+    _analysisCompareData(datas, roles) {
+        const findHis = function(role, history) {
+            let his = null;
+            for (const h of history) {
+                if (h.times < role.times || (h.audit_times === role.audit_times && h.active_order <= role.active_order)) {
+                    his = h;
+                } else {
+                    break;
+                }
+            }
+            return his;
+        };
+        for (const d of datas) {
+            if (!d.tree_is_leaf) continue;
+            for (const prop of calcFields) {
+                d[`his_${prop}`] = [];
+            }
+            d.calc_his.sort((x, y) => { return x.audit_times === y.audit_times ? x.active_order - y.active_order : x.audit_times - y.audit_times; });
+            for (const r of roles) {
+                if (r.latest) {
+                    for (const prop of calcFields) {
+                        d[`r_${prop}_${r.audit_order}`] = d[prop];
+                        d[`his_${prop}`].push(d[prop]);
+                    }
+                } else {
+                    const rHis = findHis(r, d.calc_his);
+                    if (rHis) {
+                        for (const prop of calcFields) {
+                            d[`r_${prop}_${r.audit_order}`] = rHis[prop];
+                        }
+                    }
+                    for (const prop of calcFields) {
+                        d[`his_${prop}`].push(rHis ? rHis[prop] : 0);
+                    }
+                }
+            }
+        }
+    }
+    async getCostStageAnalysisCompare() {
+        const bills = await this.ctx.service.costStageAnalysis.getCompareData(this.ctx.costStage);
+        const roles = await this.ctx.service.costStageAudit.getViewFlow(this.ctx.costStage);
+        this._analysisCompareData(bills, roles);
+        const setting = {
+            id: 'tree_id',
+            pid: 'tree_pid',
+            order: 'tree_order',
+            level: 'tree_level',
+            isLeaf: 'tree_is_leaf',
+            fullPath: 'tree_full_path',
+            rootId: -1,
+            calcFields: [],
+        };
+        for(const r of roles) {
+            for (const f of commonCalcFields) {
+                setting.calcFields.push(`r_${f}_${r.audit_order}`);
+            }
+        }
+        const compareTree = new Ledger.billsTree(this.ctx, setting);
+        compareTree.loadDatas(bills);
+        compareTree.calculateAll();
+        return compareTree.getDefaultDatas();
+    }
+    async getCostStageAnalysisDetailCompare() {
+        const detail = await this.ctx.service.costStageAnalysisDetail.getCompareData(this.ctx.costStage);
+        const roles = await this.ctx.service.costStageAudit.getViewFlow(this.ctx.costStage);
+        this._analysisCompareData(detail, roles);
+        return detail;
+    }
+
+    getCommonData(params, tableName, fields, customDefine, customSelect) {
+        switch (tableName) {
+            case 'mem_project':
+                return this.ctx.service.project.getDataByCondition({ id: this.ctx.session.sessionProject.id });
+            case 'mem_tender':
+                return [this.ctx.tender.data];
+            case 'mem_tender_info':
+                return [this.ctx.tender.info];
+            case 'mem_cost_stage':
+                return [this.ctx.costStage];
+            case 'mem_cost_stage_audit':
+                return this.ctx.service.costStageAudit.getFinalUniqAuditors(this.ctx.costStage);
+            case 'mem_cost_stage_analysis':
+                return this.getCostStageAnalysis();
+            case 'mem_cost_stage_analysis_detail':
+                return this.getCostStageAnalysisDetail();
+            case 'mem_cost_stage_analysis_compare':
+                return this.getCostStageAnalysisCompare();
+            case 'mem_cost_stage_analysis_detail_compare':
+                return this.getCostStageAnalysisDetailCompare();
+            default:
+                return [];
+        }
+    }
+}
+
+module.exports = rptMemChange;

+ 185 - 0
app/lib/rm/cost_book_stage.js

@@ -0,0 +1,185 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const RptMemBase = require('./base');
+const bindData = {};
+const Ledger = require('../ledger');
+const calcFields = ['in_tp', 'in_excl_tax_tp'];
+
+class rptMemChange extends RptMemBase {
+    constructor(ctx) {
+        super(ctx, bindData);
+    }
+
+    async doCheckCostStage(stage_id) {
+        if (this.ctx.costStage) return;
+        this.ctx.costStage = await this.ctx.service.costStage.getStage(stage_id);
+        await this.ctx.service.costStage.doCheckStage(this.ctx.costStage);
+        if (this.ctx.costStage.rela_stage && this.ctx.costStage.rela_stage.sid)
+            this.ctx.costStage.relaStage = await this.ctx.service.costStage.getStage(this.ctx.costStage.rela_stage.sid);
+    }
+
+    async doCheckTender(tenderId) {
+        if (this.ctx.tender) return;
+        this.ctx.tender = { id: tenderId };
+        this.ctx.tender.data = await this.ctx.service.tender.getTender(tenderId);
+        this.ctx.tender.info = await this.ctx.service.tenderInfo.getTenderInfo(tenderId);
+    }
+
+    async doBeforeLoadReport(params) {
+        await this.doCheckCostStage(params.cost_stage_id);
+        await this.doCheckTender(this.ctx.costStage.tid);
+    }
+
+    async getCostStageBook() {
+        const ledger = await this.ctx.service.costStageLedger.getReadData(this.ctx.costStage.relaStage);
+        ledger.forEach(l => { delete l.postil; delete l.memo; });
+        const book = this.ctx.costStage.readOnly
+            ? await this.ctx.service.costStageBook.getReadData(this.ctx.costStage)
+            : await this.ctx.service.costStageBook.getEditData(this.ctx.costStage);
+        this.ctx.helper.assignRelaData(ledger, [
+            { data: book, fields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'in_tp', 'in_excl_tax_tp', 'postil', 'memo'], prefix: '', relaId: 'ledger_id' },
+            { data: book, fields: ['id'], prefix: 'book_', relaId: 'ledger_id' },
+        ]);
+
+        const setting = {
+            id: 'tree_id',
+            pid: 'tree_pid',
+            order: 'tree_order',
+            level: 'tree_level',
+            isLeaf: 'tree_is_leaf',
+            fullPath: 'tree_full_path',
+            rootId: -1,
+            calcFields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'in_tp', 'in_excl_tax_tp'],
+        };
+        const billsTree = new Ledger.billsTree(this.ctx, setting);
+        billsTree.loadDatas(ledger);
+        billsTree.calculateAll();
+        return billsTree.getDefaultDatas();
+    }
+    async getCostStageBookDetail() {
+        const detail = await this.ctx.service.costStageDetail.getReadData(this.ctx.costStage.relaStage);
+        detail.forEach(l => { delete l.postil; delete l.memo; });
+        const bookDetail = this.ctx.costStage.readOnly
+            ? await this.ctx.service.costStageBookDetail.getReadData(this.ctx.costStage)
+            : await this.ctx.service.costStageBookDetail.getEditData(this.ctx.costStage);
+        this.ctx.helper.assignRelaData(detail, [
+            { data: bookDetail, fields: ['in_tp', 'in_excl_tax_tp', 'postil', 'memo'], prefix: '', relaId: 'detail_id' },
+            { data: bookDetail, fields: ['id'], prefix: 'book_', relaId: 'detail_id' },
+        ]);
+        return detail;
+    }
+    _analysisCompareData(datas, roles) {
+        const findHis = function(role, history) {
+            let his = null;
+            for (const h of history) {
+                if (h.times < role.times || (h.audit_times === role.audit_times && h.active_order <= role.active_order)) {
+                    his = h;
+                } else {
+                    break;
+                }
+            }
+            return his;
+        };
+        for (const d of datas) {
+            if (!d.tree_is_leaf) continue;
+            for (const prop of calcFields) {
+                d[`his_${prop}`] = [];
+            }
+            d.calc_his.sort((x, y) => { return x.audit_times === y.audit_times ? x.active_order - y.active_order : x.audit_times - y.audit_times; });
+            for (const r of roles) {
+                if (r.latest) {
+                    for (const prop of calcFields) {
+                        d[`r_${prop}_${r.audit_order}`] = d[prop];
+                        d[`his_${prop}`].push(d[prop]);
+                    }
+                } else {
+                    const rHis = findHis(r, d.calc_his);
+                    if (rHis) {
+                        for (const prop of calcFields) {
+                            d[`r_${prop}_${r.audit_order}`] = rHis[prop];
+                        }
+                    }
+                    for (const prop of calcFields) {
+                        d[`his_${prop}`].push(rHis ? rHis[prop] : 0);
+                    }
+                }
+            }
+        }
+    }
+    async getCostStageBookCompare() {
+        const ledger = await this.ctx.service.costStageLedger.getReadData(this.ctx.costStage.relaStage);
+        ledger.forEach(l => { delete l.postil; delete l.memo; });
+        const book = await this.ctx.service.costStageBook.getCompareData(this.ctx.costStage);
+        this.ctx.helper.assignRelaData(ledger, [
+            { data: book, fields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'calc_his', 'postil', 'memo'], prefix: '', relaId: 'ledger_id' },
+            { data: book, fields: ['id'], prefix: 'book_', relaId: 'ledger_id' },
+        ]);
+        const roles = await this.ctx.service.costStageAudit.getViewFlow(this.ctx.costStage);
+        this._analysisCompareData(ledger, roles);
+        const setting = {
+            id: 'tree_id',
+            pid: 'tree_pid',
+            order: 'tree_order',
+            level: 'tree_level',
+            isLeaf: 'tree_is_leaf',
+            fullPath: 'tree_full_path',
+            rootId: -1,
+            calcFields: ['in_tp', 'in_excl_tax_tp']
+        };
+        for(const r of roles) {
+            for (const f of calcFields) {
+                setting.calcFields.push(`r_${f}_${r.audit_order}`);
+            }
+        }
+        const compareTree = new Ledger.billsTree(this.ctx, setting);
+        compareTree.loadDatas(ledger);
+        compareTree.calculateAll();
+        return compareTree.getDefaultDatas();
+    }
+    async getCostStageBookDetailCompare() {
+        const detail = await this.ctx.service.costStageLedger.getReadData(this.ctx.costStage.relaStage);
+        detail.forEach(l => { delete l.postil; delete l.memo; });
+        const bookDetail = await this.ctx.service.costStageBook.getCompareData(this.ctx.costStage);
+        this.ctx.helper.assignRelaData(detail, [
+            { data: bookDetail, fields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'calc_his', 'postil', 'memo'], prefix: '', relaId: 'detail_id' },
+        ]);
+        const roles = await this.ctx.service.costStageAudit.getViewFlow(this.ctx.costStage);
+        this._analysisCompareData(detail, roles);
+        return detail;
+    }
+
+    getCommonData(params, tableName, fields, customDefine, customSelect) {
+        switch (tableName) {
+            case 'mem_project':
+                return this.ctx.service.project.getDataByCondition({ id: this.ctx.session.sessionProject.id });
+            case 'mem_tender':
+                return [this.ctx.tender.data];
+            case 'mem_tender_info':
+                return [this.ctx.tender.info];
+            case 'mem_cost_stage':
+                return [this.ctx.costStage];
+            case 'mem_cost_stage_audit':
+                return this.ctx.service.costStageAudit.getFinalUniqAuditors(this.ctx.costStage);
+            case 'mem_cost_stage_book':
+                return this.getCostStageBook();
+            case 'mem_cost_stage_book_detail':
+                return this.getCostStageBookDetail();
+            case 'mem_cost_stage_book_compare':
+                return this.getCostStageBookCompare();
+            case 'mem_cost_stage_book_detail_compare':
+                return this.getCostStageBookDetailCompare();
+            default:
+                return [];
+        }
+    }
+}
+
+module.exports = rptMemChange;

+ 159 - 0
app/lib/rm/cost_ledger_stage.js

@@ -0,0 +1,159 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const RptMemBase = require('./base');
+const bindData = {};
+const Ledger = require('../ledger');
+const calcFields = ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp'];
+
+class rptMemChange extends RptMemBase {
+    constructor(ctx) {
+        super(ctx, bindData);
+    }
+
+    async doCheckCostStage(stage_id) {
+        if (this.ctx.costStage) return;
+        this.ctx.costStage = await this.ctx.service.costStage.getStage(stage_id);
+        await this.ctx.service.costStage.doCheckStage(this.ctx.costStage);
+    }
+
+    async doCheckTender(tenderId) {
+        if (this.ctx.tender) return;
+        this.ctx.tender = { id: tenderId };
+        this.ctx.tender.data = await this.ctx.service.tender.getTender(tenderId);
+        this.ctx.tender.info = await this.ctx.service.tenderInfo.getTenderInfo(tenderId);
+    }
+
+    async doBeforeLoadReport(params) {
+        await this.doCheckCostStage(params.cost_stage_id);
+        await this.doCheckTender(this.ctx.costStage.tid);
+    }
+
+    async getCostStageLedger() {
+        const bills = this.ctx.costStage.readOnly
+            ? await this.ctx.service.costStageLedger.getReadData(this.ctx.costStage)
+            : await this.ctx.service.costStageLedger.getEditData(this.ctx.costStage);
+        const setting = {
+            id: 'tree_id',
+            pid: 'tree_pid',
+            order: 'tree_order',
+            level: 'tree_level',
+            isLeaf: 'tree_is_leaf',
+            fullPath: 'tree_full_path',
+            rootId: -1,
+            calcFields: ['pre_pay_tp', 'pre_cur_tp', 'pre_yf_tp', 'pre_sf_tp', 'pre_yf_excl_tax_tp', 'pre_sf_excl_tax_tp', 'pay_tp', 'cur_tp', 'yf_tp', 'sf_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp'],
+        };
+        const billsTree = new Ledger.billsTree(this.ctx, setting);
+        billsTree.loadDatas(bills);
+        billsTree.calculateAll();
+        return billsTree.getDefaultDatas();
+    }
+    async getCostStageDetail() {
+        const detail = this.ctx.costStage.readOnly
+            ? await this.ctx.service.costStageDetail.getReadData(this.ctx.costStage)
+            : await this.ctx.service.costStageDetail.getEditData(this.ctx.costStage);
+        return detail;
+    }
+    _analysisCompareData(datas, roles) {
+        const findHis = function(role, history) {
+            let his = null;
+            for (const h of history) {
+                if (h.times < role.times || (h.audit_times === role.audit_times && h.active_order <= role.active_order)) {
+                    his = h;
+                } else {
+                    break;
+                }
+            }
+            return his;
+        };
+        for (const d of datas) {
+            if (!d.tree_is_leaf) continue;
+            for (const prop of calcFields) {
+                d[`his_${prop}`] = [];
+            }
+            d.calc_his.sort((x, y) => { return x.audit_times === y.audit_times ? x.active_order - y.active_order : x.audit_times - y.audit_times; });
+            for (const r of roles) {
+                if (r.latest) {
+                    for (const prop of calcFields) {
+                        d[`r_${prop}_${r.audit_order}`] = d[prop];
+                        d[`his_${prop}`].push(d[prop]);
+                    }
+                } else {
+                    const rHis = findHis(r, d.calc_his);
+                    if (rHis) {
+                        for (const prop of calcFields) {
+                            d[`r_${prop}_${r.audit_order}`] = rHis[prop];
+                        }
+                    }
+                    for (const prop of calcFields) {
+                        d[`his_${prop}`].push(rHis ? rHis[prop] : 0);
+                    }
+                }
+            }
+        }
+    }
+    async getCostStageLedgerCompare() {
+        const bills = await this.ctx.service.costStageLedger.getCompareData(this.ctx.costStage);
+        const roles = await this.ctx.service.costStageAudit.getViewFlow(this.ctx.costStage);
+        this._analysisCompareData(bills, roles);
+        const setting = {
+            id: 'tree_id',
+            pid: 'tree_pid',
+            order: 'tree_order',
+            level: 'tree_level',
+            isLeaf: 'tree_is_leaf',
+            fullPath: 'tree_full_path',
+            rootId: -1,
+            calcFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp', 'pre_yf_excl_tax_tp', 'pre_sf_excl_tax_tp']
+        };
+        for(const r of roles) {
+            for (const f of calcFields) {
+                setting.calcFields.push(`r_${f}_${r.audit_order}`);
+            }
+        }
+        const compareTree = new Ledger.billsTree(this.ctx, setting);
+        compareTree.loadDatas(bills);
+        compareTree.calculateAll();
+        return compareTree.getDefaultDatas();
+    }
+    async getCostStageDetailCompare() {
+        const detail = await this.ctx.service.costStageDetail.getCompareData(this.ctx.costStage);
+        const roles = await this.ctx.service.costStageAudit.getViewFlow(this.ctx.costStage);
+        this._analysisCompareData(detail, roles);
+        return detail;
+    }
+
+    getCommonData(params, tableName, fields, customDefine, customSelect) {
+        switch (tableName) {
+            case 'mem_project':
+                return this.ctx.service.project.getDataByCondition({ id: this.ctx.session.sessionProject.id });
+            case 'mem_tender':
+                return [this.ctx.tender.data];
+            case 'mem_tender_info':
+                return [this.ctx.tender.info];
+            case 'mem_cost_stage':
+                return [this.ctx.costStage];
+            case 'mem_cost_stage_audit':
+                return this.ctx.service.costStageAudit.getFinalUniqAuditors(this.ctx.costStage);
+            case 'mem_cost_stage_ledger':
+                return this.getCostStageLedger();
+            case 'mem_cost_stage_detail':
+                return this.getCostStageDetail();
+            case 'mem_cost_stage_ledger_compare':
+                return this.getCostStageLedgerCompare();
+            case 'mem_cost_stage_detail_compare':
+                return this.getCostStageDetailCompare();
+            default:
+                return [];
+        }
+    }
+}
+
+module.exports = rptMemChange;

+ 4 - 0
app/lib/rm/tender.js

@@ -187,6 +187,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return params.budget_id ? budgetSource.budgetYu(params.budget_id) : budgetSource.tenderYu(params.tender_id);
             case 'mem_budget_zb':
                 return params.budget_id ? budgetSource.budgetZb(params.budget_id) : budgetSource.tenderZb(params.tender_id);
+            case 'mem_budget_ctrl':
+                return params.budget_id ? budgetSource.budgetCtrl(params.budget_id) : budgetSource.tenderCtrl(params.tender_id);
             case 'mem_budget_final':
                 return params.budget_id ? budgetSource.budgetFinal(params.budget_id) : budgetSource.tenderFinal(params.tender_id);
             case 'mem_budget_gu_filter':
@@ -197,6 +199,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return params.budget_id ? budgetSource.budgetYu(params.budget_id, true) : budgetSource.tenderYu(params.tender_id, true);
             case 'mem_budget_zb_filter':
                 return params.budget_id ? budgetSource.budgetZb(params.budget_id, true) : budgetSource.tenderZb(params.tender_id, true);
+            case 'mem_budget_ctrl_filter':
+                return params.budget_id ? budgetSource.budgetCtrl(params.budget_id, true) : budgetSource.tenderCtrl(params.tender_id, true);
             case 'mem_budget_final_filter':
                 return params.budget_id ? budgetSource.budgetFinal(params.budget_id, true) : budgetSource.tenderFinal(params.tender_id, true);
             case 'mem_pm_deal_pay':

+ 12 - 0
app/lib/rm/tender_budget.js

@@ -50,6 +50,13 @@ class reportMemoryBudget {
         tree.calculateAll();
         return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
     }
+    async budgetCtrl(bid, showLevel = false) {
+        const ctrl = await this.ctx.service.budgetCtrl.getAllDataByCondition({ where: { bid } });
+        const tree = new ledger.billsTree(this.ctx, { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] });
+        tree.loadDatas(ctrl);
+        tree.calculateAll();
+        return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
+    }
     async budgetFinal(bid, showLevel = false) {
         const budget = this.ctx.budget && this.ctx.budget.id === bid
             ? this.ctx.budget
@@ -100,6 +107,11 @@ class reportMemoryBudget {
         return this.budget ? await this.budgetZb(this.budget.id, filter) : [];
     }
 
+    async tenderCtrl(tid, filter = false) {
+        await this._getTenderBudget(tid);
+        return this.budget ? await this.budgetCtrl(this.budget.id, filter) : [];
+    }
+
     async tenderFinal(tid, filter = false) {
         await this._getTenderBudget(tid);
         console.log(this.budget);

+ 19 - 0
app/public/js/budget_compare.js

@@ -9,6 +9,7 @@ function customColDisplay () {
         { title: '设计概算', fields: ['gai_dgn_qty', 'gai_dgn_price', 'gai_tp'], visible: true },
         { title: '施工图预算', fields: ['yu_dgn_qty', 'yu_dgn_price', 'yu_tp'], visible: true },
         { title: '招标预算', fields: ['zb_dgn_qty', 'zb_dgn_price', 'zb_tp'], visible: true },
+        { title: '控制目标', fields: ['ctrl_dgn_qty', 'ctrl_dgn_price', 'ctrl_tp'], visible: false },
         { title: '台账', fields: ['dgn_qty', 'dgn_price', 'total_price'], visible: true },
         { title: '预估决算', fields: ['final_dgn_qty', 'final_dgn_price', 'final_tp'], visible: true },
     ];
@@ -55,6 +56,7 @@ $(document).ready(() => {
         { key: 'gai', name: '设计概算', dgn1: 'gai_dgn_qty1', dgn2: 'gai_dgn_qty2', tp: 'gai_tp'},
         { key: 'yu', name: '施工图预算', dgn1: 'yu_dgn_qty1', dgn2: 'yu_dgn_qty2', tp: 'yu_tp'},
         { key: 'zb', name: '招标预算', dgn1: 'zb_dgn_qty1', dgn2: 'zb_dgn_qty2', tp: 'zb_tp'},
+        { key: 'ctrl', name: '控制目标', dgn1: 'ctrl_dgn_qty1', dgn2: 'ctrl_dgn_qty2', tp: 'ctrl_tp'},
         { key: 'tz', name: '台账', dgn1: 'dgn_qty1', dgn2: 'dgn_qty2', tp: 'total_price'},
         { key: 'final', name: '预估决算', dgn1: 'final_dgn_qty1', dgn2: 'final_dgn_qty2', tp: 'final_tp'},
     ];
@@ -75,6 +77,9 @@ $(document).ready(() => {
             {title: '招标预算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'zb_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
             {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'zb_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'zb_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '控制目标|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'ctrl_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
+            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'ctrl_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'ctrl_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
             {title: '台账|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'dgn_qty', hAlign: 2, width: 80, bc_type: 'number', visible: false},
             {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number', visible: false},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number', visible: false},
@@ -213,6 +218,16 @@ $(document).ready(() => {
             });
             const setting = { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] };
 
+            const ctrlTree = createNewPathTree('ledger', setting);
+            ctrlTree.loadDatas(result.ctrl);
+            treeCalc.calculateAll(ctrlTree);
+            compareTree.loadTree(ctrlTree, function (cur, source) {
+                cur.base = true;
+                cur.ctrl_dgn_qty1 = ZhCalc.add(cur.ctrl_dgn_qty1, source.dgn_qty1);
+                cur.ctrl_dgn_qty2 = ZhCalc.add(cur.ctrl_dgn_qty2, source.dgn_qty2);
+                cur.ctrl_tp = ZhCalc.add(cur.ctrl_tp, source.total_price);
+            });
+
             const zbTree = createNewPathTree('ledger', setting);
             zbTree.loadDatas(result.zb);
             treeCalc.calculateAll(zbTree);
@@ -270,6 +285,10 @@ $(document).ready(() => {
                 node.zb_dgn_qty = node.zb_dgn_qty1
                     ? (node.zb_dgn_qty2 ? node.zb_dgn_qty1 + '/' + node.zb_dgn_qty2 : node.zb_dgn_qty1)
                     : (node.zb_dgn_qty2 ? '/' + node.zb_dgn_qty2 : '');
+                node.ctrl_dgn_price = ZhCalc.div(node.ctrl_tp, node.ctrl_dgn_qty1, 2);
+                node.ctrl_dgn_qty = node.ctrl_dgn_qty1
+                    ? (node.ctrl_dgn_qty2 ? node.ctrl_dgn_qty1 + '/' + node.ctrl_dgn_qty2 : node.ctrl_dgn_qty1)
+                    : (node.ctrl_dgn_qty2 ? '/' + node.ctrl_dgn_qty2 : '');
             });
             compareTree.resortChildrenByCustom(function (x, y) {
                 const iCode = compareCode(x.code, y.code);

+ 5 - 2
app/public/js/budget_info.js

@@ -298,7 +298,7 @@ $(document).ready(() => {
             const huishouInfo = _.find(tree.children, function (item) {
                 return item.name.indexOf('回收金额') !== -1 && (item.gu_tp || item.gai_tp || (item.final_tp !== undefined && item.final_tp) || item.yu_tp || (item.total_price !== undefined && item.total_price))
             });
-            let total_rate = 0, total_gai_tp = 0, total_gu_tp = 0, total_yu_tp = 0, total_zb_tp = 0, total_price = 0, total_final_tp = 0;
+            let total_rate = 0, total_gai_tp = 0, total_gu_tp = 0, total_yu_tp = 0, total_zb_tp = 0, total_ctrl_tp = 0, total_price = 0, total_final_tp = 0;
             if (level1List.length > 0) {
                 if (huishouInfo) {
                     total_gai_tp = ZhCalc.sub(ZhCalc.sum(_.map(level1List, 'gai_tp')), huishouInfo.gai_tp);
@@ -306,6 +306,7 @@ $(document).ready(() => {
                     total_gu_tp = ZhCalc.sub(ZhCalc.sum(_.map(level1List, 'gu_tp')), huishouInfo.gu_tp);
                     total_yu_tp = ZhCalc.sub(ZhCalc.sum(_.map(level1List, 'yu_tp')), huishouInfo.yu_tp);
                     total_zb_tp = ZhCalc.sub(ZhCalc.sum(_.map(level1List, 'zb_tp')), huishouInfo.zb_tp);
+                    total_ctrl_tp = ZhCalc.sub(ZhCalc.sum(_.map(level1List, 'ctrl_tp')), huishouInfo.ctrl_tp);
                     total_price = ZhCalc.sub(ZhCalc.sum(_.map(level1List, 'total_price')), huishouInfo.total_price);
                     total_rate = ZhCalc.div(ZhCalc.sub(total_final_tp, total_gai_tp), total_gai_tp);
                     $('#total_gai_tp').text(total_gai_tp ? ZhCalc.div(total_gai_tp, 10000) : 0);
@@ -313,6 +314,7 @@ $(document).ready(() => {
                     $('#total_gu_tp').text(total_gu_tp ? ZhCalc.div(total_gu_tp, 10000) : 0);
                     $('#total_yu_tp').text(total_yu_tp ? ZhCalc.div(total_yu_tp, 10000) : 0);
                     $('#total_zb_tp').text(total_zb_tp ? ZhCalc.div(total_zb_tp, 10000) : 0);
+                    $('#total_ctrl_tp').text(total_ctrl_tp ? ZhCalc.div(total_ctrl_tp, 10000) : 0);
                     // level1List.push(huishouInfo);
                 } else {
                     total_gai_tp = ZhCalc.sum(_.map(level1List, 'gai_tp'));
@@ -320,6 +322,7 @@ $(document).ready(() => {
                     total_gu_tp = ZhCalc.sum(_.map(level1List, 'gu_tp'));
                     total_yu_tp = ZhCalc.sum(_.map(level1List, 'yu_tp'));
                     total_zb_tp = ZhCalc.sum(_.map(level1List, 'zb_tp'));
+                    total_ctrl_tp = ZhCalc.sum(_.map(level1List, 'ctrl_tp'));
                     total_price = ZhCalc.sum(_.map(level1List, 'total_price'));
                     total_rate = ZhCalc.div(ZhCalc.sub(total_final_tp, total_gai_tp), total_gai_tp);
                     $('#total_gai_tp').text(total_gai_tp ? ZhCalc.div(total_gai_tp, 10000) : 0);
@@ -327,6 +330,7 @@ $(document).ready(() => {
                     $('#total_gu_tp').text(total_gu_tp ? ZhCalc.div(total_gu_tp, 10000) : 0);
                     $('#total_yu_tp').text(total_yu_tp ? ZhCalc.div(total_yu_tp, 10000) : 0);
                     $('#total_zb_tp').text(total_zb_tp ? ZhCalc.div(total_zb_tp, 10000) : 0);
+                    $('#total_ctrl_tp').text(total_ctrl_tp ? ZhCalc.div(total_ctrl_tp, 10000) : 0);
                 }
             }
             $('#total_rate').text((total_rate ? ZhCalc.round(ZhCalc.mul(total_rate,100), 2) : 0) + '%');
@@ -337,7 +341,6 @@ $(document).ready(() => {
             } else if (total_rate === 0) {
                 $('#total_rate').parents('.canyu-band').removeClass('text-success').removeClass('text-danger');
             }
-            console.log(level1List);
             option.series[0].data = [
                 ZhCalc.div(total_gu_tp, 10000),
                 ZhCalc.div(total_gai_tp, 10000),

+ 25 - 0
app/public/js/cost_tmpl.js

@@ -139,6 +139,9 @@ $(document).ready(() => {
                         comboItems: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'],
                     },
                     { title: '小数位数', colSpan: '1', rowSpan: '1', field: 'decimal', hAlign: 1, width: 60, type: 'Number' },
+                    { title: '期数据', colSpan: '1', rowSpan: '1', field: 'sum', hAlign: 1, width: 60, readOnly: true, cellType: 'signalCheckbox', show: function(data) {
+                        const typeInfo = validColInfo.find(x => { return x.key === data.type; }); return typeInfo.valid.indexOf('sum') >= 0;
+                        } },
                     { title: '计算公式', colSpan: '1', rowSpan: '1', field: 'expr', hAlign: 0, width: 250 },
                 ],
                 emptyRows: 0,
@@ -202,6 +205,23 @@ $(document).ready(() => {
                 }
                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
             });
+            this.spread.bind(spreadNS.Events.ButtonClicked, function(e, info) {
+                if (!info.sheet.zh_setting) return;
+
+                const col = info.sheet.zh_setting.cols[info.col];
+                if (col.field !== 'sum') return;
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const typeInfo = validColInfo.find(x => { return x.key === select.type; });
+                if (typeInfo.valid.indexOf('sum') <= 0) return;
+
+                select.sum = !select.sum;
+                if (select.sum) {
+                    info.sheet.zh_data.forEach(x => {
+                        if (x.field !== select.field) delete x.sum;
+                    })
+                }
+                SpreadJsObj.reloadColData(info.sheet, info.col);
+            });
             SpreadJsObj.addDeleteBind(this.spread, function(sheet){
                 if (!sheet.zh_setting) return;
 
@@ -441,6 +461,11 @@ $(document).ready(() => {
                     }
                 }
             }
+            const sumCol = this.colSetData.find(x => { return x.sum; });
+            if (!sumCol) {
+                toastr.error('未指定期数据显示的数据列');
+                return null;
+            }
             return this.colSetData;
         }
         export() {

+ 77 - 6
app/public/js/pos_calc_tmpl.js

@@ -12,7 +12,8 @@ $(document).ready(() => {
             this.tree = createNewPathTree('base', this.treeSetting);
             this.spreadSetting = {
                 cols: [
-                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 330, formatter: '@', cellType: 'tree'},
+                    {title: '选择', colSpan: '1', rowSpan: '1', field: 'selected', hAlign: 1, width: 30, readOnly: true, cellType: 'checkbox'},
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 300, formatter: '@', cellType: 'tree'},
                 ],
                 emptyRows: 0,
                 headRows: 1,
@@ -29,8 +30,8 @@ $(document).ready(() => {
         initSpread() {
             SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
             this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            this.spread.bind(spreadNS.Events.ButtonClicked, this.buttonClicked);
             if (readOnly) return;
-
             this.spread.bind(spreadNS.Events.EditEnded, this.editEnded);
             this.spread.bind(spreadNS.Events.ClipboardPasting, this.clipboardPasting);
             SpreadJsObj.addDeleteBind(this.spread, this.deletePress);
@@ -217,9 +218,24 @@ $(document).ready(() => {
             }
         }
         // 事件
+        buttonClicked(e, info) {
+            const col = info.sheet.zh_setting.cols[info.col];
+            if (col.field !== 'selected') return;
+            const select = info.sheet.zh_tree.nodes[info.row];
+            if (!select) return;
+            select.selected = !select.selected;
+            if (select.selected) {
+                for (const node of info.sheet.zh_tree.nodes) {
+                    if (node.id !== select.id) node.selected = false;
+                }
+            }
+            SpreadJsObj.reloadColData(info.sheet, info.col);
+        }
         selectionChanged(e, info) {
             if (info.newSelections) {
-                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                if (info.newSelections[0].col === 0) {
+                    info.sheet.setSelection(info.oldSelections[0].row, info.oldSelections[0].col, info.oldSelections[0].rowCount, info.oldSelections[0].colCount);
+                } else if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
                     folderObj.loadRelaData();
                 }
             }
@@ -335,6 +351,7 @@ $(document).ready(() => {
                     },
                 ],
             };
+            this._copyTemplateId = '';
 
             this.initSpread();
             this.initOtherEvent();
@@ -342,16 +359,35 @@ $(document).ready(() => {
         initSpread() {
             SpreadJsObj.initSheet(this.sheet, this.templateSpreadSetting);
             this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
-            if (readOnly) return;
+            this.spread.bind(spreadNS.Events.EditStarting, this.editStarting);
             this.spread.bind(spreadNS.Events.EditEnded, this.editEnded);
             this.spread.bind(spreadNS.Events.ClipboardPasting, this.clipboardPasting);
             SpreadJsObj.addDeleteBind(this.spread, this.deletePress);
         }
         initOtherEvent() {
+            const self = this;
             // 增删上下移升降级
             $('a[name="template-opr"]').click(function () {
                 templateObj.baseOpr(this.getAttribute('type'));
             });
+            $('#copy-template').click(function() {
+                const template = SpreadJsObj.getSelectObject(self.sheet);
+                if (!template) return;
+                self._copyTemplateId = template.id;
+                toastr.success('复制成功');
+            });
+            $('#paste-template').click(function() {
+                if (!self._copyTemplateId) {
+                    toastr.warning('请先复制模板');
+                    return;
+                }
+                const node = SpreadJsObj.getSelectObject(folderObj.sheet);
+                if (!node) {
+                    toastr.warning('请先选择您要粘贴模板的分类');
+                    return;
+                }
+                self.baseOpr('copy');
+            })
         }
         loadData(datas) {
             this.data.loadDatas(datas);
@@ -389,6 +425,21 @@ $(document).ready(() => {
                     templateObj.data.updateDatas({ add: result.add });
                     templateObj.refreshSheet();
                 });
+            } else if (type === 'copy') {
+                postData('save', {copy: { id: self._copyTemplateId, folder_id: self.folder.id }, type: 'posCalc'}, function(result) {
+                    templateObj.data.updateDatas({ add: result.copy });
+                    templateObj.refreshSheet();
+                });
+            } else if (type === 'move') {
+                const target = folderObj.tree.nodes.find(x => { return x.selected; });
+                if (!target) {
+                    toastr.warning('请在模板分类中选择要移动至的分类');
+                    return;
+                }
+                postData('save', {move: [curTemplate.id], folder_id: target.id, type: 'posCalc'}, function(result) {
+                    templateObj.data.updateDatas(result);
+                    templateObj.refreshSheet();
+                });
             }
         }
         moveTemplate(templateIds, target, fun) {
@@ -411,6 +462,7 @@ $(document).ready(() => {
             if (!curTemplate) {
                 $('#detail-user-info').html('');
                 $('#detail-ctrl').hide();
+                detailObj.loadDetail(curTemplate);
             } else {
                 $('#detail-ctrl').hide();
                 if (!curTemplate.col_set) await this.loadTemplateDetail(curTemplate);
@@ -430,6 +482,10 @@ $(document).ready(() => {
                 }
             }
         }
+        editStarting(e, info) {
+            const select = SpreadJsObj.getSelectObject(info.sheet);
+            info.cancel = !select || select.create_user_id !== userID;
+        }
         editEnded(e, info) {
             const setting = info.sheet.zh_setting;
             if (!setting) return;
@@ -761,12 +817,12 @@ $(document).ready(() => {
             SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
         }
         reset() {
-            this.colSetData = JSON.parse(JSON.stringify(this.template.col_set));
+            this.colSetData = this.template ? JSON.parse(JSON.stringify(this.template.col_set)) : [];
             SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
         }
         loadDetail(template) {
             this.template = template;
-            this.readOnly = this.template.used_count > 0;
+            this.readOnly = this.template ? this.template.used_count > 0 : true;
             if (!this.firstShowDone) {
                 this.spread.refresh();
                 this.firstShowDone = true;
@@ -798,6 +854,21 @@ $(document).ready(() => {
             }
             return this.colSetData;
         }
+        export() {
+            window.open(`/template/ctd?tid=${this.template.id}`);
+        }
+        import() {
+            const self = this;
+            BaseImportFile.show({
+                validList: ['.ctd'],
+                url: `/template/ctd/load?tid=${this.template.id}`,
+                afterImport: function (result) {
+                    self.template.col_set = result.update.col_set;
+                    self.template.multi_header = result.update.multi_header;
+                    self.reset();
+                }
+            });
+        }
     }
     const detailObj = new TemplateDetailObj();
 

+ 1 - 0
app/router.js

@@ -409,6 +409,7 @@ module.exports = app => {
     app.post('/sp/:id/budget/compare/load', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.compareLoad');
     app.post('/sp/:id/budget/compare/final', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.compareFinal');
     app.get('/sp/:id/budget/:btype', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.detail');
+    app.get('/sp/:id/budget/:btype/init', sessionAuth, subProjectCheck, budgetCheck, projectManagerCheck, 'budgetController.init');
     app.post('/sp/:id/budget/:btype/load', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.detailLoad');
     app.post('/sp/:id/budget/:btype/update', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.detailUpdate');
     app.post('/sp/:id/budget/:btype/upload-excel/:ueType', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.detailUploadExcel');

+ 1 - 0
app/service/budget.js

@@ -102,6 +102,7 @@ module.exports = app => {
             await this.ctx.service.budgetGai.initByTemplate(transaction, operate.insertId, budgetStd.gai_template_id);
             await this.ctx.service.budgetYu.initByTemplate(transaction, operate.insertId, budgetStd.yu_template_id);
             await this.ctx.service.budgetZb.initByTemplate(transaction, operate.insertId, budgetStd.zb_template_id);
+            await this.ctx.service.budgetCtrl.initByTemplate(transaction, operate.insertId, budgetStd.ctrl_template_id);
             return operate.insertId;
         }
 

+ 28 - 0
app/service/budget_ctrl.js

@@ -0,0 +1,28 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+module.exports = app => {
+    class BudgetCtrl extends app.BaseBudgetService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @param {String} tableName - 表名
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, { keyPre: 'budget_ctrl_maxLid:' });
+            this.tableName = 'budget_ctrl';
+        }
+    }
+
+    return BudgetCtrl;
+};

+ 57 - 5
app/service/calc_tmpl.js

@@ -80,14 +80,14 @@ const Cost = (function(){
         { key: 'code', name: '编号', fields: ['code'], valid: ['title', 'width'], def: { title: '编号', width: 80, type: 'code'} },
         { key: 'name', name: '名称', fields: ['name'], valid: ['title', 'width'], def: { title: '名称', width: 80, type: 'name'} },
         { key: 'party_b', name: '乙方', fields: ['party_b'], valid: ['title', 'width'], def: { title: '乙方', width: 80, type: 'party_b'} },
-        { key: 'yf_excl_tax_tp', name: '应付(不含税)', fields: ['yf_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal'], def: { title: '已结(不含税)', width: 80, type: 'yf_excl_tax_tp', decimal: 6} },
-        { key: 'in_excl_tax_tp', name: '入账(不含税)', fields: ['in_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal'], def: { title: '入账(不含税)', width: 80, type: 'in_excl_tax_tp', decimal: 6} },
-        { key: 'sf_excl_tax_tp', name: '实付(不含税)', fields: ['sf_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal'], def: { title: '实付(不含税)', width: 80, type: 'sf_excl_tax_tp', decimal: 6} },
+        { key: 'yf_excl_tax_tp', name: '应付(不含税)', fields: ['yf_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal', 'sum'], def: { title: '已结(不含税)', width: 80, type: 'yf_excl_tax_tp', decimal: 6} },
+        { key: 'in_excl_tax_tp', name: '入账(不含税)', fields: ['in_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal', 'sum'], def: { title: '入账(不含税)', width: 80, type: 'in_excl_tax_tp', decimal: 6} },
+        { key: 'sf_excl_tax_tp', name: '实付(不含税)', fields: ['sf_excl_tax_tp'], valid: ['title', 'width', 'calc_code', 'decimal', 'sum'], def: { title: '实付(不含税)', width: 80, type: 'sf_excl_tax_tp', decimal: 6} },
         { key: 'str', name: '文本', fields: ['str1', 'str2', 'str3', 'str4'], valid: ['title', 'width'], def: { title: '文本', width: 80, type: 'str'} },
         {
             key: 'num', name: '数值/计算',
             fields: ['num_a', 'num_b', 'num_c', 'num_d', 'num_e', 'num_f', 'num_g', 'num_h', 'num_i', 'num_j', 'num_k', 'num_l', 'num_m', 'num_n', 'num_o', 'num_p'], // 'num_q', 'num_r', 'num_s', 'num_t', 'num_u', 'num_v', 'num_w', 'num_x', 'num_y', 'num_z'],
-            valid: ['title', 'width', 'calc_code', 'decimal', 'unit', 'expr'],
+            valid: ['title', 'width', 'calc_code', 'decimal', 'unit', 'expr', 'sum'],
             def: { title: '数值/计算', width: 80, decimal: 6, type: 'num', unit: ''}
         },
         { key: 'postil', name: '本期批注', fields: ['postil'], valid: ['title', 'width'], def: { title: '本期批注', width: 120, type: 'postil'} },
@@ -170,6 +170,23 @@ module.exports = app => {
             await this.analysisTemplate(result);
             return result;
         }
+        async getProjectCalcTmplSet () {
+            if (this.projectSet) return;
+            const defaultSet = { posCalc: { validCount: 50 }, cost: { validCount: 10 } };
+            const project = await this.ctx.service.project.getDataById(this.ctx.session.sessionProject.id);
+            if (!project.calc_tmpl_set) {
+                this.projectSet = defaultSet;
+            } else {
+                this.projectSet = JSON.parse(project.calc_tmpl_set);
+                for (const prop in defaultSet) {
+                    if (this.projectSet[prop]) {
+                        this.projectSet[prop] = this.ctx.helper._.assign(defaultSet[prop], this.projectSet[prop]);
+                    } else {
+                        this.projectSet[prop] = defaultSet[prop];
+                    }
+                }
+            }
+        }
 
         //-------------------------- 以下方法不可随意修改 --------------------------
         // 根据列设置计算spreadjs配置相关,方法命名须严格按照【方法_类型】命名
@@ -360,8 +377,9 @@ module.exports = app => {
 
         async _addTemplate(name, type, folder_id) {
             if (ValidTemplateType.indexOf(type) < 0) throw '新增的模板类型非法';
+            await this.getProjectCalcTmplSet();
             const count = await this.count({pid: this.ctx.session.sessionProject.id, type});
-            if (count > this.TemplateRela[type].ValidCount) throw '已达模板使用上限';
+            if (count >= this.projectSet[type].validCount) throw '已达模板使用上限';
 
             const relaConst = this.TemplateRela[type];
             const insertData = {
@@ -497,12 +515,46 @@ module.exports = app => {
             await this.db.updateRows(this.tableName, updateData);
             return updateData;
         }
+        async _copyTemplate(id, folder_id) {
+            const org = await this.getDataById(id);
+            if (!org || org.pid !== this.ctx.session.sessionProject.id) throw '复制的模板不存在,请刷新后重试';
+
+            await this.getProjectCalcTmplSet();
+            const count = await this.count({pid: this.ctx.session.sessionProject.id, type});
+            if (count >= this.projectSet[org.type].validCount) throw '已达模板使用上限';
+
+            const insertData = {
+                id: this.uuid.v4(),
+                pid: this.ctx.session.sessionProject.id,
+                folder_id: folder_id,
+                name: org.name,
+                type: org.type,
+                create_user_id: this.ctx.session.sessionUser.accountId,
+                update_user_id: this.ctx.session.sessionUser.accountId,
+                col_set: org.col_set,
+                spread_cache: org.spread_cache,
+                sub_spread_cache: org.sub_spread_cache,
+                field_cache: org.field_cache,
+                decimal: org.decimal,
+                calc_expr: org.calc_expr,
+                calc_order: org.calc_order,
+                spec_set: org.spec_set,
+                spec_rela: org.spec_rela,
+                multi_header: org.multi_header,
+            };
+            await this.db.insert(this.tableName, insertData);
+
+            const result = await this.getTemplate(insertData.id);
+            result.user_name = this.ctx.session.sessionUser.name;
+            return result;
+        }
         async saveTemplate(data) {
             const result = {};
             if (data.add) result.add = await this._addTemplate(data.add.name, data.type, data.add.folder_id);
             if (data.del) result.del = await this._delTemplate(data.del);
             if (data.update) result.update = await this._saveTemplate(data.update.id, data.update);
             if (data.move) result.move = await this._moveTemplate(data.move, data.folder_id);
+            if (data.copy) result.copy = await this._copyTemplate(data.copy.id, data.copy.folder_id);
             return result;
         }
         async reCalcTemplate(id, force) {

+ 1 - 1
app/service/cost_stage.js

@@ -118,7 +118,7 @@ module.exports = app => {
                 await this.ctx.service.costStageAudit.copyPreAuditors(transaction, preStage, data);
                 await transaction.commit();
             } catch(err) {
-                console.log(err);
+                this.ctx.log(err);
                 await transaction.rollback();
                 throw err;
             }

+ 20 - 16
app/service/cost_stage_analysis.js

@@ -17,7 +17,7 @@ const costFields = {
     hisFields: ['calc_his'],
     taxFields: ['tax'],
     treeFields: ['tree_id', 'tree_pid', 'tree_level', 'tree_order', 'tree_full_path', 'tree_is_leaf'],
-    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'node_type'],
+    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'node_type', 'calc_type'],
 };
 costFields.sumFields = [...costFields.calcFields, ...costFields.selfCalcFields];
 costFields.preCopyFields = [...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
@@ -211,6 +211,7 @@ module.exports = app => {
             const userId = this.ctx.session.sessionUser.accountId;
             const insertData = [], insertDetailData = [];
             let maxId = 0;
+            if (!stage.calcTemplate) stage.calcTemplate = await this.service.calcTmpl.getTemplate(stage.calc_template);
 
             const preBills = await this.getReadData(preStage);
             const preLedger = new Ledger.baseTree(this.ctx, {
@@ -230,6 +231,7 @@ module.exports = app => {
                 this._getDefaultData(idata, stage);
                 idata.cost_id = pl.cost_id;
                 idata.node_type = pl.node_type;
+                idata.calc_type = pl.calc_type;
                 for (const prop of costFields.preCopyFields) {
                     idata[prop] = pl[prop];
                 }
@@ -244,7 +246,7 @@ module.exports = app => {
             for (const [i, node] of costStageLedger.nodes.entries()) {
                 const parent = costStageLedger.getParent(node);
                 const newParent = parent ? insertData.find(x => { return x.tree_id === parent.new_tree_id; }) : costNode;
-                const ln = {};
+                const ln = { calc_type: 2 };
                 this._getDefaultData(ln, stage);
                 ln.node_type = 0;
                 ln.cost_id = node.cost_id;
@@ -273,6 +275,7 @@ module.exports = app => {
                     for (const prop of costFields.selfCalcFields) {
                         ln[prop] = preSource ? preSource[prop] : 0;
                     }
+                    this.ctx.service.calcTmpl.calcByTemplate(ln, ln, ln, stage.calcTemplate.calc_expr);
                 } else {
                     for (const [i, d] of node.detail.entries()) {
                         const ld = {
@@ -290,6 +293,9 @@ module.exports = app => {
                         }
                         for (const prop of costFields.selfCalcFields) {
                             ld[prop] = preDetailSource ? preDetailSource[prop] || 0 : 0;
+                        }
+                        this.ctx.service.calcTmpl.calcByTemplate(ld, ld, ld, stage.calcTemplate.calc_expr);
+                        for (const prop of costFields.selfCalcFields) {
                             ln[prop] = this.ctx.helper.add(ln[prop], ld[prop]);
                         }
                         insertDetailData.push(ld);
@@ -675,20 +681,18 @@ module.exports = app => {
         }
 
         async getSum(stage) {
-            const field = 'num_h';
-            const topNode = await this.db.query(`SELECT * FROM ${this.tableName} where stage_id = ? and tree_level = 1`, [stage.id]);
-            const result = {};
-            for (const tn of topNode) {
-                if (topNode.node_type === 2) {
-                    result.out_tp = tn[field];
-                } else if (topNode.node_type === 3) {
-                    result.profit = tn[field];
-                } else if (topNode.node_type === 4) {
-                    result.profit_percent = tn[field];
-                } else {
-                    result.in_tp = tn[field];
-                }
-            }
+            const sumCol = stage.calcTemplate.col_set.find(x => { return x.sum; });
+            const field = sumCol.field;
+            const inTp = await this.db.queryOne(`SELECT SUM(${field}) as tp FROM ${this.tableName} where stage_id = ? AND calc_type = 1`, [stage.id]);
+            const outTp = await this.db.queryOne(`SELECT SUM(${field}) as tp FROM ${this.tableName} where stage_id = ? AND calc_type = 2`, [stage.id]);
+            const profit = await this.db.queryOne(`SELECT ${field} as tp FROM ${this.tableName} where stage_id = ? AND node_type = 3`, [stage.id]);
+            const profitPer = await this.db.queryOne(`SELECT ${field} as tp FROM ${this.tableName} where stage_id = ? AND node_type = 4`, [stage.id]);
+            const result = {
+                in_tp: inTp ? inTp.tp : 0,
+                out_tp: outTp ? outTp.tp : 0,
+                profit: profit ? profit.tp : 0,
+                profit_percent: profitPer ? profitPer.tp : 0
+            };
             return result;
         }
     }

+ 32 - 0
app/service/report.js

@@ -386,6 +386,10 @@ module.exports = app => {
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetZb(params.budget_id) : budgetSource.tenderZb(params.tender_id));
                             runnableKey.push(filter);
                             break;
+                        case 'mem_budget_ctrl':
+                            runnableRst.push(params.budget_id > 0 ? budgetSource.budgetCtrl(params.budget_id) : budgetSource.tenderCtrl(params.tender_id));
+                            runnableKey.push(filter);
+                            break;
                         case 'mem_budget_final':
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetFinal(params.budget_id) : budgetSource.tenderFinal(params.tender_id));
                             runnableKey.push(filter);
@@ -406,6 +410,10 @@ module.exports = app => {
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetZb(params.budget_id, true) : budgetSource.tenderZb(params.tender_id, true));
                             runnableKey.push(filter);
                             break;
+                        case 'mem_budget_ctrl_filter':
+                            runnableRst.push(params.budget_id > 0 ? budgetSource.budgetCtrl(params.budget_id, true) : budgetSource.tenderCtrl(params.tender_id, true));
+                            runnableKey.push(filter);
+                            break;
                         case 'mem_budget_final_filter':
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetFinal(params.budget_id, true) : budgetSource.tenderFinal(params.tender_id, true));
                             runnableKey.push(filter);
@@ -821,6 +829,30 @@ module.exports = app => {
             return rptPhasePay.getReportData(params, sourceFilters, memFieldKeys, customDefine, customSelect);
         }
 
+        // params = { tender_id: int, cost_stage_id: uuid }
+        async cost_ledger(params, sourceFilters, memFieldKeys, customDefine, customSelect) {
+            const RptCostStage = require('../lib/rm/cost_ledger_stage');
+            const rptCostStage = new RptCostStage(this.ctx);
+
+            return rptCostStage.getReportData(params, sourceFilters, memFieldKeys, customDefine, customSelect);
+        }
+
+        // params = { tender_id: int, cost_stage_id: uuid }
+        async cost_book(params, sourceFilters, memFieldKeys, customDefine, customSelect) {
+            const RptCostStage = require('../lib/rm/cost_book_stage');
+            const rptCostStage = new RptCostStage(this.ctx);
+
+            return rptCostStage.getReportData(params, sourceFilters, memFieldKeys, customDefine, customSelect);
+        }
+
+        // params = { tender_id: int, cost_stage_id: uuid }
+        async cost_analysis(params, sourceFilters, memFieldKeys, customDefine, customSelect) {
+            const RptCostStage = require('../lib/rm/cost_analysis_stage');
+            const rptCostStage = new RptCostStage(this.ctx);
+
+            return rptCostStage.getReportData(params, sourceFilters, memFieldKeys, customDefine, customSelect);
+        }
+
         async getReportData(source_type, params, sourceFilters, memFieldKeys, customDefine, customSelect) {
             this.clearReportCache();
             const sourceType = sourceTypeConst.sourceTypeData.find(x => { return x.id === source_type; });

+ 5 - 1
app/service/sub_project.js

@@ -18,6 +18,8 @@ const defaultFunRela = {
     showMinusCol: true,
     imType: imType.zl.value,
     needGcl: false,
+    budgetZb: true,
+    budgetCtrl: true,
 };
 const funSet = require('../const/fun_set');
 const defaultFunSet = funSet.defaultInfo;
@@ -721,7 +723,9 @@ module.exports = app => {
             const result = await this.db.update(this.tableName, {
                 id: id, fun_rela: JSON.stringify({
                     banOver: data.banOver, hintOver: data.hintOver, banMinusChangeBills: data.banMinusChangeBills,
-                    imType: data.imType, needGcl: data.needGcl, minusNoValue: data.minusNoValue,
+                    imType: data.imType,
+                    needGcl: data.needGcl, budgetCtrl: data.budgetCtrl, budgetZb: data.budgetZb,
+                    minusNoValue: data.minusNoValue,
                     lockPayExpr: data.lockPayExpr, showMinusCol: data.showMinusCol, ledgerAss: data.ledgerAss,
                 }),
             });

+ 3 - 0
app/view/budget/detail.ejs

@@ -32,6 +32,9 @@
                     <a class="btn btn-sm btn-primary" href="javascript: void(0);" id="budget-import">导入</a>
                     <a href="#import-dsk" data-toggle="modal" data-target="#import-dsk" class="btn btn-sm btn-primary">导入计价文件</a>
                     <% } %>
+                    <% if (ctx.session.sessionUser.is_admin) { %>
+                    <a class="btn btn-sm btn-primary" href="<%- ctx.params.btype %>/init" id="budget-import">初始化</a>
+                    <% } %>
                 </div>
             </div>
         </div>

+ 6 - 3
app/view/budget/info.ejs

@@ -78,15 +78,18 @@
                                     <h1 id="total_rate" class="">0%</h1>
                                     <h3 class="text-muted">增减幅度</h3>
                                     <div class="row my-4 mx-0">
-                                        <div class="col-4 text-center text-muted p-0">
+                                        <div class="col-3 text-center text-muted p-0">
                                             <h5><b id="total_gu_tp"></b></h5> <h6>投资估算</h6>
                                         </div>
-                                        <div class="col-4 text-center text-muted p-0">
+                                        <div class="col-3 text-center text-muted p-0">
                                             <h5><b id="total_yu_tp"></b></h5> <h6>施工图预算</h6>
                                         </div>
-                                        <div class="col-4 text-center text-muted p-0">
+                                        <div class="col-3 text-center text-muted p-0">
                                             <h5><b id="total_zb_tp"></b></h5> <h6>招标预算</h6>
                                         </div>
+                                        <div class="col-3 text-center text-muted p-0">
+                                            <h5><b id="total_ctrl_tp"></b></h5> <h6>控制目标</h6>
+                                        </div>
                                     </div>
                                 </div>
                             </div>

+ 6 - 1
app/view/budget/sub_menu_list.ejs

@@ -3,8 +3,13 @@
 <nav-menu title="投资估算" url="/sp/<%= ctx.subProject.id %>/budget/gu" ml="3" active="<%= ctx.url.indexOf('/gu') %>"></nav-menu>
 <nav-menu title="设计概算" url="/sp/<%= ctx.subProject.id %>/budget/gai%>" ml="3" active="<%= ctx.url.indexOf('/gai') %>"></nav-menu>
 <nav-menu title="施工图预算" url="/sp/<%= ctx.subProject.id %>/budget/yu" ml="3" active="<%= ctx.url.indexOf('/yu') %>"></nav-menu>
+<% if (ctx.subProject.page_show.budgetZb) { %>
 <nav-menu title="招标预算" url="/sp/<%= ctx.subProject.id %>/budget/zb" ml="3" active="<%= ctx.url.indexOf('/zb') %>"></nav-menu>
+<% } %>
+<% if (ctx.subProject.page_show.budgetCtrl) { %>
+<nav-menu title="控制目标" url="/sp/<%= ctx.subProject.id %>/budget/ctrl" ml="3" active="<%= ctx.url.indexOf('/ctrl') %>"></nav-menu>
+<% } %>
 <!--<nav-menu title="输出报表" url="/sp/<%= ctx.subProject.id %>/budget/report" ml="3" active="<%= ctx.url.indexOf('/report') %>"></nav-menu>-->
-<% if (!ctx.budget.readOnly && (ctx.url.indexOf('/gu') > 0 || ctx.url.indexOf('/gai') > 0 || ctx.url.indexOf('/yu') > 0 || ctx.url.indexOf('/zb') > 0)) { %>
+<% if (!ctx.budget.readOnly && (ctx.url.indexOf('/gu') > 0 || ctx.url.indexOf('/gai') > 0 || ctx.url.indexOf('/yu') > 0 || ctx.url.indexOf('/zb') > 0 || ctx.url.indexOf('/ctrl') > 0)) { %>
 <div class="contarl-box"><button class="btn btn-primary btn-sm btn-block" data-toggle="modal" data-target="#budget-set">设置</button></div>
 <% } %>

+ 14 - 0
app/view/sp_setting/fun.ejs

@@ -363,6 +363,18 @@
                                 <div class="form-group mb-4">
                                     <div>
                                         <div class="form-check form-check-inline">
+                                            <input class="form-check-input" type="checkbox" id="budget_zb" name="budget_zb" <% if (funRela.budgetZb) { %>checked<% } %> onchange="updateSetting();">
+                                            <label class="form-check-label" for="budget_zb">招标预算</label>
+                                        </div>
+                                    </div>
+                                    <div>
+                                        <div class="form-check form-check-inline">
+                                            <input class="form-check-input" type="checkbox" id="budget_ctrl" name="budget_ctrl" <% if (funRela.budgetCtrl) { %>checked<% } %> onchange="updateSetting();">
+                                            <label class="form-check-label" for="budget_ctrl">控制目标</label>
+                                        </div>
+                                    </div>
+                                    <div>
+                                        <div class="form-check form-check-inline">
                                             <input class="form-check-input" type="checkbox" id="need_gcl" name="need_gcl" <% if (funRela.needGcl) { %>checked<% } %> onchange="updateSetting();">
                                             <label class="form-check-label" for="need_gcl">显示清单信息</label>
                                         </div>
@@ -642,6 +654,8 @@
             lockPayExpr: $('#lockPayExpr')[0].checked,
             showMinusCol: $('#showMinusCol')[0].checked,
             needGcl: $('#need_gcl')[0].checked,
+            budgetZb: $('#budget_zb')[0].checked,
+            budgetCtrl: $('#budget_ctrl')[0].checked,
             openChangeProject: $('#openChangeProject')[0].checked,
             openChangeApply: $('#openChangeApply')[0].checked,
             openChangePlan: $('#openChangePlan')[0].checked,

+ 859 - 1
app/view/spss/dev_test.ejs

@@ -20,7 +20,7 @@
                                     </div>
                                     <select class="form-control m-0" id="st-rpt-source-type">
                                         <% for (const std of sourceType.sourceTypeData) { %>
-                                        <option value="<%- std.id %>" stParams="<%- JSON.stringify(std.params) %>"><%- std.name %></option>
+                                        <option value="<%- std.id %>" stType="<%- std.key %>" stParams="<%- JSON.stringify(std.params) %>"><%- std.name %></option>
                                         <% } %>
                                     </select>
                                 </div>
@@ -43,11 +43,869 @@
                         </div>
                     </div>
                 </div>
+                <div class="col-7 mb-2">
+                    <h6>数据源可用指标表</h6>
+                    <div class="card p-2">
+                        <div class="mt-1 scroll-y modal-height-600">
+                            <table class="table table-sm table-bordered table-hover" id="valid-table-list">
+                            </table>
+                        </div>
+                    </div>
+                </div>
             </div>
         </div>
     </div>
 </div>
 <script>
+    const validTablePart = [
+        {
+            "id": 1,
+            "name": "标段&期",
+            "key": "tender",
+            "tables": [
+                {
+                    type: "advance",
+                    "tables": [
+                        "advance_pay"
+                    ]
+                },
+                {
+                    type: "budget",
+                    "tables": [
+                        "mem_budget_info",
+                        "mem_budget_gu",
+                        "mem_budget_gai",
+                        "mem_budget_yu",
+                        "mem_budget_zb",
+                        "mem_budget_final"
+                    ]
+                },
+                {
+                    type: "change",
+                    "tables": [
+                        "mem_change",
+                        "mem_change_bills",
+                        "mem_change_audit"
+                    ]
+                },
+                {
+                    type: "change_apply",
+                    "tables": [
+                        "mem_change_apply",
+                        "mem_change_apply_audit",
+                        "mem_change_apply_bills"
+                    ]
+                },
+                {
+                    type: "change_plan",
+                    "tables": [
+                        "mem_change_plan",
+                        "mem_change_plan_audit",
+                        "mem_change_plan_bills"
+                    ]
+                },
+                {
+                    type: "change_project",
+                    "tables": [
+                        "mem_change_project",
+                        "mem_change_project_audit"
+                    ]
+                },
+                {
+                    type: "contract",
+                    "tables": [
+                        "contract_tree_1",
+                        "contract_1",
+                        "contract_pay_1",
+                        "contract_tree_2",
+                        "contract_2",
+                        "contract_pay_2",
+                        "sp_contract_tree_1",
+                        "sp_contract_1",
+                        "sp_contract_pay_1",
+                        "sp_contract_tree_2",
+                        "sp_contract_2",
+                        "sp_contract_pay_2"
+                    ]
+                },
+                {
+                    type: "fenjian",
+                    "tables": [
+                        "mem_fj_change_progress",
+                        "mem_fj_change_sum"
+                    ]
+                },
+                {
+                    type: "fund",
+                    "tables": [
+                        "mem_financial_pay",
+                        "mem_financial_pay_contract",
+                        "mem_financial_pay_tender",
+                        "mem_financial_transfer",
+                        "mem_financial_transfer_tender",
+                        "mem_financial_pay_stage"
+                    ]
+                },
+                {
+                    type: "gather",
+                    "tables": [
+                        "mem_gather_stage_bills",
+                        "mem_gather_stage_pos",
+                        "mem_gather_tender_info",
+                        "mem_gather_stage_pay",
+                        "mem_gather_deal_bills",
+                        "mem_gather_change",
+                        "mem_gather_change_bills",
+                        "mem_gather_stage_change",
+                        "mem_gather_stage_jgcl",
+                        "mem_gather_stage_yjcl",
+                        "mem_gather_stage_bonus",
+                        "mem_gather_stage_other",
+                        "mem_gather_stage_safe_prod",
+                        "mem_gather_stage_temp_land",
+                        "mem_gather_advance_pay",
+                        "mem_gather_tender_cert"
+                    ]
+                },
+                {
+                    type: "import_change",
+                    "tables": [
+                        "mem_import_change",
+                        "mem_import_change_bills"
+                    ]
+                },
+                {
+                    type: "jh",
+                    "tables": [
+                        "mem_jh_im_change",
+                        "mem_jh_gather_im_change",
+                        "mem_jh_gather_stage_bills_compare"
+                    ]
+                },
+                {
+                    type: "material",
+                    "tables": [
+                        "mem_material",
+                        "mem_material_gl",
+                        "mem_material_bills",
+                        "mem_material_pos",
+                        "mem_material_gl_detail",
+                        "mem_material_stage",
+                        "mem_select_material",
+                        "mem_select_material_audit",
+                        "mem_material_gather_bills",
+                        "mem_material_gather_xmj",
+                        "mem_material_gather_gl"
+                    ]
+                },
+                {
+                    type: "material_sum",
+                    "tables": [
+                        "mem_material_sum_gl"
+                    ]
+                },
+                {
+                    type: "other",
+                    "tables": [
+                        "ledger_cooperation",
+                        "mem_month_progress",
+                        "mem_sign_select",
+                        "mem_tender_cert",
+                        "mem_pos_calc_detail"
+                    ]
+                },
+                {
+                    type: "phase_pay",
+                    "tables": [
+                        "mem_phase_pay",
+                        "mem_phase_pay_detail"
+                    ]
+                },
+                {
+                    type: "pm",
+                    "tables": [
+                        "mem_pm_deal_pay",
+                        "mem_pm_deal_tree"
+                    ]
+                },
+                {
+                    type: "project",
+                    "tables": [
+                        "construction_unit",
+                        "mem_calc_template"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_gcl_gather_bills",
+                        "mem_gcl_gather_xmj",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "rpt_custom",
+                    "tables": [
+                        "mem_custom_select"
+                    ]
+                },
+                {
+                    type: "schedule",
+                    "tables": [
+                        "mem_schedule",
+                        "mem_schedule_month",
+                        "mem_schedule_month"
+                    ]
+                },
+                {
+                    type: "spss",
+                    "tables": [
+                        "mem_spss_gather_info_tender",
+                        "mem_spss_gather_info",
+                        "mem_spss_gather_ledger_tender",
+                        "mem_spss_gather_ledger_bills",
+                        "mem_spss_gather_ledger_bills",
+                        "mem_spss_gather_stage_tender",
+                        "mem_spss_gather_stage_bills",
+                        "mem_spss_gather_stage_pos",
+                        "mem_spss_gather_stage_extra_tender",
+                        "mem_spss_gather_stage_jgcl",
+                        "mem_spss_gather_stage_yjcl",
+                        "mem_spss_gather_stage_bonus",
+                        "mem_spss_gather_stage_other",
+                        "mem_spss_gather_stage_safe_prod",
+                        "mem_spss_gather_stage_temp_land",
+                        "mem_spss_gather_stage_pay_tender",
+                        "mem_spss_gather_stage_pay",
+                        "mem_spss_compare_ledger_tender",
+                        "mem_spss_compare_ledger_bills",
+                        "mem_spss_compare_ledger_pos",
+                        "mem_spss_compare_stage_tender",
+                        "mem_spss_compare_stage_bills",
+                        "mem_spss_compare_stage_pos"
+                    ]
+                },
+                {
+                    type: "stage",
+                    "tables": [
+                        "mem_stages",
+                        "mem_stage_bills",
+                        "mem_stage_bills_compare",
+                        "mem_stage_pos",
+                        "mem_stage_pos_compare",
+                        "mem_stage_pay",
+                        "stage_audit",
+                        "mem_stage_audit_ass",
+                        "mem_stage_change",
+                        "mem_stage_change_bills",
+                        "mem_stage_change_ledger",
+                        "mem_change_info",
+                        "mem_change_info_bills",
+                        "mem_stage_import_change"
+                    ]
+                },
+                {
+                    type: "stage_extra",
+                    "tables": [
+                        "mem_stage_jgcl",
+                        "mem_stage_yjcl",
+                        "mem_stage_bonus",
+                        "mem_stage_other",
+                        "mem_stage_safe_prod",
+                        "mem_stage_temp_land"
+                    ]
+                },
+                {
+                    type: "stage_im",
+                    "tables": [
+                        "mem_stage_im_zl",
+                        "mem_stage_im_tz",
+                        "mem_stage_im_tz_bills"
+                    ]
+                },
+                {
+                    type: "stage_rela",
+                    "tables": [
+                        "mem_stage_rela_im",
+                        "mem_stage_rela_im_bills"
+                    ]
+                },
+                {
+                    type: "stage_sum",
+                    "tables": [
+                        "mem_stage_sum_bills",
+                        "mem_stage_sum_pos",
+                        "mem_stage_sum_pay"
+                    ]
+                },
+                {
+                    type: "tag",
+                    "tables": [
+                        "mem_ledger_tag",
+                        "mem_stage_tag",
+                        "mem_all_tag"
+                    ]
+                },
+                {
+                    type: "xj",
+                    "tables": [
+                        "mem_xj_material_calc"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 2,
+            "name": "合同支付",
+            "key": "phase_pay",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_phase_pay",
+                        "mem_phase_pay_detail"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 10,
+            "name": "预付款",
+            "key": "advance",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_advance",
+                        "mem_advance_audit",
+                        "mem_advance_file"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 20,
+            "name": "变更令",
+            "key": "change",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_change",
+                        "mem_change_bills",
+                        "mem_change_bills_recalc",
+                        "mem_change_audit",
+                        "mem_change_att"
+                    ]
+                },
+                {
+                    type: "multi",
+                    "tables": [
+                        "mem_change_ledger_bills",
+                        "mem_change_ledger_pos"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info",
+                        "mem_deal_bills"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 21,
+            "name": "变更方案",
+            "key": "change_plan",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_change_plan",
+                        "mem_change_plan_bills",
+                        "mem_change_plan_audit",
+                        "mem_change_plan_att"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 22,
+            "name": "变更立项",
+            "key": "change_project",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_change_project",
+                        "mem_change_project_audit",
+                        "mem_change_project_att"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 23,
+            "name": "变更申请",
+            "key": "change_apply",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_change_apply",
+                        "mem_change_apply_bills",
+                        "mem_change_apply_audit",
+                        "mem_change_apply_att"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 30,
+            "name": "材料调差",
+            "key": "material",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_material",
+                        "mem_material_audit",
+                        "mem_material_gl",
+                        "mem_material_gl_detail",
+                        "mem_material_bills",
+                        "mem_material_pos",
+                        "mem_material_stage",
+                        "mem_material_exponent",
+                        "mem_material_gather_bills",
+                        "mem_material_gather_xmj",
+                        "mem_material_gather_gl",
+                        "mem_material_exponent_node",
+                        "mem_material_exponent_shard"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info",
+                        "mem_deal_bills"
+                    ]
+                },
+                {
+                    type: "xj",
+                    "tables": [
+                        "mem_xj_material_calc"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 40,
+            "name": "安全计量",
+            "key": "safe_stage",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_safe_stage",
+                        "mem_safe_stage_audit"
+                    ]
+                },
+                {
+                    type: "bills",
+                    "tables": [
+                        "mem_safe_stage_bills",
+                        "mem_safe_stage_bills_compare"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 100,
+            "name": "支付审批",
+            "key": "payment",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_payment_tender",
+                        "mem_payment_tender_info",
+                        "mem_payment_detail"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 101,
+            "name": "安全生产费",
+            "key": "payment_safe",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_payment_tender",
+                        "mem_payment_tender_info",
+                        "mem_payment_detail"
+                    ]
+                },
+                {
+                    type: "bills",
+                    "tables": [
+                        "mem_payment_safe_bills",
+                        "mem_payment_safe_bills_compare"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 200,
+            "name": "动态投资",
+            "key": "budget",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_info",
+                        "mem_payment_tender_info",
+                        "mem_budget"
+                    ]
+                },
+                {
+                    type: "bills",
+                    "tables": [
+                        "mem_budget_gu",
+                        "mem_budget_gai",
+                        "mem_budget_yu",
+                        "mem_budget_zb",
+                        "mem_budget_final"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 300,
+            "name": "合同管理",
+            "key": "contract",
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "sp_contract_tree_1",
+                        "sp_contract_1",
+                        "sp_contract_pay_1",
+                        "sp_contract_tree_2",
+                        "sp_contract_2",
+                        "sp_contract_pay_2",
+                        "contract_tree_1",
+                        "contract_1",
+                        "contract_pay_1",
+                        "contract_tree_2",
+                        "contract_2",
+                        "contract_pay_2"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 400,
+            "name": "成本报审",
+            "key": "cost_ledger",
+            "params": {
+                "tender_id": 1,
+                "cost_stage_id": "uuid"
+            },
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_cost_stage",
+                        "mem_cost_stage_audit"
+                    ]
+                },
+                {
+                    type: "bills",
+                    "tables": [
+                        "mem_cost_stage_ledger",
+                        "mem_cost_stage_ledger_compare",
+                        "mem_cost_stage_detail",
+                        "mem_cost_stage_detail_compare"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 401,
+            "name": "财务账面",
+            "key": "cost_book",
+            "params": {
+                "tender_id": 1,
+                "cost_stage_id": "uuid"
+            },
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_cost_stage",
+                        "mem_cost_stage_audit"
+                    ]
+                },
+                {
+                    type: "bills",
+                    "tables": [
+                        "mem_cost_stage_book",
+                        "mem_cost_stage_book_compare",
+                        "mem_cost_stage_book_detail",
+                        "mem_cost_stage_detail_compare"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        },
+        {
+            "id": 402,
+            "name": "成本分析",
+            "key": "cost_analysis",
+            "params": {
+                "tender_id": 1,
+                "cost_stage_id": "uuid"
+            },
+            "tables": [
+                {
+                    type: "base",
+                    "tables": [
+                        "mem_cost_stage",
+                        "mem_cost_stage_audit"
+                    ]
+                },
+                {
+                    type: "bills",
+                    "tables": [
+                        "mem_cost_stage_analysis",
+                        "mem_cost_stage_analysis_compare",
+                        "mem_cost_stage_analysis_detail",
+                        "mem_cost_stage_analysis_detail_compare"
+                    ]
+                },
+                {
+                    type: "rpt_analysis",
+                    "tables": [
+                        "mem_union_data",
+                        "mem_master_converse"
+                    ]
+                },
+                {
+                    type: "tender",
+                    "tables": [
+                        "mem_project",
+                        "mem_tender",
+                        "mem_tender_info"
+                    ]
+                }
+            ]
+        }
+    ];
+    const testInfo = {
+        cost_ledger: { params: {tender_id: 5612, cost_stage_id: "920e0388-5c66-48c3-afa0-aede084b1417" }, table: 'mem_cost_stage;mem_cost_stage_audit;mem_cost_stage_ledger;mem_cost_stage_detail' },
+        cost_book: { params: {tender_id: 5612, cost_stage_id: "5a653840-32be-42d6-8943-ef412a55d148" }, table: 'mem_cost_stage;mem_cost_stage_audit;mem_cost_stage_book;mem_cost_stage_book_detail' },
+        cost_analysis: { params: {tender_id: 5612, cost_stage_id: "e9747d42-bf09-49a9-ac0b-91ccecb95bff" }, table: 'mem_cost_stage;mem_cost_stage_audit;mem_cost_stage_analysis;mem_cost_stage_analysis_detail' },
+    };
+    function chunkArray(array, size) {
+        const chunks = [];
+        for (let i = 0; i < array.length; i += size) {
+            // slice 不会修改原数组,返回从 i 开始到 i+size 的新数组
+            chunks.push(array.slice(i, i + size));
+        }
+        return chunks;
+    }
+    const loadValidTable = function(type) {
+        const vt = validTablePart.find(x => { return x.key === type; });
+        const html = [];
+        if (vt) {
+            for (const t of vt.tables) {
+                html.push(`<tr><th class="text-center" colspan="4">${t.type}</th></tr>`);
+                const partTables = chunkArray(t.tables, 4);
+                for (const pt of partTables) {
+                    html.push(`<tr><td>${pt[0] || ''}</td><td>${pt[1] || ''}</td><td>${pt[2] || ''}</td><td>${pt[3] || ''}</td></tr>`);
+                }
+            }
+        }
+        $('#valid-table-list').html(html.join(''));
+    }
+    loadValidTable('tender');
+    $('#st-rpt-source-type').change(function() {
+        const stType = $(`option[value=${this.value}]`).attr('stType');
+        if (testInfo[stType]) {
+            $('#st-rpt-params').val(JSON.stringify(testInfo[stType].params));
+            $('#st-rpt-source-filters').val(testInfo[stType].table);
+        }
+        loadValidTable(stType);
+    });
     $('#st-rpt-check').click(function() {
         const data = { type: 'report', detail: { source_type: parseInt($('#st-rpt-source-type').val()) } };
         try {

+ 6 - 0
app/view/stage/audit_modal.ejs

@@ -258,6 +258,8 @@
                                                                 <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
                                                                 <% } else if (auditor.status === auditConst.status.checkNo || auditor.status === auditConst.status.checkNoPre || auditor.status === auditConst.status.checkCancel) { %>
                                                                 <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
+                                                                <% } else if (auditor.status === auditConst.status.checkSkip) { %>
+                                                                <span class="pull-right text-secondary"><i class="fa fa-minus-circle"></i></span>
                                                                 <% } %>
                                                             </div>
                                                             <% if (auditor.opinion) { %>
@@ -451,6 +453,8 @@
                                                             <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
                                                             <% } else if (auditor.status === auditConst.status.checking) { %>
                                                             <span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>
+                                                            <% } else if (auditor.status === auditConst.status.checkSkip) { %>
+                                                            <span class="pull-right text-secondary"><i class="fa fa-minus-circle"></i></span>
                                                             <% } %>
                                                         </div>
                                                         <% if (auditor.status !== auditConst.status.uncheck && auditor.opinion) { %>
@@ -644,6 +648,8 @@
                                                             <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
                                                             <% } else if (auditor.status === auditConst.status.checking) { %>
                                                             <span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>
+                                                            <% } else if (auditor.status === auditConst.status.checkSkip) { %>
+                                                            <span class="pull-right text-secondary"><i class="fa fa-minus-circle"></i></span>
                                                             <% } %>
                                                         </div>
                                                         <% if (auditor.status !== auditConst.status.uncheck && auditor.opinion) { %>

+ 5 - 2
app/view/template/pos_calc.ejs

@@ -23,8 +23,8 @@
                     <div id="detail-ctrl" style="display: none;">
                         <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="save"> 保存</a>
-                        <a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="export"> 导出</a>
-                        <a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="import"> 导入</a>
+                        <!--<a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="export"> 导出</a>-->
+                        <!--<a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="import"> 导入</a>-->
                     </div>
                     <div id="detail-user-info" class="ml-auto"></div>
                 </div>
@@ -46,6 +46,9 @@
                                     <li class="nav-item mt-1" id="template-ctrl">
                                         <a href="javascript: void(0);" name="template-opr" type="add" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus" aria-hidden="true"></i></a>
                                         <a href="javascript: void(0);" name="template-opr" type="delete" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
+                                        <a href="javascript: void(0);" name="template-opr" type="move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="移动"><i class="fa fa-exchange" aria-hidden="true"></i></a>
+                                        <a href="javascript: void(0);" id="copy-template" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="复制模板"><i class="fa fa-copy" aria-hidden="true"></i></a>
+                                        <a href="javascript: void(0);" id="paste-template" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="粘贴模板"><i class="fa fa-paste" aria-hidden="true"></i></a>
                                         <button class="btn btn-sm btn-light text-primary" href="#rela-no-folder" data-toggle="modal" data-target="#rela-no-folder"> 导入未分类模板</button>
                                     </li>
                                 </ul>

+ 2 - 0
app/view/tender/detail.ejs

@@ -1274,6 +1274,8 @@
                             historyHTML.push('<span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>');
                         } if (auditor.status === auditConst.status.checkNo || auditor.status === auditConst.status.checkNoPre || auditor.status === auditConst.status.checkCancel) {
                             historyHTML.push('<span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>');
+                        } else if (auditor.status === auditConst.status.checkSkip) {
+                            historyHTML.push('<span class="pull-right text-secondary"><i class="fa fa-minus-circle"></i></span>');
                         }
                         historyHTML.push('</div>');
                         if (auditor.opinion) {

+ 4 - 2
app/view/tender/modal.ejs

@@ -182,7 +182,7 @@
                         historyHTML.push('<div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>');
                     } else if(group.status === auditConst2.status.checkAgain) {
                         historyHTML.push('<div class="timeline-item-icon bg-warning text-light"><i class="fa fa-check"></i></div>');
-                    }  else {
+                    } else {
                         historyHTML.push('<div class="timeline-item-icon bg-secondary text-light"></div>');
                     }
 
@@ -207,8 +207,10 @@
                             historyHTML.push('<span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>');
                         } else if (auditor.status === auditConst2.status.checking) {
                             historyHTML.push('<span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>');
-                        }  else if (auditor.status === auditConst2.status.checkAgain) {
+                        } else if (auditor.status === auditConst2.status.checkAgain) {
                             historyHTML.push('<span class="pull-right text-warning"><i class="fa fa-check-circle"></i></span>');
+                        } else if (auditor.status === auditConst2.status.checkSkip) {
+                            historyHTML.push('<span class="pull-right text-secondary"><i class="fa fa-minus-circle"></i></span>');
                         }
                         historyHTML.push('</div>');
                         if (auditor.opinion) {

+ 45 - 0
sql/update.sql

@@ -395,6 +395,51 @@ ADD COLUMN `num_u` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值7'
 ALTER TABLE `zh_calc_tmpl`
 ADD COLUMN `folder_id` varchar(36) NOT NULL DEFAULT '' COMMENT '分类id(zh_calc_tmpl_folder.id)' AFTER `tid`;
 
+ALTER TABLE `calculation`.`zh_project`
+ADD COLUMN `calc_tmpl_set` json NULL COMMENT '计算模板配置' AFTER `common_json`;
+
+CREATE TABLE `zh_budget_ctrl`  (
+  `id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '自增id',
+  `bid` int(10) NOT NULL COMMENT '概算投资项目id',
+  `tree_id` int(10) NOT NULL COMMENT '节点id',
+  `tree_pid` int(10) NOT NULL COMMENT '父节点id',
+  `level` tinyint(4) NOT NULL COMMENT '层级',
+  `order` mediumint(4) NOT NULL DEFAULT 0 COMMENT '同级排序',
+  `full_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '层级定位辅助字段parent.full_path.ledger_id',
+  `is_leaf` tinyint(1) NOT NULL COMMENT '是否叶子节点,界面显示辅助字段',
+  `code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '节点编号',
+  `b_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称',
+  `unit` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '单位',
+  `unit_price` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '单价',
+  `quantity` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数量',
+  `total_price` decimal(24, 8) NULL DEFAULT 0.00000000 COMMENT '金额',
+  `dgn_qty1` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '设计数量1',
+  `dgn_qty2` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '设计数量2',
+  `drawing_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图册号',
+  `memo` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '备注',
+  `node_type` int(4) UNSIGNED NULL DEFAULT 0 COMMENT '节点类别',
+  `source` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '添加源',
+  `remark` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '备注',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_tender_id`(`bid`) USING BTREE,
+  INDEX `idx_template_pid`(`tree_pid`) USING BTREE,
+  INDEX `idx_level`(`level`) USING BTREE,
+  INDEX `idx_full_path`(`bid`, `full_path`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '控制目标数据' ROW_FORMAT = Dynamic;
+
+ALTER TABLE `zh_budget_final`
+ADD COLUMN `ctrl_dgn_qty1` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '控制目标-设计数量1' AFTER `zb_tp`,
+ADD COLUMN `ctrl_dgn_qty2` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '控制目标-设计数量1' AFTER `ctrl_dgn_qty1`,
+ADD COLUMN `ctrl_dgn_qty` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '控制目标-设计数量1/设计数量2' AFTER `ctrl_dgn_qty2`,
+ADD COLUMN `ctrl_dgn_price` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '控制目标-经济指标' AFTER `ctrl_dgn_qty`,
+ADD COLUMN `ctrl_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '控制目标-金额' AFTER `ctrl_dgn_price`;
+
+ALTER TABLE `zh_budget_std`
+ADD COLUMN `ctrl_chapter_id` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '' COMMENT '控制目标-项目节-id列表(\',\'分隔)' AFTER `zb_bills_id`,
+ADD COLUMN `ctrl_template_id` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '' COMMENT '控制目标-新建模板-id列表(\',\'分隔)' AFTER `ctrl_chapter_id`,
+ADD COLUMN `ctrl_bills_id` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '' COMMENT '控制目标-工程量清单-id列表(\',\'分隔)' AFTER `ctrl_template_id`;
+
 ------------------------------------
 -- 表数据
 ------------------------------------