Parcourir la source

1. 计算模板,设计量明细改版
2. 成本分析部分代码

MaiXinRong il y a 5 jours
Parent
commit
80b0b3b464
38 fichiers modifiés avec 3530 ajouts et 392 suppressions
  1. 12 0
      app/base/base_bills_service.js
  2. 3 0
      app/base/base_tree_service.js
  3. 126 38
      app/controller/cost_controller.js
  4. 39 0
      app/controller/template_controller.js
  5. 3 1
      app/middleware/cost_stage_check.js
  6. 932 0
      app/public/js/cost_stage_analysis.js
  7. 3 4
      app/public/js/cost_stage_book.js
  8. 17 3
      app/public/js/cost_stage_ledger.js
  9. 65 0
      app/public/js/cost_tmpl.js
  10. 133 13
      app/public/js/ledger.js
  11. 1 1
      app/public/js/measure_stage.js
  12. 450 119
      app/public/js/pos_calc_tmpl.js
  13. 1 0
      app/router.js
  14. 86 25
      app/service/calc_tmpl.js
  15. 49 0
      app/service/calc_tmpl_folder.js
  16. 12 2
      app/service/cost_stage.js
  17. 685 0
      app/service/cost_stage_analysis.js
  18. 159 0
      app/service/cost_stage_analysis_detail.js
  19. 35 0
      app/service/cost_stage_book.js
  20. 142 95
      app/service/cost_stage_book_detail.js
  21. 78 29
      app/service/cost_stage_detail.js
  22. 3 3
      app/service/cost_stage_ledger.js
  23. 127 0
      app/service/pos.js
  24. 1 0
      app/service/pos_calc_detail.js
  25. 2 2
      app/service/safe_stage_bills.js
  26. 84 0
      app/view/cost/analysis.ejs
  27. 11 14
      app/view/cost/analysis_list.ejs
  28. 9 16
      app/view/cost/analysis_list_modal.ejs
  29. 2 1
      app/view/cost/analysis_menu_list.ejs
  30. 30 0
      app/view/cost/analysis_modal.ejs
  31. 4 4
      app/view/cost/book_list.ejs
  32. 2 1
      app/view/cost/book_menu_list.ejs
  33. 1 0
      app/view/cost/ledger.ejs
  34. 8 0
      app/view/cost/ledger_modal.ejs
  35. 35 15
      app/view/template/pos_calc.ejs
  36. 2 1
      app/view/template/pos_calc_modal.ejs
  37. 36 1
      config/web.js
  38. 142 4
      sql/update.sql

+ 12 - 0
app/base/base_bills_service.js

@@ -759,6 +759,18 @@ class BaseBillsSerivce extends TreeService {
                                     newDetail.num_g = cd.num_g || 0;
                                     newDetail.num_h = cd.num_h || 0;
                                     newDetail.num_i = cd.num_i || 0;
+                                    newDetail.num_j = cd.num_j || 0;
+                                    newDetail.num_k = cd.num_k || 0;
+                                    newDetail.num_l = cd.num_l || 0;
+                                    newDetail.num_m = cd.num_m || 0;
+                                    newDetail.num_n = cd.num_n || 0;
+                                    newDetail.num_o = cd.num_o || 0;
+                                    newDetail.num_p = cd.num_p || 0;
+                                    newDetail.num_q = cd.num_q || 0;
+                                    newDetail.num_r = cd.num_r || 0;
+                                    newDetail.num_s = cd.num_s || 0;
+                                    newDetail.num_t = cd.num_t || 0;
+                                    newDetail.num_u = cd.num_u || 0;
                                     newDetail.qty = cd.qty || 0;
                                     newDetail.expr = cd.expr || '';
                                     newDetail.spec = cd.spec || '';

+ 3 - 0
app/base/base_tree_service.js

@@ -399,6 +399,8 @@ class TreeService extends Service {
         return data;
     }
 
+    _getDefaultData(data) {
+    }
     /**
      * 新增数据(新增为selectData的后项,该方法不可单独使用)
      *
@@ -422,6 +424,7 @@ class TreeService extends Service {
         data[this.setting.order] = select ? select[this.setting.order] + 1 : 1;
         data[this.setting.fullPath] = data[this.setting.level] > 1 ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + data[this.setting.kid]) : data[this.setting.kid] + '';
         data[this.setting.isLeaf] = true;
+        this._getDefaultData(data);
         const result = await this.transaction.insert(this.tableName, data);
 
         this._cacheMaxLid(mid, maxId + 1);

+ 126 - 38
app/controller/cost_controller.js

@@ -88,15 +88,26 @@ module.exports = app => {
             }
         }
 
+        async getTypeStages(ctx, stage_type) {
+            const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
+            for (const s of stages) {
+                if (s.audit_status !== audit.common.status.checked) {
+                    await this.ctx.service.costStage.loadUser(s);
+                    s.is_join = s.userIds.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
+                } else {
+                    const auditors = await this.ctx.service.costStageAudit.getAuditors(s.id, s.audit_times);
+                    s.is_join = auditors.findIndex(a => { return a.audit_id === this.ctx.session.sessionUser.accountId }) >= 0;
+                }
+                s.can_del = (s.create_user_id === ctx.session.sessionUser.accountId || ctx.session.sessionUser.is_admin)
+                    && (s.audit_status === audit.common.status.uncheck || s.audit_status === audit.common.status.checkNo);
+            }
+            if (this.ctx.session.sessionUser.is_admin || this.ctx.permission.cost.visitor || this.ctx.permission.cost[stage_type + '_add']) return stages;
+            return stages.filter(s => { return s.is_join; });
+        }
         async ledger(ctx) {
             try {
                 const stage_type = this.ctx.service.costStage.stageType.ledger.key;
-                const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
-                for (const s of stages) {
-                    if (s.audit_status !== audit.common.status.checked) await this.ctx.service.costStage.loadUser(s);
-                    s.can_del = (s.create_user_id === ctx.session.sessionUser.accountId || ctx.session.sessionUser.is_admin)
-                        && (s.audit_status === audit.common.status.uncheck || s.audit_status === audit.common.status.checkNo);
-                }
+                const stages = await this.getTypeStages(ctx, stage_type);
                 const renderData = {
                     stage_type,
                     auditType: audit.auditType,
@@ -113,13 +124,8 @@ module.exports = app => {
         async book(ctx) {
             try {
                 const stage_type = this.ctx.service.costStage.stageType.book.key;
-                const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
-                for (const s of stages) {
-                    if (s.audit_status !== audit.common.status.checked) await this.ctx.service.costStage.loadUser(s);
-                    s.can_del = (s.create_user_id === ctx.session.sessionUser.accountId || ctx.session.sessionUser.is_admin)
-                        && (s.audit_status === audit.common.status.uncheck || s.audit_status === audit.common.status.checkNo);
-                }
-                const validLedgerStages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, 'ledger', 'DESC');
+                const stages = await this.getTypeStages(ctx, stage_type);
+                const validLedgerStages = await this.ctx.service.costStage.getAllCheckedStages(ctx.tender.id, 'ledger', 'DESC');
                 const renderData = {
                     stage_type,
                     auditType: audit.auditType,
@@ -137,20 +143,23 @@ module.exports = app => {
         async analysis(ctx) {
             try {
                 const stage_type = this.ctx.service.costStage.stageType.analysis.key;
-                const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
-                for (const s of stages) {
-                    if (s.audit_status !== audit.common.status.checked) await this.ctx.service.costStage.loadUser(s);
-                    s.can_del = (s.create_user_id === ctx.session.sessionUser.accountId || ctx.session.sessionUser.is_admin)
-                        && (s.audit_status === audit.common.status.uncheck || s.audit_status === audit.common.status.checkNo);
+                const stages = await this.getTypeStages(ctx, stage_type);
+                // todo 兼容以ledger做关联的情况,届时只需在标段或者项目上设置这个类型即可。
+                const analysisType = 'book';
+                let validRelaStages = await this.ctx.service.costStage.getAllCheckedStages(ctx.tender.id, analysisType, 'DESC');
+                if (stages.length > 0) {
+                    const checkedStage = stages.find(x => { return x.audit_status === audit.common.status.checked; });
+                    if (checkedStage) validRelaStages = validRelaStages.filter( x => { return x.stage_order > checkedStage.rela_stage.stage_order; });
                 }
                 const renderData = {
                     stage_type,
                     auditType: audit.auditType,
                     stages,
+                    validRelaStages,
                     auditConst: audit.common,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_stage)
                 };
-                await this.layout('cost/ledger_list.ejs', renderData, 'cost/analysis_list_modal.ejs');
+                await this.layout('cost/analysis_list.ejs', renderData, 'cost/analysis_list_modal.ejs');
             } catch(err) {
                 ctx.log(err);
                 ctx.redirect(ctx.request.header.referer);
@@ -162,7 +171,7 @@ module.exports = app => {
             try {
                 if (!ctx.permission.cost[stage_type + '_add']) throw '您无权创建期';
                 const stage_date = ctx.request.body.date;
-                if (stage_type !== 'book' && !stage_date) throw '请选择年月';
+                if (stage_type === 'ledger' && !stage_date) throw '请选择年月';
 
                 const stages = await ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
                 const unCompleteCount = stages.filter(s => { return s.status !== audit.common.status.checked; }).length;
@@ -171,12 +180,21 @@ module.exports = app => {
                 let newStage;
                 if (stage_type === 'ledger') {
                     newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, stage_date);
-                } if (stage_type === 'book') {
+                } else if (stage_type === 'book') {
                     const stage_order = ctx.request.body.stage;
                     if (!stage_order) throw '请选择关联成本';
                     const ledgerStage = await ctx.service.costStage.getStageByOrder(ctx.tender.id, 'ledger', stage_order);
                     if (!ledgerStage) throw '选择的关联成本不存在';
                     newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, ledgerStage.stage_date, { sid: ledgerStage.id, sorder: ledgerStage.stage_order });
+                } else if (stage_type === 'analysis') {
+                    const stage_order = ctx.request.body.stage;
+                    if (!stage_order) throw '请选择关联期';
+                    // todo 兼容以ledger做关联的情况,届时只需在标段或者项目上设置这个类型即可。
+                    const analysisType = 'book';
+                    const relaStage = await ctx.service.costStage.getStageByOrder(ctx.tender.id, analysisType, stage_order);
+                    if (!relaStage) throw '选择的关联期不存在';
+                    newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, relaStage.stage_date, { sid: relaStage.id, sorder: relaStage.stage_order });
+                    await this.ctx.service.costStageAnalysis.reCalcProfit(newStage.id);
                 }
 
                 if (!newStage) throw '新增期失败';
@@ -196,6 +214,16 @@ module.exports = app => {
                 // 获取最新的期数
                 const stages = await ctx.service.costStage.getAllStages(ctx.tender.id, stage.stage_type, 'DESC');
                 if (stage.id !== stages[0].id) throw '非最新一期,不可删除';
+                if (stage.stage_type === 'ledger') {
+                    const bookStages = await ctx.service.costStage.getAllStages(ctx.tender.id, 'book', 'DESC');
+                    if (bookStages.findIndex(x => { return x.rela_stage && x.rela_stage.sid === stage.id; })) throw '该期已关联财务账面,请勿删除';
+                    const analysisStages = await ctx.service.costStage.getAllStages(ctx.tender.id, 'analysis', 'DESC');
+                    if (analysisStages.findIndex(x => { return x.rela_stage && x.rela_stage.sid === stage.id; })) throw '该期已关联成本分析,请勿删除';
+                }
+                if (stage.stage_type === 'book') {
+                    const analysisStages = await ctx.service.costStage.getAllStages(ctx.tender.id, 'analysis', 'DESC');
+                    if (analysisStages.findIndex(x => { return x.rela_stage && x.rela_stage.sid === stage.id; })) throw '该期已关联成本分析,请勿删除';
+                }
 
                 await ctx.service.costStage.delete(stage_id);
                 ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage.stage_type}`);
@@ -224,6 +252,22 @@ module.exports = app => {
                 const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
                 const commonJson = ctx.subProject.common_json ? JSON.parse(ctx.subProject.common_json) : null;
                 const contractTypes = commonJson && commonJson.tender_contract_type ? commonJson.tender_contract_type : [];
+                let payStages = [];
+                if (ctx.subProject.page_show.phasePay) {
+                    const pstages = await this.ctx.service.phasePay.getAllDataByCondition({
+                        columns: ['phase_order', 'phase_date'],
+                        where: { tid: ctx.costStage.tid , audit_status: audit.common.status.checked },
+                        orders: [['create_time', 'DESC']],
+                    });
+                    payStages = pstages.map(x => { return { order: x.phase_order, date: x.phase_date}; });
+                } else {
+                    const stages = await this.ctx.service.stage.getAllDataByCondition({
+                        columns: ['order', 's_time'],
+                        where: { tid: ctx.costStage.tid, status: audit.stage.status.checked },
+                        orders: [['in_time', 'DESC']],
+                    });
+                    payStages = stages.map(x => { return { order: x.order, date: x.s_time }; });
+                }
                 const renderData = {
                     auditConst: audit.common,
                     auditType: audit.auditType,
@@ -231,6 +275,7 @@ module.exports = app => {
                     accountGroup,
                     shenpiConst,
                     authMobile: pa.auth_mobile,
+                    payStages,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost[`cost_stage_${stageTypeInfo.key}`]),
                     shenpi_status: ctx.tender.info.shenpi[stageTypeInfo.shenpi_status] || 1,
                     contractTypes,
@@ -367,7 +412,43 @@ module.exports = app => {
             }
         }
         async _analysisLoad(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const filter = data.filter.split(';');
+                const responseData = { err: 0, msg: '', data: {}, hpack: [] };
+                for (const f of filter) {
+                    switch (f) {
+                        case 'bills':
+                            responseData.data.bills = ctx.costStage.readOnly
+                                ? await ctx.service.costStageAnalysis.getReadData(ctx.costStage)
+                                : await ctx.service.costStageAnalysis.getEditData(ctx.costStage);
+                            break;
+                        case 'billsCompare':
+                            responseData.data[f] = await ctx.service.costStageAnalysis.getCompareData(ctx.costStage);
+                            break;
+                        case 'detail':
+                            responseData.data.detail =  ctx.costStage.readOnly
+                                ? await ctx.service.costStageAnalysisDetail.getReadData(ctx.costStage)
+                                : await ctx.service.costStageAnalysisDetail.getEditData(ctx.costStage);
+                            break;
+                        case 'detailCompare':
+                            // todo
+                            responseData.data.detailCompare = await ctx.service.costStageAnalysisDetail.getCompareData(ctx.costStage);
+                            break;
+                        case 'auditFlow':
+                            responseData.data[f] = await ctx.service.costStageAudit.getViewFlow(ctx.costStage);
+                            break;
+                        default:
+                            responseData.data[f] = [];
+                            break;
+                    }
+                }
 
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
         }
         async stageLoad(ctx) {
             const updateFun = `_${ctx.costStage.stage_type}Load`;
@@ -462,7 +543,7 @@ module.exports = app => {
             }
             switch (type) {
                 case 'add':
-                    return await this.ctx.service.costStageAnalysis.addSafeBillsNode(stage, data.id, data.count);
+                    return await this.ctx.service.costStageAnalysis.addAnalysisNode(stage, data.id, data.count);
                 case 'delete':
                     return await this.ctx.service.costStageAnalysis.delete(stage.id, data.id, data.count);
                 case 'up-move':
@@ -478,23 +559,30 @@ module.exports = app => {
         async _analysisUpdate(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
-                if (!data.postType || !data.postData) throw '数据错误';
+                if (!data.target) throw '数据错误';
                 const responseData = { err: 0, msg: '', data: {} };
 
-                switch (data.postType) {
-                    case 'add':
-                    case 'delete':
-                    case 'up-move':
-                    case 'down-move':
-                    case 'up-level':
-                    case 'down-level':
-                        responseData.data = await this._analysisBillsBase(ctx.costStage, data.postType, data.postData);
-                        break;
-                    case 'update':
-                        responseData.data = await this.ctx.service.costStageAnalysis.updateCalc(ctx.costStage, data.postData);
-                        break;
-                    default:
-                        throw '未知操作';
+                if (data.target === 'ledger') {
+                    if (!data.postType || !data.postData) throw '数据错误';
+                    switch (data.postType) {
+                        case 'add':
+                        case 'delete':
+                        case 'up-move':
+                        case 'down-move':
+                        case 'up-level':
+                        case 'down-level':
+                            responseData.data = await this._analysisBillsBase(ctx.costStage, data.postType, data.postData);
+                            break;
+                        case 'update':
+                            responseData.data = await this.ctx.service.costStageAnalysis.updateCalc(ctx.costStage, data.postData);
+                            break;
+                        default:
+                            throw '未知操作';
+                    }
+                } else if (data.target === 'detail') {
+                    responseData.data = await this.ctx.service.costStageAnalysisDetail.updateDatas(data.update);
+                } else if (data.target === 'importPay') {
+                    responseData.data = await this.ctx.service.costStageAnalysis.importPay(ctx.costStage, data.pay_order, data.pay_tax);
                 }
                 ctx.body = responseData;
             } catch (err) {
@@ -653,7 +741,7 @@ module.exports = app => {
             } catch (err) {
                 ctx.log(err);
                 ctx.postError(err, '上报失败');
-                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${ctx.costStage.stage_type}/${ctx.costStage.stage_order}/bills`);
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${ctx.costStage.stage_type}/${ctx.costStage.stage_order}/stage`);
             }
         }
         async stageAuditCheck(ctx) {

+ 39 - 0
app/controller/template_controller.js

@@ -61,6 +61,7 @@ module.exports = app => {
                     validColInfo: ctx.service.calcTmpl.TemplateRela.posCalc.ValidColInfo,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.template.posCalc),
                 };
+                renderData.folderList = await ctx.service.calcTmplFolder.getData(ctx.session.sessionProject.id + '-' + 'posCalc');
                 renderData.templateList = await ctx.service.calcTmpl.getAllTemplate(ctx.session.sessionProject.id, 'posCalc');
                 await ctx.service.calcTmpl.checkTemplateUsed(renderData.templateList, 'posCalc');
                 const specList = await ctx.service.stdExtraList.getList(0);
@@ -88,6 +89,44 @@ module.exports = app => {
             }
         }
 
+        async saveFolder(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const responseData = { err: 0, msg: '', data: {} };
+                if (!data.postType || !data.postData || !data.target) throw '数据错误';
+                const masterId = `${this.ctx.session.sessionProject.id}-${data.target}`;
+                switch (data.postType) {
+                    case 'add':
+                        responseData.data = await this.ctx.service.calcTmplFolder.addNode(masterId, data.postData.id);
+                        break;
+                    case 'delete':
+                        responseData.data = await this.ctx.service.calcTmplFolder.delete(masterId, data.postData.id, data.postData.count);
+                        break;
+                    case 'up-move':
+                        responseData.data = await this.ctx.service.calcTmplFolder.upMoveNode(masterId, data.postData.id, data.postData.count);
+                        break;
+                    case 'down-move':
+                        responseData.data = await this.ctx.service.calcTmplFolder.downMoveNode(masterId, data.postData.id, data.postData.count);
+                        break;
+                    case 'up-level':
+                        responseData.data = await this.ctx.service.calcTmplFolder.upLevelNode(masterId, data.postData.id, data.postData.count);
+                        break;
+                    case 'down-level':
+                        responseData.data = await this.ctx.service.calcTmplFolder.downLevelNode(masterId, data.postData.id, data.postData.count);
+                        break;
+                    case 'update':
+                        responseData.data = await this.ctx.service.calcTmplFolder.updateInfos(masterId, data.postData);
+                        break;
+                    default:
+                        throw '未知操作';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
+
         //  ------------ 以下方法为所有模板共用 --------------
         async load(ctx) {
             try {

+ 3 - 1
app/middleware/cost_stage_check.js

@@ -43,7 +43,9 @@ module.exports = options => {
             yield this.service.costStage.doCheckStage(costStage);
             costStage.latestOrder = yield this.service.costStage.count({ tid: this.tender.id, stage_type: stageType });
             costStage.isLatest = costStage.latestOrder === costStage.stage_order;
-            if (stageType === 'book') costStage.relaStage = yield this.service.costStage.getStage(costStage.rela_stage.sid);
+            if (costStage.rela_stage && costStage.rela_stage.sid)
+                costStage.relaStage = yield this.service.costStage.getStage(costStage.rela_stage.sid);
+            if (costStage.calc_template) costStage.calcTemplate = yield this.service.calcTmpl.getTemplate(costStage.calc_template);
             yield this.service.costStage.checkShenpi(costStage);
             this.costStage = costStage;
             yield next;

+ 932 - 0
app/public/js/cost_stage_analysis.js

@@ -0,0 +1,932 @@
+function getTenderId() {
+    return window.location.pathname.split('/')[2];
+}
+const invalidFields = {
+    parent: ['cur_qty', 'cur_tp', 'unit_price'],
+};
+function transExpr(expr) {
+    return $.trim(expr).replace('\t', '').replace('=', '').replace('%', '/100');
+}
+
+$(document).ready(function() {
+    autoFlashHeight();
+    class BillsObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.treeSetting = {
+                id: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                rootId: -1,
+                calcFields: ['yf_excl_tax_tp', 'in_excl_tax_tp', 'sf_excl_tax_tp', 'num_a', 'num_b', 'num_c', 'num_d', 'num_e', 'num_f', 'num_h', 'num_i', 'num_g', 'num_k', 'num_l', 'num_m', 'num_o', 'num_p'],
+                keys: ['id', 'stage_id', 'tree_id'],
+                calcFun: function(node) {
+                    node.sf_percent = node.yf_excl_tax_tp ? ZhCalc.mul(ZhCalc.div(node.sf_excl_tax_tp, node.yf_excl_tax_tp), 100 , 2) : 0;
+                }
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.spreadSetting = bllsSpreadSetting;
+            sjsSettingObj.setFxTreeStyle(this.spreadSetting, sjsSettingObj.FxTreeStyle.phasePay);
+
+            this.ckBillsSpread = window.location.pathname + '-billsSelect';
+
+            this.initSpread();
+            this.initOtherEvent();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            this.spread.bind(spreadNS.Events.topRowChanged, this.topRowChanged);
+            this.spread.bind(spreadNS.Events.ClipboardChanging, function (e, info) {
+                const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
+                SpreadJsObj.Clipboard.setCopyData(copyText);
+            });
+            if (readOnly) return;
+            this.spread.bind(spreadNS.Events.EditEnded, this.editEnded);
+            this.spread.bind(spreadNS.Events.EditStarting, this.editStarting);
+            this.spread.bind(spreadNS.Events.ClipboardPasting, this.clipboardPasting);
+            SpreadJsObj.addDeleteBind(this.spread, this.deletePress);
+        }
+        initOtherEvent() {
+            const self = this;
+            // 增删上下移升降级
+            $('a[name="base-opr"]').click(function () {
+                self.baseOpr(this.getAttribute('type'));
+            });
+        }
+        refreshOperationValid() {
+            const setObjEnable = function (obj, enable) {
+                if (enable) {
+                    obj.removeClass('disabled');
+                } else {
+                    obj.addClass('disabled');
+                }
+            };
+            const invalidAll = function () {
+                setObjEnable($('a[name=base-opr][type=add]'), false);
+                setObjEnable($('a[name=base-opr][type=delete]'), false);
+                setObjEnable($('a[name=base-opr][type=up-move]'), false);
+                setObjEnable($('a[name=base-opr][type=down-move]'), false);
+                setObjEnable($('a[name=base-opr][type=up-level]'), false);
+                setObjEnable($('a[name=base-opr][type=down-level]'), false);
+            };
+            const sel = this.sheet.getSelections()[0];
+            const row = sel ? sel.row : -1;
+            const tree = this.sheet.zh_tree;
+            if (!tree) {
+                invalidAll();
+                return;
+            }
+            const first = tree.nodes[row];
+            if (!first) {
+                invalidAll();
+                return;
+            }
+            let last = first, sameParent = true, nodeUsed = this.checkNodeUsed(tree, first);
+            if (sel.rowCount > 1 && first) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = tree.nodes[sel.row + r];
+                    if (!rNode) {
+                        sameParent = false;
+                        break;
+                    }
+                    nodeUsed = nodeUsed || this.checkNodeUsed(tree, rNode);
+                    if (rNode.tree_level > first.tree_level) continue;
+                    if ((rNode.tree_level < first.tree_level) || (rNode.tree_level === first.tree_level && rNode.tree_pid !== first.tree_pid)) {
+                        sameParent = false;
+                        break;
+                    }
+                    last = rNode;
+                }
+            }
+            const preNode = tree.getPreSiblingNode(first);
+            const valid = !this.sheet.zh_setting.readOnly;
+            const topParent = tree.getTopParent(first);
+            const isFixed = first.tree_level === 1 || [2,3,4].indexOf(topParent.node_type) >= 0;
+
+            setObjEnable($('a[name=base-opr][type=add]'), valid && first && !isFixed);
+            setObjEnable($('a[name=base-opr][type=delete]'), valid && first && sameParent && !isFixed && !first.node_type);
+            setObjEnable($('a[name=base-opr][type=up-move]'), valid && first && sameParent && preNode && !isFixed);
+            setObjEnable($('a[name=base-opr][type=down-move]'), valid && first && sameParent  && !tree.isLastSibling(last) && !isFixed);
+            setObjEnable($('a[name=base-opr][type=up-level]'), valid && first && sameParent && tree.getParent(first) && !nodeUsed && first.tree_level > 2 && !isFixed);
+            setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && sameParent && preNode && !isFixed && !preNode.node_type);
+        }
+        loadRelaData() {
+            this.refreshOperationValid();
+            SpreadJsObj.saveTopAndSelect(this.sheet, this.ckBillsSpread);
+            const select = SpreadJsObj.getSelectObject(this.sheet);
+            detailObj.loadDetailData(select);
+        }
+        refreshTree(data) {
+            const sheet = this.sheet;
+            SpreadJsObj.massOperationSheet(sheet, function () {
+                const tree = sheet.zh_tree;
+                // 处理删除
+                if (data.delete) {
+                    data.delete.sort(function (a, b) {
+                        return b.deleteIndex - a.deleteIndex;
+                    });
+                    for (const d of data.delete) {
+                        sheet.deleteRows(d.deleteIndex, 1);
+                    }
+                }
+                // 处理新增
+                if (data.create) {
+                    const newNodes = data.create;
+                    if (newNodes) {
+                        newNodes.sort(function (a, b) {
+                            return a.index - b.index;
+                        });
+
+                        for (const node of newNodes) {
+                            sheet.addRows(node.index, 1);
+                            SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(node), 1);
+                        }
+                    }
+                }
+                // 处理更新
+                if (data.update) {
+                    const rows = [];
+                    for (const u of data.update) {
+                        rows.push(tree.nodes.indexOf(u));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    const expanded = [];
+                    for (const e of data.expand) {
+                        if (expanded.indexOf(e) === -1) {
+                            const posterity = tree.getPosterity(e);
+                            for (const p of posterity) {
+                                sheet.setRowVisible(tree.nodes.indexOf(p), p.visible);
+                                expanded.push(p);
+                            }
+                        }
+                    }
+                }
+            });
+        }
+        loadData(datas) {
+            this.tree.loadDatas(datas);
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.tree);
+            SpreadJsObj.loadTopAndSelect(this.sheet, this.ckBillsSpread);
+            this.refreshOperationValid();
+        }
+        getDefaultSelectInfo() {
+            if (!this.tree) return;
+            const sel = this.sheet.getSelections()[0];
+            const node = this.sheet.zh_tree.nodes[sel.row];
+            if (!node) return;
+            let count = 1;
+            if (sel.rowCount > 1) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = this.sheet.zh_tree.nodes[sel.row + r];
+                    if (rNode.tree_level > node.tree_level) continue;
+                    if ((rNode.tree_level < node.tree_level) || (rNode.tree_level === node.tree_level && rNode.tree_pid !== node.tree_pid)) {
+                        toastr.warning('请选择同一节点下的节点,进行该操作');
+                        return;
+                    }
+                    count += 1;
+                }
+            }
+            return [this.tree, node, count];
+        }
+        checkNodeUsed(tree, node) {
+            if (node.pre_pay_tp || node.pre_cut_tp || node.pre_yf_tp || node.pre_sf_tp) return true;
+            const posterity = tree.getPosterity(node);
+            for (const p of posterity) {
+                if (p.pre_pay_tp || p.pre_cut_tp || p.pre_yf_tp || p.pre_sf_tp) return true;
+            }
+            return false;
+        }
+        baseOpr(type, addCount = 1) {
+            const self = this;
+            const sheet = self.sheet;
+            const sel = sheet.getSelections()[0];
+            const [tree, node, count] = this.getDefaultSelectInfo();
+            if (!tree || !node || !count) return;
+
+            if (type === 'delete') {
+                const parent = tree.getParent(node);
+                const children = parent ? parent.children : tree.children;
+                const index = children.indexOf(node);
+                for (let i = 0; i < count; i++) {
+                    const child = children[i+index];
+                    if (this.checkNodeUsed(tree, child)) {
+                        toastr.warning('选中的节点已使用,不可删除');
+                        return;
+                    }
+                }
+            } else if (type === 'up-level') {
+                const parent = tree.getParent(node);
+                const children = parent ? parent.children : tree.children;
+                const index = children.indexOf(node);
+                for (let i = index; i < children.length; i++) {
+                    const child = children[index];
+                    if (this.checkNodeUsed(tree, child)) {
+                        if (i >= index + count) {
+                            toastr.warning('其后节点已使用,选中的节点不可升级');
+                        } else {
+                            toastr.warning('选中的节点已使用,不可升级');
+                        }
+                        return;
+                    }
+                }
+            } else if (type === 'down-level') {
+                const parent = tree.getParent(node);
+                const children = parent ? parent.children : tree.children;
+                const index = children.indexOf(node);
+                if (index > 0 && this.checkNodeUsed(tree, children[index-1])) {
+                    toastr.warning('其前节点已使用,选中的节点不可降级');
+                    return;
+                }
+                for (let i = index; i < count; i++) {
+                    const child = children[i+index];
+                    if (this.checkNodeUsed(tree, child)) {
+                        toastr.warning('选中的节点已使用,不可降级');
+                        return;
+                    }
+                }
+            }
+
+            const updateData = {
+                target: 'ledger', postType: type,
+                postData: {
+                    id: node.tree_id,
+                    count: type === 'add' ? addCount : count,
+                }
+            };
+            if (type === 'delete') {
+                deleteAfterHint(function () {
+                    postData('update', updateData, function (result) {
+                        const refreshData = tree.loadPostData(result);
+                        self.refreshTree(refreshData);
+                        if (sel) {
+                            sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
+                        }
+                        self.refreshOperationValid();
+                    });
+                });
+            } else {
+                postData('update', updateData, function (result) {
+                    const refreshData = tree.loadPostData(result);
+                    self.refreshTree(refreshData);
+                    if (['up-move', 'down-move'].indexOf(type) > -1) {
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
+                        }
+                    } else if (type === 'add') {
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
+                        }
+                    }
+                    self.refreshOperationValid();
+                });
+            }
+        }
+        // 事件
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    billsObj.loadRelaData();
+                }
+            }
+        }
+        topRowChanged(e, info) {
+            SpreadJsObj.saveTopAndSelect(info.sheet, billsObj.ckBillsSpread);
+        }
+        editStarting(e, info) {
+            if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+            const tree = info.sheet.zh_tree;
+            const col = info.sheet.zh_setting.cols[info.col];
+            const node = info.sheet.zh_tree.nodes[info.row];
+            if (!node) {
+                info.cancel = true;
+                return;
+            }
+            const topParent = tree.getTopParent(node);
+
+            switch (col.field) {
+                case 'code':
+                case 'name':
+                case 'yf_excl_tax_tp':
+                case 'in_excl_tax_tp':
+                case 'sf_excl_tax_tp':
+                    info.cancel = node.tree_level === 1 || node.node_type > 0  || [2,3,4].indexOf(topParent.node_type) >= 0;
+                    break;
+                case 'num_a':
+                case 'num_b':
+                case 'num_c':
+                case 'num_d':
+                case 'num_e':
+                case 'num_f':
+                case 'num_g':
+                case 'num_h':
+                case 'num_i':
+                case 'num_j':
+                case 'num_k':
+                case 'num_l':
+                case 'num_m':
+                case 'num_n':
+                case 'num_o':
+                case 'num_p':
+                    info.cancel = node.children && node.children.length > 0;
+                    if (!info.cancel) {
+                        const detailRange = detailObj.data.getPartData(node.id);
+                        info.cancel = detailRange && detailRange.length > 0;
+                    }
+                    break;
+            }
+        }
+        editEnded(e, info) {
+            if (!info.sheet.zh_setting) return;
+
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            const data = { id: node.id, stage_id: node.stage_id, tree_id: node.tree_id };
+            // 未改变值则不提交
+            const col = info.sheet.zh_setting.cols[info.col];
+            const orgValue = node[col.field];
+            const newValue = trimInvalidChar(info.editingText);
+            if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) return;
+
+            if (info.editingText) {
+                const text = newValue;
+                if (col.type === 'Number') {
+                    const num = _.toNumber(text);
+                    if (_.isFinite(num)) {
+                        data[col.field] = num;
+                    } else {
+                        try {
+                            data[col.field] = ZhCalc.mathCalcExpr(transExpr(text));
+                        } catch(err) {
+                            toastr.error('输入的表达式非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                    }
+                } else {
+                    data[col.field] = text;
+                }
+            } else {
+                data[col.field] = col.type === 'Number' ? 0 : '';
+            }
+            // 更新至服务器
+            postData('update', {target: 'ledger', postType: 'update', postData: data}, function (result) {
+                const refreshNode = billsObj.tree.loadPostData(result);
+                billsObj.refreshTree(refreshNode);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            });
+        }
+        deletePress (sheet) {
+            if (!sheet.zh_setting) return;
+            const sel = sheet.getSelections()[0], datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                let bDel = false;
+                const node = sheet.zh_tree.nodes[iRow];
+                const data = sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    const col = sheet.zh_setting.cols[iCol];
+                    const style = sheet.getStyle(iRow, iCol);
+                    if (style.locked) continue;
+
+                    data[col.field] = col.type === 'Number' ? 0 : '';
+                    bDel = true;
+                }
+                if (bDel) datas.push(data);
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(sheet, sel.row, sel.rowCount);
+                });
+            }
+        }
+        clipboardPasting(e, info) {
+            info.cancel = true;
+            const tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
+            if (!setting || !tree) return;
+
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            const datas = [], filterNodes = [];
+
+            let level, filterRow = 0;
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = tree.nodes[curRow];
+                if (!node) continue;
+
+                if (!level) level = node.level;
+                if (node.level < level) break;
+
+                let bPaste = false;
+                const data = info.sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    data[colSetting.field] = trimInvalidChar(pasteData[iRow-filterRow][iCol]);
+                    bPaste = true;
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
+                }
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) refreshNode.update = refreshNode.update.concat(filterNodes);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        }
+    }
+    const billsObj = new BillsObj();
+    class DetailObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#detail-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.data = createAncillaryGcl({ id: 'id', masterId: 'ledger_id', sort: [['d_order', 'asc']] });
+            this.dealSpreadSetting = dealSpreadSetting;
+            this.commonSpreadSetting = commonSpreadSetting;
+
+            this.initSpread();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.commonSpreadSetting);
+            this.spread.bind(spreadNS.Events.ClipboardChanging, function (e, info) {
+                const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
+                SpreadJsObj.Clipboard.setCopyData(copyText);
+            });
+            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);
+        }
+        loadData(datas) {
+            this.data.loadDatas(datas);
+            this.loadDetailData(SpreadJsObj.getSelectObject(billsObj.sheet));
+        }
+        refreshSheet() {
+            if (this.sheet.zh_data.length === 0) {
+                this.reloadDetailData();
+            } else {
+                SpreadJsObj.reLoadSheetData(this.sheet);
+            }
+        }
+        reloadDetailData() {
+            const data = this.billsNode ? this.data.getPartData(this.billsNode.id) || [] : [];
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, data);
+        }
+        loadDetailData(bills) {
+            detailObj.billsNode = bills;
+            const data = this.billsNode ? this.data.getPartData(this.billsNode.id) || [] : [];
+            const spreadSetting = data && data.length > 0 && data[0].is_deal ? this.dealSpreadSetting : this.commonSpreadSetting;
+            if (data.length === 0) {
+                spreadSetting.readOnly = true;
+            } else {
+                spreadSetting.readOnly = readOnly;
+            }
+            SpreadJsObj.initSheet(this.sheet, spreadSetting);
+            this.reloadDetailData();
+        }
+        // 事件
+        editEnded(e, info) {
+            const setting = info.sheet.zh_setting;
+            if (!setting) return;
+            const detailData = SpreadJsObj.getSelectObject(info.sheet);
+
+            const col = setting.cols[info.col];
+            const orgText = detailData ? detailData[col.field] : '', newText = trimInvalidChar(info.editingText);
+            if (orgText === newText || (!orgText && !newText)) return;
+
+            const relaBills = detailObj.billsNode;
+            if (!relaBills) {
+                toastr.error('数据错误,请选择台账节点后再试');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            }
+
+            const data = { target: 'detail' };
+            if (detailData) {
+                const updateData = { id: detailData.id };
+                if (col.type === 'Number') {
+                    const num = _.toNumber(newText);
+                    if (!_.isFinite(num)) {
+                        toastr.error('输入的数字非法');
+                        SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                        return;
+                    }
+                    updateData[col.field] = num;
+                } else {
+                    updateData[col.field] = newText;
+                }
+                data.update = [ updateData ];
+            }
+            // 更新至服务器
+            postData('update', data, function (result) {
+                detailObj.data.updateDatas(result.detail);
+                detailObj.refreshSheet();
+                const refreshNode = billsObj.tree.loadPostData({ update: result.ledger });
+                billsObj.refreshTree(refreshNode);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            });
+        }
+        deletePress (sheet) {
+            if (!sheet.zh_setting) return;
+            const relaBills = detailObj.billsNode;
+            if (!relaBills) {
+                toastr.error('数据错误,请选择台账节点后再试');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            }
+
+            const sel = sheet.getSelections()[0], datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                let bDel = false;
+                const node = sheet.zh_data[iRow];
+                const data = { id: node.id };
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    const col = sheet.zh_setting.cols[iCol];
+                    const style = sheet.getStyle(iRow, iCol);
+                    if (style.locked) continue;
+
+                    data[col.field] = col.type === 'Number' ? 0 : '';
+                    bDel = true;
+                }
+                if (bDel) datas.push(data);
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'detail', update: datas}, function (result) {
+                    detailObj.data.updateDatas(result.detail);
+                    detailObj.refreshSheet();
+                    const refreshNode = billsObj.tree.loadPostData({ update: result.ledger });
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, sel.row, sel.rowCount);
+                });
+            }
+        }
+        clipboardPasting(e, info) {
+            info.cancel = true;
+
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            const hint = {
+                invalidExpr: {type: 'warning', msg: '粘贴的表达式非法'},
+            };
+            const datas = [], filterNodes = [];
+
+            // todo
+            let filterRow = 0;
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = info.sheet.zh_data[curRow];
+                if (!node) continue;
+
+                let bPaste = false;
+                const data = { id: node.id };
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    const value = trimInvalidChar(pasteData[iRow-filterRow][iCol]);
+                    if (colSetting.type === 'Number') {
+                        const num = _.toNumber(value);
+                        if (num) {
+                            data[colSetting.field] = num;
+                        } else {
+                            try {
+                                data[colSetting.field] = ZhCalc.mathCalcExpr(transExpr(value));
+                                bPaste = true;
+                            } catch (err) {
+                                toastMessageUniq(hint.invalidExpr);
+                                continue;
+                            }
+                        }
+                    } else {
+                        data[colSetting.field] = value;
+                    }
+                    bPaste = true;
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
+                }
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'detail', update: datas}, function (result) {
+                    detailObj.data.updateDatas(result.detail);
+                    detailObj.refreshSheet();
+                    const refreshNode = billsObj.tree.loadPostData({ update: result.ledger });
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        }
+    }
+    const detailObj = new DetailObj();
+
+    // 清单右键菜单
+    const billsContextMenuOptions = {
+        selector: '#bills-spread',
+        build: function ($trigger, e) {
+            const target = SpreadJsObj.safeRightClickSelection($trigger, e, billsObj.spread);
+            billsObj.loadRelaData();
+            return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+        },
+        items: {}
+    };
+    if (!readOnly) {
+        $.contextMenu.types.batchInsert = function (item, opt, root) {
+            const self = this;
+            if ($.isFunction(item.icon)) {
+                item._icon = item.icon.call(this, this, $t, key, item);
+            } else {
+                if (typeof(item.icon) === 'string' && item.icon.substring(0, 3) === 'fa-') {
+                    // to enable font awesome
+                    item._icon = root.classNames.icon + ' ' + root.classNames.icon + '--fa fa ' + item.icon;
+                } else {
+                    item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon;
+                }
+            }
+            this.addClass(item._icon);
+            const $obj = $('<div>' + item.name + '<input class="text-right ml-1 mr-1" type="tel" max="100" min="1" value="' + item.value + '" style="width: 30px; height: 18px; padding-right: 4px;">行</div>')
+                .appendTo(this);
+            const $input = $obj.find('input');
+            const event = () => {
+                if (self.hasClass('context-menu-disabled')) return;
+                item.batchInsert($input[0], root);
+            };
+            $obj.on('click', event).keypress(function (e) {if (e.keyCode === 13) { event(); }});
+            $input.click((e) => {e.stopPropagation();})
+                .keyup((e) => {if (e.keyCode === 13) item.batchInsert($input[0], root);})
+                .on('input', function () {this.value = this.value.replace(/[^\d]/g, '');});
+        };
+        billsContextMenuOptions.items.create = {
+            name: '新增',
+            icon: 'fa-sign-in',
+            callback: function (key, opt) {
+                billsObj.baseOpr('add');
+            },
+            disabled: function (key, opt) {
+                const sheet = billsObj.sheet;
+                const selection = sheet.getSelections();
+                const sel = selection ? selection[0] : sheet.getSelections()[0];
+                const row = sel ? sel.row : -1;
+                const tree = sheet.zh_tree;
+                if (!tree) return true;
+                const first = sheet.zh_tree.nodes[row];
+                const valid = !sheet.zh_setting.readOnly;
+                return !(valid && first);
+            }
+        };
+        billsContextMenuOptions.items.delete = {
+            name: '删除',
+            icon: 'fa-remove',
+            callback: function (key, opt) {
+                billsObj.baseOpr('delete');
+            },
+            disabled: function (key, opt) {
+                const sheet = billsObj.sheet;
+                const selection = sheet.getSelections();
+                const sel = selection ? selection[0] : sheet.getSelections()[0];
+                const row = sel ? sel.row : -1;
+                const tree = sheet.zh_tree;
+                if (!tree) return true;
+                const first = sheet.zh_tree.nodes[row];
+                let last = first, sameParent = true, nodeUsed = billsObj.checkNodeUsed(tree, first);
+                if (sel.rowCount > 1 && first) {
+                    for (let r = 1; r < sel.rowCount; r++) {
+                        const rNode = tree.nodes[sel.row + r];
+                        if (!rNode) {
+                            sameParent = false;
+                            break;
+                        }
+                        nodeUsed = nodeUsed || billsObj.checkNodeUsed(tree, rNode);
+                        if (rNode.tree_level > first.tree_level) continue;
+                        if ((rNode.tree_level < first.tree_level) || (rNode.tree_level === first.tree_level && rNode.tree_pid !== first.tree_pid)) {
+                            sameParent = false;
+                            break;
+                        }
+                        last = rNode;
+                    }
+                }
+                const valid = !sheet.zh_setting.readOnly;
+                return !(valid && first && sameParent && !nodeUsed);
+            }
+        };
+        billsContextMenuOptions.items.batchInsert = {
+            name: '批量插入',
+            type: 'batchInsert',
+            value: '2',
+            icon: 'fa-sign-in',
+            batchInsert: function (obj, root) {
+                if (_.toNumber(obj.value) > _.toNumber(obj.max)) {
+                    obj.value = obj.max;
+                    toastr.warning('批量插入不可多于' + obj.max);
+                } else if(_.toNumber(obj.value) < _.toNumber(obj.min)) {
+                    obj.value = obj.min;
+                    toastr.warning('批量插入不可少于' + obj.min);
+                } else {
+                    billsObj.baseOpr('add', parseInt(obj.value));
+                    root.$menu.trigger('contextmenu:hide');
+                }
+            },
+            disabled: function (key, opt) {
+                const sheet = billsObj.sheet;
+                const selection = sheet.getSelections();
+                const sel = selection ? selection[0] : sheet.getSelections()[0];
+                const row = sel ? sel.row : -1;
+                const tree = sheet.zh_tree;
+                if (!tree) return true;
+                const first = sheet.zh_tree.nodes[row];
+                const valid = !sheet.zh_setting.readOnly;
+                return !(valid && first);
+            },
+            visible: function (key, opt) {
+                return !readOnly;
+            }
+        };
+        billsContextMenuOptions.items.baseSpr = '----';
+        billsContextMenuOptions.items.importDealPay = {
+            name: '导入计量数据',
+            callback: function() {
+                const node = SpreadJsObj.getSelectObject(billsObj.sheet);
+                $('#pay-tax').val(node.tax || '');
+                $('#import-pay').modal('show');
+            },
+            disabled: function() {
+                const node = SpreadJsObj.getSelectObject(billsObj.sheet);
+                return !node || (node.children && node.children.length > 0) || node.node_type !== 1;
+            }
+        };
+        $('#import-pay-ok').click(function() {
+            const updateData = { target: 'importPay', pay_order: $('#pay-order').val(), pay_tax: parseFloat($('#pay-tax').val()) };
+            if (updateData.pay_tax < 0 || updateData.pay_tax > 100) {
+                toastr.warning('税率需在0-100以内');
+                return;
+            }
+            postData('update', updateData, function (result) {
+                const refreshNode = billsObj.tree.loadPostData(result);
+                billsObj.refreshTree(refreshNode);
+                $('#import-pay').modal('hide');
+            });
+        })
+    }
+    $.contextMenu(billsContextMenuOptions);
+
+    const searchObj = $.ledgerSearch({
+        selector: '#search',
+        searchRangeStr: '编号/名称',
+        ledger: {
+            billsTree: billsObj.tree,
+            getLedgerPos: function(node) { return detailObj.data.getPartData(node.id); },
+            posId: 'id',
+        },
+        resultSpreadSetting: {
+            cols: [
+                {title: '编号', field: 'code', hAlign: 0, width: 90, formatter: '@'},
+                {title: '名称', field: 'name', width: 150, hAlign: 0, formatter: '@'},
+                {title: '单位', field: 'unit', width: 50, hAlign: 1, formatter: '@'},
+            ],
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            headColWidth: [30],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            selectedBackColor: '#fffacd',
+            readOnly: true,
+        },
+        locate: function(cur) {
+            if (!cur.lid) return;
+
+            SpreadJsObj.locateTreeNode(billsObj.sheet, cur.lid, true);
+            billsObj.loadRelaData();
+            if (cur.pid) {
+                const pIndex = detailObj.sheet.zh_data.findIndex(x => { return x.id === cur.pid; });
+                SpreadJsObj.locateRow(detailObj.sheet, pIndex);
+            }
+        },
+    });
+    // 加载数据
+    postData('load', { filter: 'bills;detail;att;tags' }, function(result) {
+        billsObj.loadData(result.bills);
+        detailObj.loadData(result.detail);
+    });
+
+    // 展开收起标准清单
+    $('a', '#side-menu').bind('click', function (e) {
+        e.preventDefault();
+        const tab = $(this), tabPanel = $(tab.attr('content'));
+        // 展开工具栏、切换标签
+        if (!tab.hasClass('active')) {
+            // const close = $('.active', '#side-menu').length === 0;
+            $('a', '#side-menu').removeClass('active');
+            $('.tab-content .tab-select-show.tab-pane.active').removeClass('active');
+            tab.addClass('active');
+            tabPanel.addClass('active');
+            // $('.tab-content .tab-pane').removeClass('active');
+            showSideTools(tab.hasClass('active'));
+            if (tab.attr('content') === '#search') {
+                searchObj.spread.refresh();
+            }
+        } else { // 收起工具栏
+            tab.removeClass('active');
+            tabPanel.removeClass('active');
+            showSideTools(tab.hasClass('active'));
+        }
+        billsObj.spread.refresh();
+        detailObj.spread.refresh();
+    });
+
+    $.divResizer({
+        select: '#detail-spr',
+        callback: function () {
+            billsObj.spread.refresh();
+            detailObj.spread.refresh();
+        }
+    });
+    // 工具栏spr
+    $.divResizer({
+        select: '#right-spr',
+        callback: function () {
+            billsObj.spread.refresh();
+            detailObj.spread.refresh();
+        }
+    });
+    // 导航Menu
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+            billsObj.spread.refresh();
+            detailObj.spread.refresh();
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            setTimeout(() => {
+                showWaitingView();
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', billsObj.sheet);
+});

+ 3 - 4
app/public/js/cost_stage_book.js

@@ -503,7 +503,7 @@ $(document).ready(function() {
 
             const data = { target: 'detail' };
             if (detailData) {
-                const updateData = { book_id: detailData.book_id, detail_id: detailData.id };
+                const updateData = { book_id: detailData.book_id, detail_id: detailData.id, ledger_id: detailData.ledger_id };
                 if (col.type === 'Number') {
                     const num = _.toNumber(newText);
                     if (!_.isFinite(num)) {
@@ -534,7 +534,7 @@ $(document).ready(function() {
             for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
                 let bDel = false;
                 const node = sheet.zh_tree.nodes[iRow];
-                const data = { book_id: node.book_id, detail_id: node.id };
+                const data = { book_id: node.book_id, detail_id: node.id, ledger_id: node.ledger_id };
                 for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
                     const col = sheet.zh_setting.cols[iCol];
                     const style = sheet.getStyle(iRow, iCol);
@@ -567,7 +567,6 @@ $(document).ready(function() {
             };
             const datas = [], filterNodes = [];
 
-            // todo
             let filterRow = 0;
             for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
                 const curRow = info.cellRange.row + iRow;
@@ -575,7 +574,7 @@ $(document).ready(function() {
                 if (!node) continue;
 
                 let bPaste = false;
-                const data = { book_id: node.book_id, detail_id: node.id };
+                const data = { book_id: node.book_id, detail_id: node.id, ledger_id: node.ledger_id };
                 for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
                     const curCol = info.cellRange.col + iCol;
                     const colSetting = info.sheet.zh_setting.cols[curCol];

+ 17 - 3
app/public/js/cost_stage_ledger.js

@@ -960,12 +960,18 @@ $(document).ready(function() {
                     SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
                 }
             });
+            const typePayDate =  $('#type-pay-date').datepicker({}).data('datepicker');
+            const selectPayDate =  $('#type-pay-date').datepicker({}).data('datepicker');
             $('#import-deal-type-ok').click(function() {
-                const updateData = { target: 'importContract', ledger_id: curNode.id, cost_id: curNode.cost_id, types: $('[name=contract_type]').val() };
+                const updateData = { target: 'importContract', ledger_id: curNode.id, cost_id: curNode.cost_id, types: $('[name=contract_type]').val(), months: $('#type-pay-date').val() };
                 if (updateData.types.length === 0) {
                     toastr.warning('请至少选择一种类型');
                     return;
                 }
+                if (!updateData.months) {
+                    toastr.warning('请选择导入合同的支付年月范围');
+                    return;
+                }
                 postData('update', updateData, function(result) {
                     detailObj.data.updateDatas(result.detail);
                     detailObj.reloadDetailData();
@@ -977,6 +983,8 @@ $(document).ready(function() {
             });
             const importByType = function(node) {
                 curNode = node;
+                typePayDate.clear();
+                typePayDate.selectDate(moment(stageDate, 'YYYY-MM').toDate());
                 $('#import-deal-type').modal('show');
             };
             const loadContractTree = function() {
@@ -994,11 +1002,15 @@ $(document).ready(function() {
                 contractSelectSpread.refresh();
             });
             $('#import-deal-select-ok').click(function() {
-                const updateData = { target: 'importContract', ledger_id: curNode.id, cost_id: curNode.cost_id, ids: getSelectContractId() };
+                const updateData = { target: 'importContract', ledger_id: curNode.id, cost_id: curNode.cost_id, ids: getSelectContractId(), months: $('#select-pay-date').val() };
                 if (updateData.ids.length === 0) {
                     toastr.warning('请至少选择一项合同数据');
                     return;
                 }
+                if (!updateData.months) {
+                    toastr.warning('请选择导入合同的支付年月范围');
+                    return;
+                }
                 postData('update', updateData, function(result) {
                     detailObj.data.updateDatas(result.detail);
                     detailObj.refreshSheet();
@@ -1011,7 +1023,9 @@ $(document).ready(function() {
             const importBySelect = function(node) {
                 curNode = node;
                 $('#import-deal-select').modal('show');
-                 if (!contractTree) loadContractTree();
+                selectPayDate.clear();
+                selectPayDate.selectDate(moment(stageDate, 'YYYY-MM').toDate());
+                if (!contractTree) loadContractTree();
             };
             return {importByType, importBySelect};
         })();

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

@@ -230,6 +230,71 @@ $(document).ready(() => {
                     return (target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader) && !self.readOnly && self.template;
                 },
                 items: {
+                    'add_code': {
+                        name: '新增编号列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('code', select);
+                        },
+                    },
+                    'add_name': {
+                        name: '新增名称列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('name', select);
+                        },
+                    },
+                    'add_party_b': {
+                        name: '新增乙方列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('party_b', select);
+                        },
+                    },
+                    'add_yf_excl_tax_tp': {
+                        name: '新增应付(不含税)列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('yf_excl_tax_tp', select);
+                        },
+                    },
+                    'add_in_excl_tax_tp': {
+                        name: '新增入账(不含税)列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('in_excl_tax_tp', select);
+                        },
+                    },
+                    'add_sf_excl_tax_tp': {
+                        name: '新增实付(不含税)列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('in_excl_tax_tp', select);
+                        },
+                    },
+                    'add_sf_percent': {
+                        name: '新增实付(不含税)列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('sf_percent', select);
+                        },
+                    },
+                    'add_postil': {
+                        name: '新增本期批注列',
+                        icon: 'fa-plus',
+                        callback: function (key, opt) {
+                            const select = SpreadJsObj.getSelectObject(self.sheet);
+                            self.addCol('postil', select);
+                        },
+                    },
+                    addSpecSpr: '----',
                     'add_str': {
                         name: '新增文本列',
                         icon: 'fa-plus',

+ 133 - 13
app/public/js/ledger.js

@@ -17,6 +17,7 @@ function getTenderId() {
 }
 
 const copyBlockTag = 'zh.calc.copyBlock';
+const copyBlockTagPos = 'zh.calc.copyBlockPos';
 const invalidFields = {
     parent: ['sgfh_qty', 'sgfh_tp', 'sjcl_qty', 'sjcl_tp', 'qtcl_qty', 'qtcl_tp', 'deal_qty', 'deal_tp', 'unit_price', 'ex_qty1'],
     gcl: ['dgn_qty1', 'dgn_qty2'],
@@ -187,7 +188,7 @@ $(document).ready(function() {
                     treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet());
                 }
             },
-            baseOpr: function (type) {
+            baseOpr: function (type, addCount = 1) {
                 const data = {};
 
                 const detailRange = sheet.zh_data;
@@ -200,7 +201,10 @@ $(document).ready(function() {
                 const first = detailRange[row];
                 if (type === 'insert') {
                     const node = SpreadJsObj.getSelectObject(posSheet);
-                    data.add = [{ lid: node.lid, pid: node.id, pcd_order: detailRange.length + 1 }];
+                    data.add = [];
+                    for (let i = 1; i <= addCount; i++) {
+                        data.add.push({ lid: node.lid, pid: node.id, pcd_order: detailRange.length + i });
+                    }
                 } else if (type === 'delete') {
                     data.del = [];
                     for (let iRow = 0; iRow < count; iRow++) {
@@ -274,11 +278,16 @@ $(document).ready(function() {
                     if (col.type === 'Number') {
                         const num = _.toNumber(newText);
                         if (!_.isFinite(num)) {
-                            toastr.error('输入的数字非法');
-                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
-                            return;
+                            try {
+                                updateData[col.field] = ZhCalc.mathCalcExpr(transExpr(newText));
+                            } catch (err) {
+                                toastr.error('输入的表达式非法');
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                                return;
+                            }
+                        } else {
+                            updateData[col.field] = num;
                         }
-                        updateData[col.field] = num;
                     } else {
                         updateData[col.field] = newText;
                     }
@@ -290,11 +299,16 @@ $(document).ready(function() {
                     if (col.type === 'Number') {
                         const num = _.toNumber(newText);
                         if (!_.isFinite(num)) {
-                            toastr.error('输入的数字非法');
-                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
-                            return;
+                            try {
+                                addData[col.field] = ZhCalc.mathCalcExpr(transExpr(newText));
+                            } catch (err) {
+                                toastr.error('输入的表达式非法');
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                                return;
+                            }
+                        } else {
+                            addData[col.field] = num;
                         }
-                        addData[col.field] = num;
                     } else {
                         addData[col.field] = newText;
                     }
@@ -361,10 +375,16 @@ $(document).ready(function() {
                         if (col.type === 'Number') {
                             const num = _.toNumber(value);
                             if (!_.isFinite(num)) {
-                                toastMessageUniq(hint.num);
-                                return;
+                                try {
+                                    targetData[col.field] = ZhCalc.mathCalcExpr(transExpr(value));
+                                } catch (err) {
+                                    toastr.error('输入的表达式非法');
+                                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                                    return;
+                                }
+                            } else {
+                                targetData[col.field] = num;
                             }
-                            targetData[col.field] = num;
                         } else {
                             targetData[col.field] = value;
                         }
@@ -417,6 +437,28 @@ $(document).ready(function() {
                             ctrlObj.baseOpr('insert');
                         }
                     },
+                    'batchInsert': {
+                        name: '批量插入',
+                        type: 'batchInsert',
+                        value: '2',
+                        icon: 'fa-plus',
+                        disabled: function (key, opt) {
+                            const pos = SpreadJsObj.getSelectObject(posSheet);
+                            return !pos;
+                        },
+                        batchInsert: function (obj, root) {
+                            if (_.toNumber(obj.value) > _.toNumber(obj.max)) {
+                                obj.value = obj.max;
+                                toastr.warning('批量插入不可多于' + obj.max);
+                            } else if(_.toNumber(obj.value) < _.toNumber(obj.min)) {
+                                obj.value = obj.min;
+                                toastr.warning('批量插入不可少于' + obj.min);
+                            } else {
+                                ctrlObj.baseOpr('insert', parseInt(obj.value));
+                                root.$menu.trigger('contextmenu:hide');
+                            }
+                        },
+                    },
                     'delete': {
                         name: '删除',
                         icon: 'fa-remove',
@@ -2869,6 +2911,49 @@ $(document).ready(function() {
                 posOperationObj.loadCurPosData();
                 treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet());
             });
+        },
+        getBlockData: function() {
+            const copyBlockList = [];
+            const sheet = posSpread.getActiveSheet();
+            const sel = sheet.getSelections()[0];
+            let iRow = sel.row;
+            const node = SpreadJsObj.getSelectObject(ledgerSheet);
+            while (iRow < sel.row + sel.rowCount) {
+                const p = sheet.zh_data[iRow];
+                if (!p) break;
+                copyBlockList.push(p);
+                if (node.calc_template) {
+                    const detailRange = posCalcDetail.detail.getPartData(p.id);
+                    p.calcDetail = detailRange;
+                }
+                iRow++;
+            }
+            return copyBlockList;
+        },
+        pasteBlock: function(copyInfo) {
+            const node = SpreadJsObj.getSelectObject(ledgerSheet);
+            if (!node) {
+                toastr.error('数据错误,请选择台账节点后再试');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            } else if (node.children && node.children.length > 0) {
+                toastr.error('父节点不可插入计量单元');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            }else if (!node.b_code || node.b_code === '') {
+                toastr.error('项目节不可插入计量单元');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            }
+            const data = { updateType: 'paste-block', updateData: { lid: node.id, tid: copyInfo.tid, block: copyInfo.block } };
+            postData('/tender/' + getTenderId() + '/pos/update', data, function (result) {
+                pos.updateDatas(result.pos);
+                if (result.posCalcDetail) posCalcDetail.detail.updateDatas(result.posCalcDetail);
+                const ledgerResult = ledgerTree.loadPostData(data.ledger);
+                treeOperationObj.refreshTree(ledgerSheet, ledgerResult);
+                removeLocalCache(copyBlockTag);
+                posOperationObj.loadCurPosData();
+            });
         }
     };
     posSpread.bind(spreadNS.Events.SelectionChanged, posOperationObj.selectionChanged);
@@ -2970,6 +3055,41 @@ $(document).ready(function() {
                     return !readOnly;
                 }
             },
+            copyBlock: {
+                name: '复制整块',
+                icon: 'fa-files-o',
+                callback: function (key, opt) {
+                    posOperationObj.block = [];
+                    const copyBlockList = posOperationObj.getBlockData();
+                    setLocalCache(copyBlockTag, JSON.stringify({ block: copyBlockList, tag: 'pos' }));
+                },
+                disabled: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(posSheet);
+                    return !node;
+                }
+            },
+            pasteBlock: {
+                name: '粘贴整块',
+                icon: 'fa-clipboard',
+                disabled: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(ledgerSheet);
+                    if (!node || (node.children && node.children.length > 0) || node._invalid || node.code) return true;
+
+                    const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
+                    return !(copyInfo && copyInfo.block && copyInfo.block.length > 0 && copyInfo.tag === 'pos');
+                },
+                callback: function (key, opt) {
+                    const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
+                    if (copyInfo.block.length > 0) {
+                        posOperationObj.pasteBlock(copyInfo);
+                    } else {
+                        document.execCommand('paste');
+                    }
+                },
+                visible: function (key, opt) {
+                    return !readOnly;
+                }
+            },
             'merge-peg': {
                 name: '合并起讫桩号',
                 disabled: function (key, opt) {

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

@@ -228,7 +228,7 @@ $(function () {
         onSelect: function (formattedDate, date, inst) {
             if (!date) return;
             $('#stage-period').val(`${formattedDate}-01`);
-            stagePeriod.clear();;
+            stagePeriod.clear();
             stagePeriod.selectDate(new Date(date.getFullYear(), date.getMonth(), 1));
         }
     }).data('datepicker');

+ 450 - 119
app/public/js/pos_calc_tmpl.js

@@ -1,5 +1,454 @@
 $(document).ready(() => {
     autoFlashHeight();
+    class FolderObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#folder-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.treeSetting = {
+                id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
+                level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
+                rootId: -1, calcFields: [], keys: ['id', 'master_id', 'tree_id'],
+            };
+            this.tree = createNewPathTree('base', this.treeSetting);
+            this.spreadSetting = {
+                cols: [
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 330, formatter: '@', cellType: 'tree'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: readOnly,
+            };
+
+            this.initSpread();
+            this.initOtherEvent();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            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);
+        }
+        initOtherEvent() {
+            // 增删上下移升降级
+            $('a[name="base-opr"]').click(function () {
+                folderObj.baseOpr(this.getAttribute('type'));
+            });
+        }
+        refreshOperationValid() {
+            const setObjEnable = function (obj, enable) {
+                if (enable) {
+                    obj.removeClass('disabled');
+                } else {
+                    obj.addClass('disabled');
+                }
+            };
+            const invalidAll = function () {
+                setObjEnable($('a[name=base-opr][type=add]'), false);
+                setObjEnable($('a[name=base-opr][type=delete]'), false);
+                setObjEnable($('a[name=base-opr][type=up-move]'), false);
+                setObjEnable($('a[name=base-opr][type=down-move]'), false);
+                setObjEnable($('a[name=base-opr][type=up-level]'), false);
+                setObjEnable($('a[name=base-opr][type=down-level]'), false);
+            };
+            const sel = this.sheet.getSelections()[0];
+            const row = sel ? sel.row : -1;
+            const tree = this.sheet.zh_tree;
+            if (!tree) {
+                invalidAll();
+                return;
+            }
+            const first = tree.nodes[row];
+            let last = first, sameParent = true;
+            if (sel.rowCount > 1 && first) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = tree.nodes[sel.row + r];
+                    if (!rNode) {
+                        sameParent = false;
+                        break;
+                    }
+                    if (rNode.tree_level > first.tree_level) continue;
+                    if ((rNode.tree_level < first.tree_level) || (rNode.tree_level === first.tree_level && rNode.tree_pid !== first.tree_pid)) {
+                        sameParent = false;
+                        break;
+                    }
+                    last = rNode;
+                }
+            }
+            const preNode = tree.getPreSiblingNode(first);
+            const valid = !this.sheet.zh_setting.readOnly;
+
+            setObjEnable($('a[name=base-opr][type=add]'), valid);
+            setObjEnable($('a[name=base-opr][type=delete]'), valid && first && sameParent);
+            setObjEnable($('a[name=base-opr][type=up-move]'), valid && first && sameParent && preNode);
+            setObjEnable($('a[name=base-opr][type=down-move]'), valid && first && sameParent && !tree.isLastSibling(last));
+            setObjEnable($('a[name=base-opr][type=up-level]'), valid && first && sameParent && tree.getParent(first) && first.tree_level > 1);
+            setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && sameParent && preNode);
+        }
+        loadRelaData() {
+            this.refreshOperationValid();
+            templateObj.loadDetailData(SpreadJsObj.getSelectObject(this.sheet));
+        }
+        refreshTree(data) {
+            const sheet = this.sheet;
+            SpreadJsObj.massOperationSheet(sheet, function () {
+                const tree = sheet.zh_tree;
+                // 处理删除
+                if (data.delete) {
+                    data.delete.sort(function (a, b) {
+                        return b.deleteIndex - a.deleteIndex;
+                    });
+                    for (const d of data.delete) {
+                        sheet.deleteRows(d.deleteIndex, 1);
+                    }
+                }
+                // 处理新增
+                if (data.create) {
+                    const newNodes = data.create;
+                    if (newNodes) {
+                        newNodes.sort(function (a, b) {
+                            return a.index - b.index;
+                        });
+
+                        for (const node of newNodes) {
+                            sheet.addRows(node.index, 1);
+                            SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(node), 1);
+                        }
+                    }
+                }
+                // 处理更新
+                if (data.update) {
+                    const rows = [];
+                    for (const u of data.update) {
+                        rows.push(tree.nodes.indexOf(u));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    const expanded = [];
+                    for (const e of data.expand) {
+                        if (expanded.indexOf(e) === -1) {
+                            const posterity = tree.getPosterity(e);
+                            for (const p of posterity) {
+                                sheet.setRowVisible(tree.nodes.indexOf(p), p.visible);
+                                expanded.push(p);
+                            }
+                        }
+                    }
+                }
+            });
+        }
+        loadData(datas) {
+            this.tree.loadDatas(datas);
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.tree);
+            this.refreshOperationValid();
+        }
+        getDefaultSelectInfo() {
+            if (!this.tree) return;
+            const sel = this.sheet.getSelections()[0];
+            const node = this.sheet.zh_tree.nodes[sel.row];
+            let count = 1;
+            if (!node) return [this.tree, null, count];
+            if (sel.rowCount > 1) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = this.sheet.zh_tree.nodes[sel.row + r];
+                    if (rNode.tree_level > node.tree_level) continue;
+                    if ((rNode.tree_level < node.tree_level) || (rNode.tree_level === node.tree_level && rNode.tree_pid !== node.tree_pid)) {
+                        toastr.warning('请选择同一节点下的节点,进行该操作');
+                        return;
+                    }
+                    count += 1;
+                }
+            }
+            return [this.tree, node, count];
+        }
+        baseOpr(type, addCount = 1) {
+            const self = this;
+            const sheet = self.sheet;
+            const sel = sheet.getSelections()[0];
+            const [tree, node, count] = this.getDefaultSelectInfo();
+            if (!tree || !count) return;
+
+            const updateData = {
+                target: 'posCalc',
+                postType: type,
+                postData: {
+                    count: type === 'add' ? addCount : count,
+                }
+            };
+            if (node) updateData.postData.id = node.tree_id;
+            if (type === 'delete') {
+                deleteAfterHint(function () {
+                    postData('folder', updateData, function (result) {
+                        const refreshData = tree.loadPostData(result);
+                        self.refreshTree(refreshData);
+                        if (sel) {
+                            sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
+                        }
+                        self.refreshOperationValid();
+                        self.loadRelaData();
+                    });
+                }, '确认删除「当前分类」?');
+            } else {
+                postData('folder', updateData, function (result) {
+                    const refreshData = tree.loadPostData(result);
+                    self.refreshTree(refreshData);
+                    if (['up-move', 'down-move'].indexOf(type) > -1) {
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                        }
+                    } else if (type === 'add') {
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                        }
+                        self.loadRelaData();
+                    }
+                    self.refreshOperationValid();
+                });
+            }
+        }
+        // 事件
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    folderObj.loadRelaData();
+                }
+            }
+        }
+        editEnded(e, info) {
+            if (!info.sheet.zh_setting) return;
+
+            const tree = info.sheet.zh_tree;
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            const data = tree.getNodeKeyData(node);
+            // 未改变值则不提交
+            const col = info.sheet.zh_setting.cols[info.col];
+            if (col.field !== 'name') return;
+
+            const orgValue = node[col.field];
+            const newValue = trimInvalidChar(info.editingText);
+            if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) return;
+
+            if (info.editingText) {
+                const text = newValue;
+                if (col.type === 'Number') {
+                    const num = _.toNumber(text);
+                    if (_.isFinite(num)) {
+                        data[col.field] = num;
+                    } else {
+                        try {
+                            data[col.field] = ZhCalc.mathCalcExpr(transExpr(text));
+                        } catch(err) {
+                            toastr.error('输入的表达式非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                    }
+                } else {
+                    data[col.field] = text;
+                }
+            } else {
+                data[col.field] = col.type === 'Number' ? 0 : '';
+            }
+            // 更新至服务器
+            postData('folder', {target: 'posCalc', postType: 'update', postData: data}, function (result) {
+                const refreshNode = folderObj.tree.loadPostData(result);
+                folderObj.refreshTree(refreshNode);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            });
+        }
+        deletePress (sheet) {
+            if (!sheet.zh_setting) return;
+            const sel = sheet.getSelections()[0], datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                let bDel = false;
+                const node = sheet.zh_tree.nodes[iRow];
+                const data = sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    const col = sheet.zh_setting.cols[iCol];
+                    const style = sheet.getStyle(iRow, iCol);
+                    if (style.locked) continue;
+
+                    data[col.field] = col.type === 'Number' ? 0 : '';
+                    bDel = true;
+                }
+                if (bDel) datas.push(data);
+            }
+            if (datas.length > 0) {
+                postData('folder', {target: 'posCalc', postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    folderObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, sel.row, sel.rowCount);
+                });
+            }
+        }
+        clipboardPasting(e, info) {
+            info.cancel = true;
+        }
+    }
+    const folderObj = new FolderObj();
+    folderObj.loadData(folderList);
+    class TemplateObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#template-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.data = createAncillaryGcl({ id: 'id', masterId: 'folder_id', sort: [['create_time', 'desc']] });
+            this.templateSpreadSetting = {
+                cols: [
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 240, formatter: '@'},
+                    {title: '创建人', colSpan: '1', rowSpan: '1', field: 'user_name', hAlign: 0, width: 80, formatter: '@', readOnly: true},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                headColWidth: [50],
+            };
+
+            this.initSpread();
+            this.initOtherEvent();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.templateSpreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            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);
+        }
+        initOtherEvent() {
+            // 增删上下移升降级
+            $('a[name="template-opr"]').click(function () {
+                templateObj.baseOpr(this.getAttribute('type'));
+            });
+        }
+        loadData(datas) {
+            this.data.loadDatas(datas);
+            this.loadDetailData(SpreadJsObj.getSelectObject(folderObj.sheet));
+        }
+        refreshSheet() {
+            if (this.sheet.zh_data.length === 0) {
+                this.reloadDetailData();
+            } else {
+                SpreadJsObj.reLoadSheetData(this.sheet);
+            }
+        }
+        reloadDetailData() {
+            const data = this.folder ? this.data.getPartData(this.folder.id) || [] : [];
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, data);
+        }
+        loadDetailData(folder) {
+            this.folder = folder;
+            this.reloadDetailData();
+            this.loadTemplate();
+        }
+        baseOpr(type) {
+            const self = this;
+            const curTemplate = SpreadJsObj.getSelectObject(this.sheet);
+            if (type === 'delete') {
+                deleteAfterHint(function () {
+                    postData('save', {del: curTemplate.id, type: 'posCalc'}, function(result) {
+                        templateObj.data.updateDatas({ del: curTemplate.id });
+                        templateObj.refreshSheet();
+                        templateObj.loadTemplate();
+                    });
+                }, '确认删除「当前模板数据」?');
+            } else if (type === 'add') {
+                postData('save', {add: { name: '', folder_id: self.folder.id }, type: 'posCalc'}, function(result) {
+                    templateObj.data.updateDatas({ add: result.add });
+                    templateObj.refreshSheet();
+                });
+            }
+        }
+        async loadTemplateDetail(template) {
+            const result = await postDataAsync('load', {filter: 'detail', id: template.id, type: 'posCalc'});
+            if (result && result.detail) {
+                template.col_set = result.detail.col_set;
+                template.user_name = result.detail.user_name;
+                template.create_time_str = result.detail.create_time_str;
+            }
+        }
+        async loadTemplate() {
+            const curTemplate = SpreadJsObj.getSelectObject(this.sheet);
+            if (!curTemplate) {
+                $('#detail-user-info').html('');
+                $('#detail-ctrl').hide();
+            } else {
+                $('#detail-ctrl').hide();
+                if (!curTemplate.col_set) await this.loadTemplateDetail(curTemplate);
+                detailObj.loadDetail(curTemplate);
+                if (curTemplate.user_name) {
+                    $('#detail-user-info').html(`新建人:${curTemplate.user_name}(${curTemplate.create_time_str})`);
+                } else {
+                    $('#detail-user-info').html('');
+                }
+            }
+        }
+        // 事件
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    templateObj.loadTemplate();
+                }
+            }
+        }
+        editEnded(e, info) {
+            const setting = info.sheet.zh_setting;
+            if (!setting) return;
+            const detailData = SpreadJsObj.getSelectObject(info.sheet);
+            if (detailData.create_user_id !== userID) {
+                toastr.warning('不可修改');
+                return;
+            }
+
+            const col = setting.cols[info.col];
+            if (col.field !== 'name') return;
+            const orgText = detailData ? detailData[col.field] : '', newText = trimInvalidChar(info.editingText);
+            if (orgText === newText || (!orgText && !newText)) return;
+
+            postData('save', { update: { id: detailData.id, name: newText }, type: 'posCalc'}, function(result){
+                templateObj.data.updateDatas(result);
+                templateObj.refreshSheet();
+            });
+        }
+        deletePress (sheet) {
+            if (!sheet.zh_setting) return;
+            const col = setting.cols[info.col];
+            if (col.field !== 'name') return;
+            const detailData = SpreadJsObj.getSelectObject(info.sheet);
+            if (detailData.create_user_id !== userID) {
+                toastr.warning('不可修改');
+                return;
+            }
+            if (datas.length > 0) {
+                postData('update',  { update: { id: detailData.id, name: '' }, type: 'posCalc'}, function (result) {
+                    detailObj.data.updateDatas(result.detail);
+                    detailObj.refreshSheet();
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, sel.row, sel.rowCount);
+                });
+            }
+        }
+        clipboardPasting(e, info) {
+            info.cancel = true;
+        }
+    }
+    const templateObj = new TemplateObj();
+    templateObj.loadData(templateList);
+
     class TemplateDetailObj {
         constructor() {
             const self = this;
@@ -81,7 +530,7 @@ $(document).ready(() => {
                 } else if (col.field === 'rela_col') {
                     const exist = self.colSetData.find(x => { return x.type === 'num' && x.calc_code === validText });
                     if (!exist) {
-                        toastr.warning(`暂无计代号为${validText}的列,请先配置该数值列`);
+                        toastr.warning(`暂无计代号为${validText}的列,请先配置该数值列`);
                     } else {
                         select.rela_col = validText;
                     }
@@ -283,85 +732,6 @@ $(document).ready(() => {
         calcTemplatePreview.preview(data);
     });
 
-    const templateObj = (function(list){
-        const templates = list;
-        let curTemplate;
-
-        const loadTemplateDetail = async function(template) {
-            const result = await postDataAsync('load', {filter: 'detail', id: template.id, type: 'posCalc'});
-            if (result && result.detail) {
-                template.col_set = result.detail.col_set;
-                template.user_name = result.detail.user_name;
-                template.create_time_str = result.detail.create_time_str;
-            }
-        };
-        const refreshTemplate = async function() {
-            if (!curTemplate) {
-                $('#detail-user-info').html('');
-            } else {
-                $('dd[templateId]').removeClass('bg-warning');
-                $(`dd[templateId=${curTemplate.id}]`).addClass('bg-warning');
-                if (!curTemplate.col_set) await loadTemplateDetail(curTemplate);
-                detailObj.loadDetail(curTemplate);
-                if (curTemplate.user_name) {
-                    $('#detail-user-info').html(`新建人:${curTemplate.user_name}(${curTemplate.create_time_str})`);
-                } else {
-                    $('#detail-user-info').html('');
-                }
-            }
-        };
-        const setCurTemplate = function(template) {
-            curTemplate = template;
-            refreshTemplate();
-        };
-        const getCurTemplate = function() {
-            return curTemplate;
-        };
-        const getTemplateCaptionInnerHtml = function(template) {
-            const usedHtml = template.used_count > 0 ? '<i class="ml-1 fa fa-lock text-danger"></i>' : '';
-            return `<div>${template.name}${usedHtml}</div>` +
-                '    <div class="btn-group-table" style="display: none;">\n' +
-                '    <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameTemplate"><i class="fa fa-pencil fa-fw"></i></a>\n' +
-                '    <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delTemplate"><i class="fa fa-trash-o fa-fw text-danger"></i></a>\n' +
-                '</div>';
-        };
-        const getTemplateCaptionHtml = function(template) {
-            return `<div class="d-flex justify-content-between align-items-center table-file" templateId="${template.id}">` + getTemplateCaptionInnerHtml(template) + '</div>';
-        };
-        const getTemplateHtml = function(template) {
-            const html = [];
-            html.push(`<dd class="list-group-item" templateId="${template.id}">`, getTemplateCaptionHtml(template), '</dd>');
-            return html.join('');
-        };
-
-        const addTemplate = function() {
-            postData('save', {add: { name: '' }, type: 'posCalc'}, function(result) {
-                templates.push(result.add);
-                $('#template-list').append(getTemplateHtml(result.add));
-            });
-        };
-        const renameTemplate = function(id, name) {
-            postData('save', { update: { id, name }, type: 'posCalc'}, function(result){
-                const template = templates.find(x => { return x.id === result.update.id; });
-                template.name = result.update.name;
-                $(`dd[templateId=${template.id}]`).html(getTemplateCaptionHtml(template));
-            });
-        };
-        const delTemplate = function(id){
-            postData('save', {del: id, type: 'posCalc'}, function(result) {
-                $(`dd[templateId=${id}]`).remove();
-                const tIndex = templates.findIndex(x => { return x.id === id; });
-                templates.splice(tIndex, 1);
-                if (curTemplate && curTemplate.id === id) {
-                    curTemplate = null;
-                    refreshTemplate();
-                }
-            });
-        };
-        if (templates.length > 0) setCurTemplate(templates[0]);
-        return { setCurTemplate, getCurTemplate, addTemplate, delTemplate, renameTemplate, getTemplateCaptionHtml, getTemplateCaptionInnerHtml }
-    })(templateList);
-
     $('body').on('click', '.table-file', function(e) {
         if (this.getAttribute('renaming') === '1') return;
         if (e.target.tagName === 'A' || e.target.tagName === 'I' || e.target.tagName === 'INPUT') return;
@@ -376,49 +746,10 @@ $(document).ready(() => {
         $(this).children(".btn-group-table").css("display","none");
     });
 
-    $('body').on('click', 'a[name=renameTemplate]', function(e){
-        $(this).parents('.table-file').attr('renaming', '1');
-        $(`#${this.getAttribute('aria-describedby')}`).remove();
-        const templateId = $(this).parents('.table-file').attr('templateId');
-        const template = templateList.find(x => { return x.id === templateId; });
-        if (!template) return;
-
-        const html = [];
-        html.push(`<div><input type="text" class="form-control form-control-sm" style="width: 160px" value="${template.name}"/></div>`);
-        html.push('<div class="btn-group-table" style="display: none;">',
-            `<a href="javascript: void(0)" name="renameOk" class="mr-1"><i class="fa fa-check fa-fw"></i></a>`,
-            `<a href="javascript: void(0)" class="mr-1" name="renameCancel"><i class="fa fa-remove fa-fw text-danger"></i></a>`, '</div>');
-        $(`.table-file[templateId=${templateId}]`).html(html.join(''));
-        e.stopPropagation();
-    });
-    $('body').on('click', 'a[name=renameOk]', function(){
-        const templateId = $(this).parents('.table-file').attr('templateId');
-        const newName = $(this).parents('.table-file').find('input').val();
-        templateObj.renameTemplate(templateId, newName);
-        $(this).parents('.table-file').attr('renaming', '0');
-    });
-    $('body').on('click', 'a[name=renameCancel]', function() {
-        $(this).parents('.table-file').attr('renaming', '0');
-        const templateId = $(this).parents('.table-file').attr('templateId');
-        const template = templateList.find(x => { return x.id === templateId; });
-        if (!template) return;
-
-        $(`.table-file[templateId=${templateId}]`).html(templateObj.getTemplateCaptionInnerHtml(template));
-    });
-    $('body').on('click', 'a[name=delTemplate]', function(e) {
-        e.stopPropagation();
-        const templateId = $(this).parents('.table-file').attr('templateId');
-        templateObj.delTemplate(templateId);
-    });
-
     $('#export').click(function() {
         detailObj.export();
     });
     $('#import').click(function() {
         detailObj.import();
     });
-
-    $('#addTemplate').click(function() {
-        templateObj.addTemplate();
-    });
 });

+ 1 - 0
app/router.js

@@ -653,6 +653,7 @@ module.exports = app => {
 
     app.get('/template/posCalc', sessionAuth, 'templateController.posCalc');
     app.get('/template/cost', sessionAuth, 'templateController.cost');
+    app.post('/template/folder', sessionAuth, 'templateController.saveFolder');
     app.post('/template/save', sessionAuth, 'templateController.saveTemplate');
     app.post('/template/load', sessionAuth, 'templateController.load');
     app.post('/template/preview', sessionAuth, 'templateController.preview');

+ 86 - 25
app/service/calc_tmpl.js

@@ -34,7 +34,10 @@ const PosCalc = (function(){
     };
     const ValidColInfo = [
         { 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'], valid: ['title', 'width', 'calc_code', 'decimal', 'unit'], def: { title: '数值', width: 80, decimal: 2, type: 'num', unit: ''} },
+        {
+            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'], def: { title: '数值', width: 80, decimal: 2, type: 'num', unit: ''} },
         { key: 'spec', name: '规格', fields: ['spec'], valid: ['title', 'width', 'rela_col', 'spec_set'], def: { title: '规格', width: 80, rela_col: '', type: 'spec'} },
         { key: 'qty', name: '数量', fields: ['qty'], valid: ['title', 'width', 'expr', 'decimal'], def: { title: '数量', width: 80, decimal: 2, expr: '', type: 'qty' } },
     ];
@@ -53,7 +56,7 @@ const PosCalc = (function(){
 const Cost = (function(){
     const EmptySpreadCache = {
         cols: [],
-        emptyRows: 3,
+        emptyRows: 0,
         headRows: 1,
         headRowHeight: [32],
         defaultRowHeight: 21,
@@ -62,21 +65,36 @@ const Cost = (function(){
         frozenLineColor: '#93b5e4',
     };
     const BaseSpreadColSetting = {
+        code: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
+        name: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
+        party_b: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
+        yf_excl_tax_tp: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
+        in_excl_tax_tp: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
+        sf_excl_tax_tp: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
         str: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
         num: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
+        postil: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 0, width: 80, formatter: '@' },
+        sf_percent: { title: '', colSpan: '1', rowSpan: '1', field: '', hAlign: 2, width: 80, type: 'Number' },
     };
     const ValidColInfo = [
-        { key: 'str', name: '文本', fields: ['str1', 'str2', 'str3', 'str4', 'str5', 'str6'], valid: ['title', 'width'], def: { title: '文本', width: 80, type: 'str'} },
+        { 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: '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'],
+            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'],
-            def: { title: '数值/计算', width: 80, decimal: 2, type: 'num', unit: ''}
+            def: { title: '数值/计算', width: 80, decimal: 6, type: 'num', unit: ''}
         },
-        { key: 'memo', name: '长文本', fields: ['memo1', 'memo2'], valid: ['title', 'width'], def: { title: '长文本', width: 120, type: 'memo'} },
+        { key: 'postil', name: '本期批注', fields: ['postil'], valid: ['title', 'width'], def: { title: '本期批注', width: 120, type: 'postil'} },
+        { key: 'sf_percent', name: '实付比例', fields: ['sf_percent'], valid: ['title', 'width', 'calc_code'], def: { title: '实付比例', width: 80, type: 'sf_percent', decimal: 2} },
     ];
     ValidColInfo.forEach(vci => { return vci.count = vci.fields.length; });
-    return { EmptySpreadCache, BaseSpreadColSetting, ValidColInfo, ValidCount: 10 };
+    return { EmptySpreadCache, BaseSpreadColSetting, ValidColInfo, ValidCount: 10, subSpreadReadOnly: ['code', 'name', 'party_b', 'settle_tp', 'in_excl_tax_tp'] };
 })();
 function randomWord(randomFlag, min, max){
     let str = "",
@@ -110,11 +128,14 @@ module.exports = app => {
         }
 
         async getAllTemplate(pid, type, sort = 'asc') {
-            return this.getAllDataByCondition({
-                columns: ['id', 'pid', 'name', 'create_user_id', 'is_locked'],
-                where: { pid, type },
-                orders: [['create_time', sort]]
-            });
+            // return this.getAllDataByCondition({
+            //     columns: ['id', 'pid', 'name', 'create_user_id', 'is_locked'],
+            //     where: { pid, type },
+            //     orders: [['create_time', sort]]
+            // });
+            const sql = 'SELECT ct.id, ct.pid, ct.folder_id, ct.name, ct.create_user_id, ct.is_locked, pa.name AS user_name' +
+                        `    FROM ${this.tableName} ct LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON ct.create_user_id = pa.id WHERE ct.pid = ? and ct.type = ? ORDER BY ct.create_time ${sort}`;
+            return this.db.query(sql, [pid, type])
         }
         async analysisTemplate(data) {
             const datas = data instanceof Array ? data : [data];
@@ -126,6 +147,7 @@ module.exports = app => {
                 x.multi_header = x.multi_header ? JSON.parse(x.multi_header) : undefined;
                 x.calc_expr = x.calc_expr ? JSON.parse(x.calc_expr) : [];
                 x.calc_order = x.calc_order ? x.calc_order.split(',') : [];
+                x.sub_spread_cache = x.sub_spread_cache ? JSON.parse(x.sub_spread_cache) : {};
             });
             for (const d of datas) {
                 if (d.spec_set) {
@@ -196,7 +218,7 @@ module.exports = app => {
 
             let newExpr = expr;
             colSet.forEach(x => {
-                if (x.type === 'num' && x.calc_code) newExpr = newExpr.replace(new RegExp(x.calc_code, 'gm'), x.field);
+                if (x.calc_code) newExpr = newExpr.replace(new RegExp(x.calc_code, 'gm'), x.field);
             });
             return newExpr;
         }
@@ -212,7 +234,7 @@ module.exports = app => {
                         spreadCol.title = col.title + '(' + col.unit + ')';
                     } else if (v === 'expr') {
                         spreadCol.expr = this._transExpr(col.expr, colSet);
-                        spreadCol.readOnly = true;
+                        spreadCol.readOnly = spreadCol.expr;
                     } else {
                         if (col[v] !== undefined) spreadCol[v] = col[v];
                     }
@@ -246,7 +268,7 @@ module.exports = app => {
                         colTitle = colTitle + '(' + col.unit + ')';
                     } else if (v === 'expr') {
                         spreadCol.expr = this._transExpr(col.expr, colSet);
-                        spreadCol.readOnly = true;
+                        spreadCol.readOnly = spreadCol.expr;
                     } else {
                         if (col[v] !== undefined) spreadCol[v] = col[v];
                     }
@@ -336,7 +358,7 @@ module.exports = app => {
         }
         // ----------------------------------------------------------------------
 
-        async _addTemplate(name, type) {
+        async _addTemplate(name, type, folder_id) {
             if (ValidTemplateType.indexOf(type) < 0) throw '新增的模板类型非法';
             const count = await this.count({pid: this.ctx.session.sessionProject.id, type});
             if (count > this.TemplateRela[type].ValidCount) throw '已达模板使用上限';
@@ -347,6 +369,7 @@ module.exports = app => {
                 pid: this.ctx.session.sessionProject.id,
                 // spid: this.ctx.subProject.id,
                 // tid: this.ctx.tender.id,
+                folder_id: folder_id || '',
                 name: name || '新增计算模板',
                 type,
                 create_user_id: this.ctx.session.sessionUser.accountId,
@@ -356,9 +379,12 @@ module.exports = app => {
                 spread_cache: JSON.stringify(relaConst.EmptySpreadCache),
                 field_cache: '{}',
                 decimal: JSON.stringify([{ def: 2 }]),
+                sub_spread_cache: '[]',
             };
             await this.db.insert(this.tableName, insertData);
-            return await this.getTemplate(insertData.id);
+            const result = await this.getTemplate(insertData.id);
+            result.user_name = this.ctx.session.sessionUser.name;
+            return result;
         }
         async checkTemplateEdit(id) {
             const template = await this.getDataById(id);
@@ -396,7 +422,7 @@ module.exports = app => {
             const calcOrder = [];
             for (const c of cols) {
                 if (c.type !== 'Number') continue;
-                calcOrder.push({ field: c.field, expr: c.expr || '', decimal: c.decimal });
+                calcOrder.push({ field: c.field, expr: c.expr || '', decimal: c.decimal, calc_code: c.calc_code });
             }
             for (const co of calcOrder) {
                 co.leaf = this._getCalcLeaf(co, calcOrder);
@@ -415,7 +441,28 @@ module.exports = app => {
                 updateData.col_set = JSON.stringify(data.col_set);
                 updateData.multi_header = JSON.stringify(data.multi_header);
                 const spread_cache = this.calcSpreadCache(org.type, data.col_set, data.multi_header);
-                updateData.spread_cache = JSON.stringify(spread_cache);
+                if (org.type === 'cost') {
+                    spread_cache.cols[0].cellType = 'tree';
+                    const treeSpreadCache = this.ctx.helper.clone(spread_cache);
+                    treeSpreadCache.cols.splice(treeSpreadCache.cols.findIndex(x => { return x.field === 'party_b';}), 1);
+                    updateData.spread_cache = JSON.stringify(treeSpreadCache);
+                    const treeCol = treeSpreadCache.cols.find(x => { return x.cellType === 'tree'});
+                    delete treeCol.cellType;
+                    for (const col of treeSpreadCache.cols) {
+                        if (this.TemplateRela.cost.subSpreadReadOnly.indexOf(col.field) >= 0) col.readOnly = true;
+                    }
+                    const sub_spread_cache = { common: treeSpreadCache };
+                    const dealSpreadCache = this.ctx.helper.clone(spread_cache);
+                    const treeDCol = dealSpreadCache.cols.find(x => { return x.cellType === 'tree'});
+                    delete treeDCol.cellType;
+                    sub_spread_cache.deal = dealSpreadCache;
+                    for (const col of dealSpreadCache.cols) {
+                        if (this.TemplateRela.cost.subSpreadReadOnly.indexOf(col.field) >= 0) col.readOnly = true;
+                    }
+                    updateData.sub_spread_cache = JSON.stringify(sub_spread_cache);
+                } else {
+                    updateData.spread_cache = JSON.stringify(spread_cache);
+                }
                 const field_cache = {};
                 data.col_set.forEach(x => { field_cache[x.field] = x.title; });
                 updateData.field_cache = JSON.stringify(field_cache);
@@ -444,7 +491,7 @@ module.exports = app => {
         }
         async saveTemplate(data) {
             const result = {};
-            if (data.add) result.add = await this._addTemplate(data.add.name, data.type);
+            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);
             return result;
@@ -503,24 +550,38 @@ module.exports = app => {
                 return 0;
             }
         }
+
         calcByTemplate(data, updateData, orgData, calcExpr, updateExprInfo) {
             const codeReg = /num_[a-z]{1}/gm;
+            const specRegs = [];
+            for (const ce of calcExpr) {
+                if (!ce.field.match(codeReg)) specRegs.push(new RegExp(ce.field, 'gm'));
+            }
             for (const ce of calcExpr) {
                 if (ce.expr) {
                     let calcExpr = ce.expr;
+                    for (const sr of specRegs) {
+                        const specParam = calcExpr.match(sr);
+                        if (specParam) {
+                            for (const sp of specParam) {
+                                calcExpr = calcExpr.replace(new RegExp(sp, 'gm'), (data[sp] !== undefined ? data[sp] : orgData[sp]) || 0);
+                            }
+                        }
+                    }
                     const codeParam = calcExpr.match(codeReg);
-                    for (const cp of codeParam) {
-                        calcExpr = calcExpr.replace(new RegExp(cp, 'gm'), data[cp] || orgData[cp] || 0);
+                    if (codeParam) {
+                        for (const cp of codeParam) {
+                            calcExpr = calcExpr.replace(new RegExp(cp, 'gm'), (data[cp] !== undefined ? data[cp] : orgData[cp]) || 0);
+                        }
                     }
                     if (updateExprInfo && updateExprInfo[ce.field]) updateExprInfo[ce.field] = calcExpr;
                     try {
-                        data[ce.field] = this.ctx.helper.round(this._calcExpr(calcExpr.replace(new RegExp('%', 'gm'), '/100')), ce.decimal);
+                        data[ce.field] = this.ctx.helper.round(this._calcExpr(calcExpr.replace(new RegExp('%', 'gm'), '/100')), ce.decimal || 2);
                     } catch(err) {
                         data[ce.field] = 0;
                     }
                 } else {
-                    if (updateData[ce.field] === undefined) continue;
-                    data[ce.field] = this.ctx.helper.round(updateData[ce.field], ce.decimal || 2);
+                    if (updateData[ce.field] !== undefined) data[ce.field] = this.ctx.helper.round(updateData[ce.field], ce.decimal || 2);
                 }
             }
         }

+ 49 - 0
app/service/calc_tmpl_folder.js

@@ -0,0 +1,49 @@
+'use strict';
+
+/**
+ *
+ * 计算模板-分类树
+ * @author Mai
+ * @date
+ * @version
+ */
+
+module.exports = app => {
+
+    class CostStageLedger extends app.BaseTreeService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, {
+                mid: 'master_id',
+                kid: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                keyPre: 'calc_tmpl_maxTreeId:',
+                uuid: true,
+            });
+            this.tableName = 'calc_tmpl_folder';
+        }
+
+        _getDefaultData(data) {
+            data.pid = this.ctx.session.sessionProject.id;
+            data.type = data.master_id.split('-')[1];
+            data.add_user_id = this.ctx.session.sessionUser.accountId;
+            data.update_user_id = data.add_user_id;
+        }
+
+        async _deleteRelaData(mid, deleteData) {
+            await this.transaction.query(`UPDATE ${this.ctx.service.calcTmpl.tableName} SET folder_id = '' WHERE folder_id IN(${deleteData.map(x => { return `'${x.id}'`; }).join(', ')})`);
+        }
+    }
+
+    return CostStageLedger;
+};

+ 12 - 2
app/service/cost_stage.js

@@ -27,7 +27,7 @@ module.exports = app => {
             this.stageType = {
                 ledger: { key: 'ledger', decimal: { tp: 6, tax: 2 }, shenpi_status: 'cost_stage_ledger', push_type: 'costStageLedger', dataService: 'costStageLedger', detailService: 'costStageDetail' },
                 book: { key: 'book', decimal: { tp: 6 }, shenpi_status: 'cost_stage_book', push_type: 'costStageBook', dataService: 'costStageBook', detailService: 'costStageBookDetail' },
-                analysis: { key: 'analysis', decimal: { tp: 6 }, shenpi_status: 'cost_stage_analysis', push_type: 'costStageAnalysis', dataService: 'costStageAnalysis' },
+                analysis: { key: 'analysis', decimal: { tp: 6 }, shenpi_status: 'cost_stage_analysis', push_type: 'costStageAnalysis', dataService: 'costStageAnalysis', detailService: 'costStageAnalysisDetail' },
             };
         }
 
@@ -65,6 +65,13 @@ module.exports = app => {
             this._analysisstage(result);
             return result;
         }
+        async getAllCheckedStages(tid, stage_type, sort = 'ASC') {
+            const sql = `SELECT cls.*, pa.name AS user_name FROM ${this.tableName} cls LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON cls.create_user_id = pa.id` +
+                `  WHERE cls.tid = ? AND cls.stage_type = ? AND audit_status = ? ORDER BY cls.stage_order ${sort}`;
+            const result = await this.db.query(sql, [tid, stage_type, audit.status.checked]);
+            this._analysisstage(result);
+            return result;
+        }
         async getStage(id) {
             const result = await this.getDataById(id);
             this._analysisstage(result);
@@ -95,6 +102,7 @@ module.exports = app => {
                 audit_times: 1, audit_status: audit.status.uncheck,
                 decimal: JSON.stringify({ tp: 6 }),
             };
+            if (stage_type === 'analysis') data.calc_template = this.ctx.subProject.cost_calc_template;
             if (rela_stage) data.rela_stage = JSON.stringify(rela_stage);
 
             const preStage = maxOrder > 0 ? await this.getStageByOrder(tid, stage_type, maxOrder) : null;
@@ -109,11 +117,12 @@ module.exports = app => {
                 await this.ctx.service[typeInfo.dataService].initStageData(transaction, data, preStage);
                 await this.ctx.service.costStageAudit.copyPreAuditors(transaction, preStage, data);
                 await transaction.commit();
-                return data;
             } catch(err) {
+                console.log(err);
                 await transaction.rollback();
                 throw err;
             }
+            return data;
         }
         async delete(id) {
             const stage = await this.getDataById(id);
@@ -122,6 +131,7 @@ module.exports = app => {
             try {
                 await conn.delete(this.tableName, { id });
                 await conn.delete(this.ctx.service[typeInfo.dataService].tableName, { stage_id: id });
+                await conn.delete(this.ctx.service[typeInfo.detailService].tableName, { stage_id: id });
                 const files = await this.ctx.service.costStageFile.getFiles({ where: { stage_id: id} });
                 for (const f of files) {
                     this.ctx.app.fujianOss.delete(f.filepath);

+ 685 - 0
app/service/cost_stage_analysis.js

@@ -0,0 +1,685 @@
+'use strict';
+
+/**
+ *
+ * 支付审批-安全生产
+ * @author Mai
+ * @date
+ * @version
+ */
+const Ledger = require('../lib/ledger');
+const costFields = {
+    textFields: ['code', 'name', 'unit', 'postil'],
+    selfTextFields: ['str1', 'str2', 'str3', 'str4'],
+    calcFields: ['yf_excl_tax_tp', 'in_excl_tax_tp', 'sf_excl_tax_tp', 'sf_percent'],
+    selfCalcFields: ['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'],
+    readFields: ['calc_read'],
+    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'],
+};
+costFields.sumFields = [...costFields.calcFields, ...costFields.selfCalcFields];
+costFields.preCopyFields = [...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.readFields, ...costFields.taxFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.taxFields, ...costFields.hisFields];
+const calcType = { other: 0, in: 1, out: 2 };
+
+const auditConst = require('../const/audit').costStage;
+
+module.exports = app => {
+
+    class CostStageAnalysis extends app.BaseTreeService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, {
+                mid: 'stage_id',
+                kid: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                keyPre: 'cost_ledger_maxTreeId:',
+                uuid: true,
+            });
+            // this.depart = ctx.app.config.table_depart.light;
+            this.tableName = 'cost_stage_analysis';
+        }
+
+        // 继承方法
+        clearParentingData(data) {
+            for (const f of costFields.sumFields) {
+                data[f] = 0;
+            }
+        }
+
+        _getDefaultData(data, stage) {
+            data.id = this.uuid.v4();
+            data.cost_id = data.id;
+            data.tender_id = stage.tid;
+            data.stage_id = stage.id;
+            data.add_user_id = this.ctx.session.sessionUser.accountId;
+            data.update_user_id = data.add_user_id;
+        }
+
+        async getEditData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.editQueryFields
+            });
+            return result;
+        }
+        async getReadData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.readQueryFields
+            });
+            result.forEach(x => {
+                x.calc_read = x.calc_read ? JSON.parse(x.calc_read) : {};
+                for(const prop of costFields.sumFields) {
+                    x[prop] = x.calc_read[prop] || 0;
+                }
+            });
+            return result;
+        }
+        async getCompareData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.compareQueryFields
+            });
+            result.forEach(x => {
+                x.calc_his = x.calc_his ? JSON.parse(x.calc_his) : [];
+            });
+            return result;
+        }
+
+        async getCostStageData(costStage) {
+            if (costStage.rela_stage.sid) costStage.relaStage = await this.ctx.service.costStage.getStage(costStage.rela_stage.sid);
+            const ledgerData = costStage.relaStage
+                ? await this.ctx.service.costStageLedger.getReadData(costStage.relaStage)
+                : await this.ctx.service.costStageLedger.getReadData(costStage);
+            if (costStage.relaStage) {
+                const bookData = await this.ctx.service.costStageBook.getReadData(costStage);
+                this.ctx.helper.assignRelaData(ledgerData, [
+                    { data: bookData, fields: ['end_in_tp', 'end_in_excl_tax_tp'], prefix: '', relaId: 'ledger_id' },
+                ]);
+            }
+            const detail = costStage.relaStage
+                ? await this.ctx.service.costStageBookDetail.getEndData(costStage.relaStage)
+                : await this.ctx.service.costStageDetail.getEndData(costStage);
+            const ledger = new Ledger.baseTree(this.ctx, {
+                id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
+                level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
+                rootId: -1, calcField: [],
+            });
+            ledger.loadDatas(ledgerData);
+            ledger.nodes.forEach(x => {
+                if (x.tree_is_leaf) x.detail = detail.filter(d => { return x.cost_id === d.cost_id; });
+            });
+            return ledger;
+        }
+        async init(stage, transaction) {
+            if (!this.ctx.subProject.cost_analysis_template) throw '未设置成本分析模板,请联系管理员设置';
+            if (!stage || !transaction) throw '成本分析数据错误';
+            const userId = this.ctx.session.sessionUser.accountId;
+
+            const templateData = await this.ctx.service.tenderNodeTemplate.getData(this.ctx.subProject.cost_analysis_template);
+            if (templateData.length === 0) throw '台账模板无数据,连请联系管理员修改';
+            const calcTemplate = await this.ctx.service.calcTmpl.getTemplate(stage.calc_template);
+            if (!calcTemplate) throw '计算模板未定义,请联系管理员设置';
+
+            const insertTemplateData = [], insertLedgerData = [], insertDetailData = [];
+            let maxId = 0;
+            for (const b of templateData) {
+                const bills = {
+                    tree_id: b.template_id, tree_pid: b.pid, tree_order: b.order, tree_level: b.level, tree_full_path: b.full_path, tree_is_leaf: b.is_leaf,
+                    code: b.code || '', name: b.name || '', unit: b.unit || '', node_type: b.node_type || 0,
+                    calc_type: b.node_type === 2 ? 2 : ([3, 4].indexOf(b.node_type) >= 0 ? 0 : 1),
+                };
+
+                maxId = Math.max(maxId, bills.tree_id);
+                this._getDefaultData(bills, stage);
+                insertTemplateData.push(bills);
+            }
+            const relaStage = await this.service.costStage.getStage(JSON.parse(stage.rela_stage).sid);
+            const costStageLedger = await this.getCostStageData(relaStage);
+            const costNode = insertTemplateData.find(x => { return x.node_type === 2 && x.tree_level === 1; });
+            if (costStageLedger.nodes.length > 0) costNode.tree_is_leaf = 0;
+            for (const [i, node] of costStageLedger.nodes.entries()) {
+                const parent = costStageLedger.getParent(node);
+                const newParent = parent ? insertLedgerData.find(x => { return x.tree_id === parent.new_tree_id; }) : costNode;
+                const ln = { calc_type: 2 };
+                this._getDefaultData(ln, stage);
+                ln.node_type = 0;
+                ln.cost_id = node.cost_id;
+                ln.tree_id = maxId + i + 1;
+                node.new_tree_id = ln.tree_id;
+                ln.tree_pid = newParent.tree_id;
+                ln.tree_order = node.tree_order;
+                ln.tree_level = newParent.tree_level + 1;
+                ln.tree_full_path = newParent.tree_full_path + '-' + ln.tree_id;
+                ln.tree_is_leaf = node.tree_is_leaf;
+                ln.code = node.code;
+                ln.name = node.name;
+                ln.unit = node.unit;
+                ln.tax = 0;
+                for (const prop of costFields.sumFields) {
+                    ln[prop] = 0;
+                }
+                insertLedgerData.push(ln);
+                if (node.detail) {
+                    for (const [i, d] of node.detail.entries()) {
+                        const idd = {
+                            id: this.uuid.v4(), tender_id: stage.tid, stage_id: stage.id,
+                            add_user_id: userId, update_user_id: userId,
+                            ledger_id: ln.id, cost_id: ln.cost_id, source_id: d.source_cid,
+                            d_order: i+1,
+                            code: d.code, name: d.name, party_b: d.party_b || '', tax: d.tax || 0,
+                            yf_excl_tax_tp: d.yf_excl_tax_tp || 0, in_excl_tax_tp: d.in_excl_tax_tp || 0, sf_excl_tax_tp: d.sf_excl_tax_tp || 0,
+                        };
+                        this.ctx.service.calcTmpl.calcByTemplate(idd, idd, idd, calcTemplate.calc_expr);
+                        idd.sf_percent = idd.yf_excl_tax_tp ? this.ctx.helper.mul(this.ctx.helper.div(idd.sf_excl_tax_tp, idd.yf_excl_tax_tp), 100, 2) : 0;
+                        insertDetailData.push(idd);
+                        for (const prop of costFields.sumFields) {
+                            ln[prop] = this.ctx.helper.add(ln[prop], idd[prop]);
+                        }
+                    }
+                } else {
+                    ln.tax = node.tax;
+                    ln.yf_excl_tax_tp = node.end_yf_excl_tax_tp || 0;
+                    ln.in_excl_tax_tp = node.end_in_excl_tax_tp || 0;
+                    ln.sf_excl_tax_tp = node.end_sf_excl_tax_tp || 0;
+                    this.ctx.service.calcTmpl.calcByTemplate(ln, ln, ln, calcTemplate.calc_expr);
+                }
+                ln.sf_percent = ln.yf_excl_tax_tp ? this.ctx.helper.mul(this.ctx.helper.div(ln.sf_excl_tax_tp, ln.yf_excl_tax_tp), 100, 2) : 0;
+            }
+
+            await transaction.insert(this.tableName, insertTemplateData);
+            await transaction.insert(this.tableName, insertLedgerData);
+            if (insertDetailData.length > 0) await transaction.insert(this.ctx.service.costStageAnalysisDetail.tableName, insertDetailData);
+        }
+        async initByPre(stage, preStage, transaction) {
+            if (!stage || !preStage || !transaction) throw '成本分析数据错误';
+            const userId = this.ctx.session.sessionUser.accountId;
+            const insertData = [], insertDetailData = [];
+            let maxId = 0;
+
+            const preBills = await this.getAllDataByCondition({
+                where: { stage_id: preStage.id },
+                columns: costFields.readQueryFields
+            });
+            const preLedger = Ledger.baseTree(this.ctx, {
+                id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
+                level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
+                rootId: -1, calcField: [],
+            });
+            preLedger.loadDatas(preBills);
+            const preDetails = await this.ctx.service.costStageAnalysisDetail.getReadData(preStage);
+            for (const pl of preLedger.nodes) {
+                if (pl.tree_is_leaf) pl.detail = preDetails.filter(x => { return x.ledger_id === pl.id; });
+                if (pl.tree_level > 1) {
+                    const parent = preLedger.getTopParent(pl);
+                    if (parent.node_type === 2) continue;
+                }
+                const idata = {};
+                this._getDefaultData(idata, stage);
+                idata.cost_id = pl.cost_id;
+                for (const prop of costFields.preCopyFields) {
+                    idata[prop] = pl[prop];
+                }
+                maxId = Math.max(maxId, idata.tree_id);
+                insertData.push(idata);
+            }
+
+            const relaStage = await this.service.costStage.getStage(stage.rela_stage.sid);
+            const costStageLedger = await this.getCostStageData(relaStage);
+            const costNode = insertData.find(x => { return x.node_type === 2 && x.tree_level === 1; });
+            if (costStageLedger.nodes.length > 0) costNode.tree_is_leaf = 0;
+            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 = {};
+                this._getDefaultData(ln, stage);
+                ln.node_type = 0;
+                ln.cost_id = node.cost_id;
+                ln.tree_id = maxId + i + 1;
+                node.new_tree_id = ln.tree_id;
+                ln.tree_pid = newParent.tree_id;
+                ln.tree_order = node.tree_order;
+                ln.tree_level = newParent.tree_level + 1;
+                ln.tree_full_path = newParent.tree_full_path + '-' + ln.tree_id;
+                ln.tree_is_leaf = node.tree_is_leaf;
+                ln.code = node.code;
+                ln.name = node.name;
+                ln.unit = node.unit;
+                ln.tax = node.tax;
+                ln.yf_excl_tax_tp = node.end_yf_excl_tax_tp;
+                ln.in_excl_tax_tp = node.end_in_excl_tax_tp;
+                ln.sf_excl_tax_tp = node.end_sf_excl_tax_tp;
+                insertData.push(ln);
+                const preSource = preLedger.nodes.find(x => { return x.cost_id === node.cost_id; });
+                for (const prop of costFields.selfTextFields) {
+                    ln[prop] = preSource ? preSource[prop] : '';
+                }
+                if (!node.detail) {
+                    for (const prop of costFields.selfCalcFields) {
+                        ln[prop] = preSource ? preSource[prop] : 0;
+                    }
+                } else {
+                    for (const [i, d] of node.detail.entries()) {
+                        const ld = {
+                            id: this.uuid.v4(), tender_id: stage.tid, stage_id: stage.id,
+                            add_user_id: userId, update_user_id: userId,
+                            ledger_id: ln.id, cost_id: ln.cost_id, source_id: d.source_cid,
+                            d_order: i+1,
+                            code: d.code, name: d.name, party_b: d.party_b || '', tax: ln.tax || 0,
+                            yf_excl_tax_tp: d.yf_excl_tax_tp, in_excl_tax_tp: d.in_excl_tax_tp, sf_excl_tax_tp: d.sf_excl_tax_tp,
+                            is_deal: d.is_deal,
+                        };
+                        const preDetailSource = preSource ? preSource.detail.find(x => { return x.source_id === d.source_cid; }) : null;
+                        for (const prop of costFields.selfTextFields) {
+                            ld[prop] = preDetailSource ? preDetailSource[prop] : '';
+                        }
+                        for (const prop of costFields.selfCalcFields) {
+                            ld[prop] = preDetailSource ? preDetailSource[prop] : 0;
+                            ln[prop] = this.ctx.helper.add(ln[prop], ld[prop]);
+                        }
+                        insertDetailData.push(ld);
+                    }
+                }
+            }
+
+            await transaction.insert(this.tableName, insertData);
+            if (insertDetailData.length > 0) await transaction.insert(this.ctx.service.costStageAnalysisDetail.tableName, insertDetailData);
+        }
+        async initStageData(transaction, stage, preStage) {
+            try {
+                if (preStage) {
+                    await this.initByPre(stage, preStage, transaction);
+                } else {
+                    await this.init(stage, transaction);
+                }
+            } catch(err) {
+                this.ctx.log(err);
+                throw '初始化数据错误';
+            }
+        }
+
+        /**
+         * 新增数据(供内部或其他service类调用, controller不可直接使用)
+         * @param {Array|Object} data - 新增数据
+         * @param {Number} stageId - 期id
+         * @param {Object} transaction - 新增事务
+         * @return {Promise<boolean>} - {Promise<是否正确新增成功>}
+         */
+        async innerAdd(data, stageId, transaction) {
+            const datas = data instanceof Array ? data : [data];
+            if (!stageId) {
+                throw '期id错误';
+            }
+            if (datas.length <= 0) {
+                throw '插入数据为空';
+            }
+            if (!transaction) {
+                throw '内部错误';
+            }
+            // 整理数据
+            const insertData = [];
+            for (const tmp of datas) {
+                tmp[this.setting.id] = tmp.template_id;
+                tmp[this.setting.pid] = tmp.pid;
+                tmp[this.setting.mid] = stageId;
+                delete tmp.template_id;
+                delete tmp.pid;
+                tmp.id = this.uuid.v4();
+                insertData.push(tmp);
+            }
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === datas.length;
+        }
+        /**
+         * 新增数据
+         *
+         * @param {Object} data - 新增的数据(可批量)
+         * @param {Number} stageId - 安全计量期id
+         * @return {Boolean} - 返回新增的结果
+         */
+        async add(data, stageId) {
+            this.transaction = await this.db.beginTransaction();
+            let result = false;
+            try {
+                result = await this.innerAdd(data, stageId, this.transaction);
+                if (!result) {
+                    throw '新增数据错误';
+                }
+                await this.transaction.commit();
+            } catch (error) {
+                await this.transaction.rollback();
+                result = false;
+            }
+
+            return result;
+        }
+
+        /**
+         * 根据节点Id获取数据
+         *
+         * @param {Number} stageId - 安全计量期id
+         * @param {Number} nodeId - 项目节/工程量清单节点id
+         * @return {Object} - 返回查询到的节点数据
+         */
+        async getDataByNodeId(stageId, nodeId) {
+            if ((nodeId <= 0) || (stageId <= 0)) {
+                return undefined;
+            }
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodeId;
+            const data = await this.db.getDataByCondition(where);
+
+            return data;
+        }
+        /**
+         * 根据节点Id获取数据
+         * @param {Number} stageId - 期Id
+         * @param {Array} nodesIds - 节点Id
+         * @return {Array}
+         */
+        async getDataByNodeIds(stageId, nodesIds) {
+            if (stageId <= 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodesIds;
+            const data = await this.db.getAllDataByCondition({ where });
+
+            return this._.sortBy(data, function(d) {
+                return nodesIds.indexOf(d[this.setting.id]);
+            });
+        }
+        /**
+         * 根据主键id获取数据
+         * @param {Array|Number} id - 主键id
+         * @return {Promise<*>}
+         */
+        async getDataByIds(id) {
+            if (!id) return [];
+            const ids = id instanceof Array ? id : [id];
+            if (ids.length === 0) return [];
+
+            const data = await this.db.getAllDataByCondition({ where: { id: ids } });
+            return data;
+        }
+
+        /**
+         * 根据 父节点id 获取子节点
+         * @param stageId
+         * @param nodeId
+         * @return {Promise<*>}
+         */
+        async getChildrenByParentId(stageId, nodeId) {
+            if (stageId <= 0 || !nodeId) {
+                return undefined;
+            }
+            const nodeIds = nodeId instanceof Array ? nodeId : [nodeId];
+            if (nodeIds.length === 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.pid] = nodeIds;
+            const data = await this.getAllDataByCondition({ where, orders: [[this.setting.order, 'ASC']] });
+
+            return data;
+        }
+
+        async addAnalysisNode(stage, targetId, count) {
+            if (!stage) return null;
+
+            const select = targetId ? await this.getDataByKid(stage.id, targetId) : null;
+            if (targetId && !select) throw '新增节点数据错误';
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                if (select) await this._updateChildrenOrder(stage.id, select[this.setting.pid], select[this.setting.order] + 1, count);
+                const newDatas = [];
+                const maxId = await this._getMaxLid(stage.id);
+                for (let i = 1; i < count + 1; i++) {
+                    const newData = {};
+                    newData[this.setting.kid] = maxId + i;
+                    newData[this.setting.pid] = select ? select[this.setting.pid] : this.rootId;
+                    newData[this.setting.mid] = stage.id;
+                    newData[this.setting.level] = select ? select[this.setting.level] : 1;
+                    newData[this.setting.order] = select ? select[this.setting.order] + i : i;
+                    newData[this.setting.fullPath] = newData[this.setting.level] > 1
+                        ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
+                        : newData[this.setting.kid] + '';
+                    newData[this.setting.isLeaf] = true;
+                    newData.calc_type = calcType.in;
+                    this._getDefaultData(newData, stage);
+                    newDatas.push(newData);
+                }
+                const insertResult = await this.transaction.insert(this.tableName, newDatas);
+                this._cacheMaxLid(stage.id, maxId + count);
+
+                if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+                await this.transaction.commit();
+                this.transaction = null;
+            } catch (err) {
+                await this.transaction.rollback();
+                this.transaction = null;
+                throw err;
+            }
+
+            if (select) {
+                const createData = await this.getChildBetween(stage.id, select[this.setting.pid], select[this.setting.order], select[this.setting.order] + count + 1);
+                const updateData = await this.getNextsData(stage.id, select[this.setting.pid], select[this.setting.order] + count);
+                return {create: createData, update: updateData};
+            } else {
+                const createData = await this.getChildBetween(stage.id, -1, 0, count + 1);
+                return {create: createData};
+            }
+        }
+
+        async getProfitUpdateData(updateData) {
+            const leafNode = await this.getAllDataByCondition({ where: { stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
+            const profit = leafNode.find(x => { return x.node_type === 3; });
+            if (!profit) throw '成本分析基础数据错误,找不到利润节点';
+            const profitRate = leafNode.find(x => { return x.node_type === 4; });
+            if (!profitRate) throw '成本分析基础数据错误,找不到利润率节点';
+
+            const profitUpdate = { id: profit.id, tree_id: profit.tree_id, update_user_id: this.ctx.session.sessionUser.accountId };
+            const profitRateUpdate = { id: profitRate.id, tree_id: profitRate.tree_id, update_user_id: this.ctx.session.sessionUser.accountId };
+            const inSum = {};
+            for (const node of leafNode) {
+                const ud = updateData.find(x => { return x.id === node.id; });
+                const calcData = ud || node;
+                if (node.calc_type === 1) {
+                    for (const prop of costFields.sumFields) {
+                        profitUpdate[prop] = this.ctx.helper.add(profitUpdate[prop], calcData[prop] !== undefined ? calcData[prop] : node[prop]);
+                        inSum[prop] = this.ctx.helper.add(inSum[prop], calcData[prop] !== undefined ? calcData[prop] : node[prop]);
+                    }
+                } else if (node.calc_type === 2) {
+                    for (const prop of costFields.sumFields) {
+                        profitUpdate[prop] = this.ctx.helper.sub(profitUpdate[prop], calcData[prop] !== undefined ? calcData[prop] : node[prop]);
+                    }
+                }
+            }
+            this.ctx.service.calcTmpl.calcByTemplate(profitUpdate, profitUpdate, profit, this.ctx.costStage.calcTemplate.calc_expr);
+            for (const prop of costFields.sumFields) {
+                if (prop !== 'sf_percent') {
+                    profitRateUpdate[prop] = inSum[prop] ? this.ctx.helper.mul(this.ctx.helper.div(profitUpdate[prop], inSum[prop]), 100, 2) : 0;
+                } else {
+                    profitRateUpdate[prop] = 0;
+                }
+            }
+            return [profitUpdate, profitRateUpdate];
+        }
+        async updateCalc(stage, data) {
+            const helper = this.ctx.helper;
+            // 简单验证数据
+            if (!stage) throw '成本分析不存在';
+            const decimal = stage.decimal;
+            if (!data) throw '提交数据错误';
+            const datas = data instanceof Array ? data : [data];
+            const ids = datas.map(x => { return x.id; });
+            const orgData = await this.getAllDataByCondition({ where: { id: ids }});
+            const updateData = [];
+            for (const row of datas) {
+                const oData = orgData.find(x => { return x.id === row.id });
+                if (!oData || oData.stage_id !== stage.id || oData.tree_id !== row.tree_id) throw '提交数据错误';
+
+                let nData = { id: oData.id, tree_id: oData.tree_id, update_user_id: this.ctx.session.sessionUser.accountId };
+
+                // calc
+                nData.tax = row.tax !== undefined ? helper.round(row.tax || 0, decimal.tax) : oData.tax;
+                for (const field of costFields.textFields) {
+                    if (row[field] !== undefined) nData[field] = row[field] || '';
+                }
+                for (const field of costFields.selfTextFields) {
+                    if (row[field] !== undefined) nData[field] = row[field] || '';
+                }
+                this.ctx.service.calcTmpl.calcByTemplate(nData, row, oData, stage.calcTemplate.calc_expr);
+                updateData.sf_percent = updateData.yf_excl_tax_tp ? this.ctx.helper.mul(this.ctx.helper.div(updateData.sf_excl_tax_tp, updateData.yf_excl_tax_tp), 100, 2) : 0;
+                updateData.push(nData);
+            }
+            const profitUpdate = await this.getProfitUpdateData(updateData);
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.updateRows(this.tableName, updateData);
+                await conn.updateRows(this.tableName, profitUpdate);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                throw '计算错误';
+            }
+
+            return { update: [...updateData, ...profitUpdate] };
+        }
+        async importPay(stage, payOrder, payTax) {
+            const payNode = await this.getDataByCondition({ stage_id: stage.id, node_type: 1 });
+            if (!payNode) throw '找不到计价收入节点';
+
+            const payUpdate = { id: payNode.id, tree_id: payNode.tree_id, update_user_id: this.ctx.session.sessionUser.accountId, tax: payTax || 0 };
+            const divNum = payUpdate.tax ? this.ctx.helper.add(1, this.ctx.helper.div(payUpdate.tax, 100)) : 1;
+            let payStage;
+            if (this.ctx.subProject.page_show.phasePay) {
+                payStage = await this.ctx.service.phasePay.getPhasePayByOrder(stage.tid, payOrder);
+                if (!payStage) throw '选择的合同支付期不存在';
+            } else {
+                payStage = await this.ctx.service.phasePay.getDataByCondition({ tid: stage.tid, order: payOrder });
+                if (!payStage) throw '选择的计量期不存在';
+            }
+            const yf = this.ctx.helper.add(payStage.yf_tp, payStage.pre_yf_tp);
+            const sf = this.ctx.helper.add(payStage.sf_tp, payStage.pre_sf_tp);
+            payUpdate.yf_excl_tax_tp = this.ctx.helper.div(yf, divNum, this.ctx.costStage.decimal.tp);
+            payUpdate.sf_excl_tax_tp = this.ctx.helper.div(sf, divNum, this.ctx.costStage.decimal.tp);
+            this.ctx.service.calcTmpl.calcByTemplate(payUpdate, payUpdate, payNode, stage.calcTemplate.calc_expr);
+
+            const profitUpdate = await this.getProfitUpdateData([payUpdate]);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.tableName, payUpdate);
+                await conn.updateRows(this.tableName, profitUpdate);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                throw '计算错误';
+            }
+            return { update: [payUpdate, ...profitUpdate] };
+        }
+        async reCalcProfit(stageId) {
+            if (!this.ctx.costStage) {
+                const costStage = await this.service.costStage.getStage(stageId);
+                if (costStage.calc_template) costStage.calcTemplate = await this.service.calcTmpl.getTemplate(costStage.calc_template);
+                this.ctx.costStage = costStage;
+            }
+            const profitUpdate = await this.getProfitUpdateData([]);
+            await this.db.updateRows(this.tableName, profitUpdate);
+        }
+
+        async auditCache(transaction, stageId, auditInfo) {
+            const leaf = await this.getAllDataByCondition({ where: { stage_id: stageId, tree_is_leaf: true} });
+            const updateData = [];
+            for (const l of leaf) {
+                l.calc_read = l.calc_read ? JSON.parse(l.calc_read) : {};
+                const diff = costFields.sumFields.find(x => {
+                    return l[x] !== l.calc_read[x];
+                });
+                if (diff) {
+                    const data = { id: l.id, calc_read: {} };
+                    // cache read
+                    for (const f of costFields.sumFields) {
+                        data.calc_read[f] = l[f];
+                    }
+                    data.calc_read = JSON.stringify(data.calc_read);
+                    // cache his
+                    const his = l.calc_his ? JSON.parse(l.calc_his) : [];
+                    const fi = his.find(x => { return x.audit_times === auditInfo.audit_times && x.active_order === auditInfo.active_order; });
+                    if (fi >= 0) his.splice(fi, 1);
+                    const newHis = { ...auditInfo };
+                    for (const f of costFields.sumFields) {
+                        newHis[f] = data[f];
+                    }
+                    his.push(newHis);
+                    data.calc_his = JSON.stringify(his);
+                    updateData.push(data);
+                }
+            }
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+        }
+
+        async setDecimal(decimal) {
+            if (!this.ctx.costStage) throw '读取数据错误';
+            if (this.ctx.costStage.stage_order !== this.ctx.costStage.latestOrder) throw '往期不可修改小数位数';
+            if (this.ctx.costStage.audit_status !== auditConst.status.uncheck && this.ctx.costStage.audit_status !== auditConst.status.checkNo) throw '仅原报可修改小数位数';
+            const orgDecimal = this.ctx.costStage.decimal;
+
+            const calcTp = decimal.tp < orgDecimal.tp;
+            this.ctx.costStage.decimal = { tp: decimal.tp, tax: decimal.tax || this.ctx.costStage.decimal.tax };
+
+            const updateData = [];
+            if (calcTp) {
+                const calcData = await this.getAllDataByCondition({ where: {stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
+                for (const cd of calcData) {
+                    const nd = { id: cd.id, tree_id: cd.tree_id };
+                    for (const prop of costFields.calcFields) {
+                        nd[prop] = this.ctx.helper.round(nd[prop], decimal.tp);
+                    }
+                    updateData.push(nd);
+                }
+            }
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.ctx.service.costStage.tableName, { id: this.ctx.costStage.id, decimal: JSON.stringify(this.ctx.costStage.decimal)});
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                await conn.commit();
+                return { calc: calcTp, update: updateData };
+            } catch(err) {
+                await conn.rollback();
+                return { calc: false, update: [] };
+            }
+
+        }
+
+        async getSum(stage) {
+            const sumFields = costFields.calcFields.map(f => { return `SUM(${f}) AS ${f}`});
+            const result = await this.db.queryOne(`SELECT ${sumFields.join(', ')} FROM ${this.tableName} where stage_id = ?`, [stage.id]);
+            return result;
+        }
+    }
+
+    return CostStageAnalysis;
+};

+ 159 - 0
app/service/cost_stage_analysis_detail.js

@@ -0,0 +1,159 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const costFields = {
+    textFields: ['code', 'name', 'party_b', 'postil'],
+    selfTextFields: ['str1', 'str2', 'str3', 'str4'],
+    calcFields: ['yf_excl_tax_tp', 'in_excl_tax_tp', 'sf_excl_tax_tp', 'sf_percent'],
+    selfCalcFields: ['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'],
+    readFields: ['calc_read'],
+    hisFields: ['calc_his'],
+    taxFields: ['tax'],
+    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'ledger_id', 'source_id', 'is_deal'],
+};
+costFields.preCopyFields = [...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.readFields, ...costFields.taxFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.taxFields, ...costFields.hisFields];
+
+module.exports = app => {
+    class CostStageDetail extends app.BaseService {
+
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'cost_stage_analysis_detail';
+        }
+
+        async getEditData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.editQueryFields
+            });
+            return result;
+        }
+        async getReadData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.readQueryFields
+            });
+            result.forEach(x => {
+                x.calc_read = x.calc_read ? JSON.parse(x.calc_read) : {};
+                for(const prop of costFields.calcFields) {
+                    x[prop] = x.calc_read[prop] || 0;
+                }
+            });
+            return result;
+        }
+        async getCompareData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.compareQueryFields
+            });
+            result.forEach(x => {
+                x.calc_his = x.calc_his ? JSON.parse(x.calc_his) : [];
+            });
+            return result;
+        }
+
+        async _getLedgerUpdateData(data, ledger_id, details) {
+            const detailDatas = details || await this.getAllDataByCondition({ columns: ['id', 'ledger_id', ...costFields.calcFields], where: { ledger_id, stage_id: this.ctx.costStage.id } });
+
+            const curCalcFields = costFields.selfCalcFields.filter(x => { return data[0][x] !== undefined; });
+            const updateData = { id: ledger_id, update_user_id: this.ctx.session.sessionUser.accountId };
+            for (const d of data) {
+                for (const prop of curCalcFields) {
+                    updateData[prop] = this.ctx.helper.add(updateData[prop], d[prop] || 0);
+                }
+            }
+            for (const d of detailDatas) {
+                if (data.findIndex(x => { return x.id === d.id; }) >= 0) continue;
+                for (const prop of curCalcFields) {
+                    updateData[prop] = this.ctx.helper.add(updateData[prop], d[prop] || 0);
+                }
+            }
+            return updateData;
+        }
+        async updateDatas(data) {
+            if (!data || data.length === 0) throw '提交数据错误';
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const datas = data instanceof Array ? data : [data];
+            const orgDataIds = this.ctx.helper._.map(datas.filter(x => { return x.id }), 'id');
+            const orgDatas = orgDataIds.length > 0 ? await this.getAllDataByCondition({ where: { id: orgDataIds } }) : [];
+
+            const updateDetail = [];
+            for (const d of datas) {
+                const od = orgDatas.find(x => { return x.id === d.id; });
+                if (!od) throw '数据错误';
+
+                const nd = { id: od.id, update_user_id: user_id };
+                for (const prop of costFields.textFields) {
+                    if (d[prop] !== undefined) nd[prop] = d[prop];
+                }
+                for (const prop of costFields.selfTextFields) {
+                    if (d[prop] !== undefined) nd[prop] = d[prop];
+                }
+                this.ctx.service.calcTmpl.calcByTemplate(nd, d, od, this.ctx.costStage.calcTemplate.calc_expr, {});
+
+                updateDetail.push(nd);
+            }
+            const billsUpdate = await this._getLedgerUpdateData(updateDetail, orgDatas[0].ledger_id);
+            const bills = await this.ctx.service.costStageAnalysis.getDataById(billsUpdate.id);
+            billsUpdate.tree_id = bills.tree_id;
+            const profitUpdate = await this.ctx.service.costStageAnalysis.getProfitUpdateData([billsUpdate]);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.updateRows(this.tableName, updateDetail);
+                await conn.update(this.ctx.service.costStageAnalysis.tableName, billsUpdate);
+                await conn.updateRows(this.ctx.service.costStageAnalysis.tableName, profitUpdate);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return { detail: { update: updateDetail }, ledger: [billsUpdate, ...profitUpdate] };
+        }
+
+        async auditCache(transaction, stageId, auditInfo) {
+            const leaf = await this.getAllDataByCondition({ where: { stage_id: stageId} });
+            const updateData = [];
+            for (const l of leaf) {
+                l.calc_read = l.calc_read ? JSON.parse(l.calc_read) : {};
+                const diff = costFields.calcFields.find(x => {
+                    return l[x] !== l.calc_read[x];
+                });
+                if (diff) {
+                    const data = { id: l.id, calc_read: {} };
+                    // cache read
+                    for (const f of costFields.calcFields) {
+                        data.calc_read[f] = l[f];
+                    }
+                    data.calc_read = JSON.stringify(data.calc_read);
+                    // cache his
+                    const his = l.calc_his ? JSON.parse(l.calc_his) : [];
+                    const fi = his.find(x => { return x.audit_times === auditInfo.audit_times && x.active_order === auditInfo.active_order; });
+                    if (fi >= 0) his.splice(fi, 1);
+                    const newHis = { ...auditInfo };
+                    for (const f of costFields.calcFields) {
+                        newHis[f] = data[f];
+                    }
+                    his.push(newHis);
+                    data.calc_his = JSON.stringify(his);
+                    updateData.push(data);
+                }
+            }
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+        }
+    }
+
+    return CostStageDetail;
+};
+

+ 35 - 0
app/service/cost_stage_book.js

@@ -152,6 +152,41 @@ module.exports = app => {
             });
             return { update: [...insertDetail, ...updateDetail] };
         }
+
+        async auditCache(transaction, stageId, auditInfo) {
+            const leaf = await this.getAllDataByCondition({ where: { stage_id: stageId } });
+            const updateData = [];
+            for (const l of leaf) {
+                const diff = costFields.curFields.find(x => {
+                    return l[x] !== l['read_' + x];
+                });
+                if (diff) {
+                    const data = { id: l.id };
+                    // cache read
+                    for (const f of costFields.curFields) {
+                        data['read_' + f] = l[f];
+                    }
+                    // cache his
+                    const his = l.calc_his ? JSON.parse(l.calc_his) : [];
+                    const fi = his.find(x => { return x.audit_times === auditInfo.audit_times && x.active_order === auditInfo.active_order; });
+                    if (fi >= 0) his.splice(fi, 1);
+                    const newHis = { ...auditInfo };
+                    for (const f of costFields.curFields) {
+                        newHis[f] = data[f];
+                    }
+                    his.push(newHis);
+                    data.calc_his = JSON.stringify(his);
+                    updateData.push(data);
+                }
+            }
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+        }
+
+        async getSum(stage) {
+            const sumFields = costFields.curFields.map(f => { return `SUM(${f}) AS ${f}`});
+            const result = await this.db.queryOne(`SELECT ${sumFields.join(', ')} FROM ${this.tableName} where stage_id = ?`, [stage.id]);
+            return result;
+        }
     }
 
     return CostStageBook;

+ 142 - 95
app/service/cost_stage_book_detail.js

@@ -56,127 +56,174 @@ module.exports = app => {
             });
             return result;
         }
+        async getEndData(stage) {
+            const commonDetailSql =
+                'SELECT sd.cost_id, sd.code, sd.name, sd.tax, sd.is_deal, sd.id AS source_cid, sd.pay_tp, sd.cut_tp, sd.yf_tp, sd.sf_tp, sd.yf_excl_tax_tp, sd.sf_excl_tax_tp, sbd.in_tp, sbd.in_excl_tax_tp ' +
+                `  FROM ${this.ctx.service.costStageDetail.tableName} sd LEFT JOIN ${this.tableName} sbd ON sbd.detail_id = sd.id` +
+                `    LEFT JOIN ${this.ctx.service.costStage.tableName} cs ON sd.stage_id = cs.id` +
+                '  WHERE cs.tid = ? AND cs.stage_order <= ? AND sd.is_deal = 0';
+            const commonDetail = await this.db.query(commonDetailSql, [stage.tid, stage.stage_order]);
+            // 以下sql需要8.0的mysql才支持
+            // const dealDetailSql =
+            //     'SELECT * FROM ( ' +
+            //     '  SELECT sd.cost_id, sd.code, sd.name, sd.tax, sd.source_cid AS source_id,' +
+            //     '      sum(sd.pay_tp) OVER (PARTITION BY sd.source_cid) AS pay_tp,' +
+            //     '      sum(sd.cut_tp) OVER (PARTITION BY sd.source_cid) AS cut_tp,' +
+            //     '      sum(sd.yf_tp) OVER (PARTITION BY sd.source_cid) AS yf_tp,' +
+            //     '      sum(sd.sf_tp) OVER (PARTITION BY sd.source_cid) AS sf_tp,' +
+            //     '      sum(sd.yf_excl_tax_tp) OVER (PARTITION BY sd.source_cid) AS yf_excl_tax_tp,' +
+            //     '      sum(sd.sf_excl_tax_tp) OVER (PARTITION BY sd.source_cid) AS sf_excl_tax_tp,' +
+            //     '      sum(sbd.in_tp) OVER (PARTITION BY sd.source_cid) AS in_tp,' +
+            //     '      sum(sbd.in_excl_tax_tp) OVER (PARTITION BY sd.source_cid) AS in_excl_tax_tp,' +
+            //     '      ROW_NUMBER() OVER (PATITION BY sd.source_cid ORDER BY create_time DESC) AS rn' +
+            //     `    FROM ${this.ctx.service.costStageDetail.tableName} sd LEFT JOIN ${this.tableName} sbd ON sbd.detail_id = sd.id` +
+            //     `      LEFT JOIN ${this.ctx.service.costStage.tableName} cs ON sd.stage_id = cs.id` +
+            //     '    WHERE cs.tid = ? AND cs.stage_order <= ? AND sd.is_deal = 1' +
+            //     '  ) t WHERE t.rn = 1';
+            const dealDetailSql =
+                'SELECT a.cost_id, a.code, a.name, a.tax, a.is_deaL, b.* FROM (' +
+                '  SELECT sd.cost_id, sd.code, sd.name, sd.tax, sd.is_deal, sd.source_cid' +
+                '    FROM (' +
+                '      SELECT source_cid, max(add_time) AS latest_time FROM zh_cost_stage_detail WHERE tender_id = ? AND stage_order <= ? AND is_deal = 1 GROUP BY source_cid' +
+                '    ) lastTable LEFT JOIN zh_cost_stage_detail sd ON lastTable.latest_time = sd.add_time AND sd.source_cid = lastTable.source_cid' +
+                '  ) a' +
+                '  LEFT JOIN ( ' +
+                '    SELECT csd.source_cid, sum(csd.pay_tp) AS pay_tp, sum(csd.cut_tp) AS cut_tp, ' +
+                '        sum(csd.yf_tp) AS yf_tp, sum(csd.sf_tp) AS sf_tp, ' +
+                '        sum(csd.yf_excl_tax_tp) AS yf_excl_tax_tp, sum(csd.sf_excl_tax_tp) AS sf_excl_tax_tp,' +
+                '        sum(csbd.in_tp) AS in_tp, sum(csbd.in_excl_tax_tp) As in_excl_tax_tp' +
+                '      FROM zh_cost_stage_detail csd LEFT JOIN zh_cost_stage_book_detail csbd ON csd.id = csbd.detail_id' +
+                '      WHERE csd.tender_id = ? AND csd.stage_order <= ? AND csd.is_deal = 1 GROUP BY csd.source_cid' +
+                '  ) b ON a.source_cid = b.source_cid';
+            const dealDetail = await this.db.query(dealDetailSql, [stage.tid, stage.stage_order, stage.tid, stage.stage_order]);
+            return [...commonDetail, ...dealDetail];
+        }
 
-        async _addDatas(data) {
-            const user_id = this.ctx.session.sessionUser.accountId;
+        async _getBookUpdateData(data, ledger_id, details) {
+            const detailDatas = details || await this.getAllDataByCondition({ columns: ['id', 'ledger_id', ...costFields.curFields], where: { ledger_id, stage_id: this.ctx.costStage.id } });
 
-            const datas = data instanceof Array ? data : [data];
-            const insertData = [];
-            for (const d of datas) {
-                if (!d.ledger_id || !d.cost_id || !d.d_order) throw '新增明细数据,提交的数据错误';
-                const nd = {
-                    id: this.uuid.v4(), tender_id: this.ctx.costStage.tid, stage_id: this.ctx.costStage.id,
-                    ledger_id: d.ledger_id, cost_id: d.cost_id, d_order: d.d_order,
-                    add_user_id: user_id, update_user_id: user_id,
-                };
-                for (const prop of costFields.textFields) {
-                    if (d[prop] !== undefined) nd[prop] = d[prop] || '';
+            const updateData = { id: ledger_id, update_user_id: this.ctx.session.sessionUser.accountId };
+            for (const d of data) {
+                for (const prop of costFields.curFields) {
+                    updateData[prop] = this.ctx.helper.add(updateData[prop], d[prop] || 0);
                 }
-                if (d.pay_tp !== undefined || d.cut_tp !== undefined) {
-                    nd.pay_tp = d.pay_tp !== undefined ? this.ctx.helper.round(d.pay_tp || 0, this.ctx.costStage.decimal.tp) : 0;
-                    nd.cut_tp = d.cut_tp !== undefined ? this.ctx.helper.round(d.cut_tp || 0, this.ctx.costStage.decimal.tp) : 0;
-                    nd.yf_tp = this.ctx.helper.sub(nd.pay_tp, nd.cut_tp);
-                    nd.sf_tp = nd.yf_tp;
-                }
-                if (d.sf_tp !== undefined) nd.sf_tp = this.ctx.helper.round(d.sf_tp || 0, this.ctx.costStage.decimal.tp);
-                if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
-                    nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.ctx.costStage.decimal.tax) : 0;
-                    const divNum = this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100));
-                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
-                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
-                }
-                insertData.push(nd);
             }
-            const billsUpdate = await this._getLedgerUpdateData(insertData, insertData[0].ledger_id);
-
-            const conn = await this.db.beginTransaction();
-            try {
-                await conn.insert(this.tableName, insertData);
-                await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
-                await conn.commit();
-            } catch(err) {
-                await conn.rollback();
-                throw err;
+            for (const d of detailDatas) {
+                if (data.findIndex(x => { return x.id === d.id; }) >= 0) continue;
+                for (const prop of costFields.curFields) {
+                    updateData[prop] = this.ctx.helper.add(updateData[prop], d[prop] || 0);
+                }
             }
-            const addData = await this.getAllDataByCondition({
-                where: { id: this.ctx.helper._.map(insertData, 'id') }
-            });
-            const ledgerData = await this.ctx.service.costStageLedger.getDataById(addData[0].ledger_id);
-            return [addData, ledgerData]
+            return updateData;
         }
-        async _updateDatas (data) {
+        async updateDatas(data) {
             if (!data || data.length === 0) throw '提交数据错误';
             const user_id = this.ctx.session.sessionUser.accountId;
 
             const datas = data instanceof Array ? data : [data];
-            const orgDatas = await this.getAllDataByCondition({
-                where: { id: this.ctx.helper._.map(datas, 'id') }
+            const orgDetail = await this.ctx.service.costStageDetail.getAllDataByCondition({
+                where: {
+                    stage_id: this.ctx.costStage.relaStage.id,
+                    id: this.ctx.helper._.map(datas, 'detail_id')
+                }
             });
-            if (!orgDatas || orgDatas.length === 0) throw '修改的明细不存在';
+            const orgDataIds = this.ctx.helper._.map(datas.filter(x => { return x.book_id }), 'book_id');
+            const orgDatas = orgDataIds.length > 0 ? await this.getAllDataByCondition({ where: { id: orgDataIds } }) : [];
 
-            const uDatas = [];
+            const updateDetail = [], insertDetail = [];
             for (const d of datas) {
-                const od = orgDatas.find(x => { return x.id === d.id; });
-                if (!od) continue;
+                const od = orgDatas.find(x => { return x.id === d.book_id; });
+                const cd = orgDetail.find(x => { return x.id === d.detail_id });
+                if (!cd) throw '关联期明细错误';
 
-                const nd = { id: od.id, update_user_id: user_id };
+                const nd = od
+                    ? { id: od.id, update_user_id: user_id }
+                    : { id: this.uuid.v4(), tender_id: this.ctx.costStage.tid, stage_id: this.ctx.costStage.id, ledger_id: d.ledger_id, detail_id: d.detail_id, add_user_id: user_id, update_user_id: user_id };
                 for (const prop of costFields.textFields) {
                     if (d[prop] !== undefined) nd[prop] = d[prop];
                 }
-                if (d.pay_tp !== undefined || d.cut_tp !== undefined) {
-                    nd.pay_tp = d.pay_tp !== undefined ? this.ctx.helper.round(d.pay_tp || 0, this.ctx.costStage.decimal.tp) : od.pay_tp;
-                    nd.cut_tp = d.cut_tp !== undefined ? this.ctx.helper.round(d.cut_tp || 0, this.ctx.costStage.decimal.tp) : od.cut_tp;
-                    nd.yf_tp = this.ctx.helper.sub(nd.pay_tp, nd.cut_tp);
-                    nd.sf_tp = od.yf_tp === od.sf_tp ? nd.yf_tp : od.sf_tp;
-                } else {
-                    nd.pay_tp = od.pay_tp;
-                    nd.cut_tp = od.cut_tp;
-                    nd.yf_tp = od.yf_tp;
-                    nd.sf_tp = od.sf_tp;
+                if (d.in_tp !== undefined ) {
+                    nd.in_tp = d.in_tp !== undefined ? this.ctx.helper.round(d.in_tp || 0, this.ctx.costStage.decimal.tp) : (od.in_tp ? od.in_tp : 0);
+                    const divNum = this.ctx.helper.add(1, this.ctx.helper.div(cd.tax, 100));
+                    nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp, divNum, this.ctx.costStage.decimal.tp);
                 }
-                if (d.sf_tp !== undefined) nd.sf_tp = this.ctx.helper.round(d.sf_tp || 0, this.ctx.costStage.decimal.tp);
-                if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
-                    nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.ctx.costStage.decimal.tax) : od.tax;
-                    const divNum = this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100));
-                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : od.yf_tp, divNum, this.ctx.costStage.decimal.tp);
-                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : od.sf_tp, divNum, this.ctx.costStage.decimal.tp);
+                if (od) {
+                    updateDetail.push(nd);
+                } else {
+                    insertDetail.push(nd);
                 }
-                uDatas.push(nd);
             }
-            if (uDatas.length > 0) {
-                const billsUpdate = await this._getLedgerUpdateData(uDatas, orgDatas[0].ledger_id);
-                const conn = await this.db.beginTransaction();
-                try {
-                    await conn.updateRows(this.tableName, uDatas);
-                    await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
-                    await conn.commit();
-                    return [uDatas, billsUpdate];
-                } catch(err) {
-                    await conn.rollback();
-                    throw err;
-                }
-            } else {
-                return [];
+            const bookUpdate = await this._getBookUpdateData([...insertDetail, ...updateDetail], orgDetail[0].ledger_id);
+            const orgBook = await this.ctx.service.costStageBook.getDataByCondition({ ledger_id: orgDetail[0].ledger_id });
+            const orgLedger = await this.ctx.service.costStageLedger.getDataById(orgDetail[0].ledger_id);
+            if (!orgBook) {
+                bookUpdate.tender_id = this.ctx.costStage.tid;
+                bookUpdate.stage_id = this.ctx.costStage.id;
+                bookUpdate.ledger_id = orgLedger.id;
+                bookUpdate.cost_id = orgLedger.cost_id;
+                bookUpdate.add_user_id = user_id;
             }
-        }
-        async updateDatas(data) {
-            const result = { detail: { add: [], del: [], update: [] }, ledger: {} };
+
+            const conn = await this.db.beginTransaction();
             try {
-                if (data.add) {
-                    [result.detail.add, result.ledger] = await this._addDatas(data.add);
-                }
-                if (data.update) {
-                    [result.detail.update, result.ledger] = await this._updateDatas(data.update);
+                if (updateDetail.length > 0) await conn.updateRows(this.tableName, updateDetail);
+                if (insertDetail.length > 0) await conn.insert(this.tableName, insertDetail);
+                if (orgBook) {
+                    await conn.update(this.ctx.service.costStageBook.tableName, bookUpdate);
+                } else {
+                    await conn.insert(this.ctx.service.costStageBook.tableName, bookUpdate);
                 }
-                return result;
+                await conn.commit();
             } catch (err) {
-                if (err.stack) {
-                    throw err;
-                } else {
-                    result.err = err.toString();
-                    return result;
+                await conn.rollback();
+                throw err;
+            }
+
+            insertDetail.forEach(i => {
+                i.book_id = i.id;
+                const cl = orgDetail.find(x => { return x.id === i.detail_id });
+                i.id = cl.id;
+            });
+            updateDetail.forEach(u => {
+                u.book_id = u.id;
+                const od = orgDatas.find(x => { return x.id === u.book_id; });
+                const cl = orgDetail.find(x => { return x.id === od.detail_id; });
+                u.id = cl.id;
+            });
+            bookUpdate.book_id = bookUpdate.id;
+            delete bookUpdate.id;
+            bookUpdate.tree_id = orgLedger.tree_id;
+            return { detail: { update: [...insertDetail, ...updateDetail] }, ledger: bookUpdate };
+        }
+
+
+        async auditCache(transaction, stageId, auditInfo) {
+            const leaf = await this.getAllDataByCondition({ where: { stage_id: stageId } });
+            const updateData = [];
+            for (const l of leaf) {
+                const diff = costFields.curFields.find(x => {
+                    return l[x] !== l['read_' + x];
+                });
+                if (diff) {
+                    const data = { id: l.id };
+                    // cache read
+                    for (const f of costFields.curFields) {
+                        data['read_' + f] = l[f];
+                    }
+                    // cache his
+                    const his = l.calc_his ? JSON.parse(l.calc_his) : [];
+                    const fi = his.find(x => { return x.audit_times === auditInfo.audit_times && x.active_order === auditInfo.active_order; });
+                    if (fi >= 0) his.splice(fi, 1);
+                    const newHis = { ...auditInfo };
+                    for (const f of costFields.curFields) {
+                        newHis[f] = data[f];
+                    }
+                    his.push(newHis);
+                    data.calc_his = JSON.stringify(his);
+                    updateData.push(data);
                 }
             }
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
         }
     }
 

+ 78 - 29
app/service/cost_stage_detail.js

@@ -57,9 +57,44 @@ module.exports = app => {
             });
             return result;
         }
+        async getEndData(stage) {
+            const commonDetailSql = 'SELECT sd.cost_id, sd.id AS source_id, sd.code, sd.name, sd.tax, sd.pay_tp, sd.cut_tp, sd.yf_tp, sd.sf_tp, sd.yf_excl_tax_tp, sd.sf_excl_tax_tp ' +
+                `  FROM ${this.tableName} sd LEFT JOIN ${this.ctx.service.costStage.tableName} cs ON sd.stage_id = cs.id` +
+                '  WHERE cs.tid = ? AND cs.stage_order <= ? AND sd.is_deal = 0';
+            const commonDetail = await this.db.query(commonDetailSql, [stage.tid, stage.stage_order]);
+            // 一下sql只有在mysqlV8.0以后才可使用
+            // const dealDetailSql =
+            //     'SELECT * FROM ( ' +
+            //     '  SELECT sd.cost_id, sd.code, sd.name, sd.tax, source_cid AS source_id,' +
+            //     '      sum(sd.pay_tp) OVER (PARTITION BY sd.source_cid) AS pay_tp,' +
+            //     '      sum(sd.cut_tp) OVER (PARTITION BY sd.source_cid) AS cut_tp,' +
+            //     '      sum(sd.yf_tp) OVER (PARTITION BY sd.source_cid) AS yf_tp,' +
+            //     '      sum(sd.sf_tp) OVER (PARTITION BY sd.source_cid) AS sf_tp,' +
+            //     '      sum(sd.yf_excl_tax_tp) OVER (PARTITION BY sd.source_cid) AS yf_excl_tax_tp,' +
+            //     '      sum(sd.sf_excl_tax_tp) OVER (PARTITION BY sd.source_cid) AS sf_excl_tax_tp,' +
+            //     '      ROW_NUMBER() OVER (PATITION BY sd.source_cid ORDER BY create_time DESC) AS rn' +
+            //     `    FROM ${this.tableName} sd LEFT JOIN ${this.ctx.service.costStage.tableName} cs ON sd.stage_id = cs.id` +
+            //     '    WHERE cs.tid = ? AND cs.stage_order <= ? AND sd.is_deal = 1' +
+            //     '  ) t WHERE t.rn = 1';
+            const dealDetailSql =
+                'SELECT a.cost_id, a.code, a.name, a.tax, b.* FROM (' +
+                '  SELECT sd.cost_id, sd.code, sd.name, sd.tax, sd.source_cid' +
+                '    FROM (' +
+                '      SELECT source_cid, max(add_time) AS latest_time FROM zh_cost_stage_detail WHERE tender_id = ? AND stage_order <= ? AND is_deal = 1 GROUP BY source_cid' +
+                '    ) lastTable LEFT JOIN zh_cost_stage_detail sd ON lastTable.latest_time = sd.add_time AND sd.source_cid = lastTable.source_cid' +
+                '  ) a' +
+                '  LEFT JOIN ( ' +
+                '    SELECT source_cid, sum(pay_tp) AS pay_tp, sum(cut_tp) AS cut_tp, ' +
+                '        sum(yf_tp) AS yf_tp, sum(sf_tp) AS sf_tp, ' +
+                '        sum(yf_excl_tax_tp) AS yf_excl_tax_tp, sum(sf_excl_tax_tp) AS sf_excl_tax_tp' +
+                '      FROM zh_cost_stage_detail WHERE tender_id = ? AND stage_order <= ? AND is_deal = 1 GROUP BY source_cid' +
+                ') b ON a.source_cid = b.source_cid';
+            const dealDetail = await this.db.query(dealDetailSql, [stage.tid, stage.stage_order, stage.tid, stage.stage_order]);
+            return [...commonDetail, ...dealDetail];
+        }
 
-        async _getLedgerUpdateData(data, ledger_id) {
-            const detailDatas = await this.getAllDataByCondition({ columns: ['id', 'ledger_id', ...costFields.curFields], where: { ledger_id, stage_id: this.ctx.costStage.id } });
+        async _getLedgerUpdateData(data, ledger_id, details) {
+            const detailDatas = details || await this.getAllDataByCondition({ columns: ['id', 'ledger_id', ...costFields.curFields], where: { ledger_id, stage_id: this.ctx.costStage.id } });
 
             const updateData = { id: ledger_id, update_user_id: this.ctx.session.sessionUser.accountId };
             for (const d of data) {
@@ -83,7 +118,8 @@ module.exports = app => {
             for (const d of datas) {
                 if (!d.ledger_id || !d.cost_id || !d.d_order) throw '新增明细数据,提交的数据错误';
                 const nd = {
-                    id: this.uuid.v4(), tender_id: this.ctx.costStage.tid, stage_id: this.ctx.costStage.id,
+                    id: this.uuid.v4(), tender_id: this.ctx.costStage.tid,
+                    stage_id: this.ctx.costStage.id, stage_order: this.ctx.costStage.stage_order,
                     ledger_id: d.ledger_id, cost_id: d.cost_id, d_order: d.d_order,
                     add_user_id: user_id, update_user_id: user_id,
                 };
@@ -99,7 +135,7 @@ module.exports = app => {
                 if (d.sf_tp !== undefined) nd.sf_tp = this.ctx.helper.round(d.sf_tp || 0, this.ctx.costStage.decimal.tp);
                 if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
                     nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.ctx.costStage.decimal.tax) : 0;
-                    const divNum = this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100));
+                    const divNum = nd.tax ? this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100)) : 1;
                     nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
                     nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
                 }
@@ -189,7 +225,7 @@ module.exports = app => {
                 if (d.sf_tp !== undefined) nd.sf_tp = this.ctx.helper.round(d.sf_tp || 0, this.ctx.costStage.decimal.tp);
                 if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
                     nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.ctx.costStage.decimal.tax) : od.tax;
-                    const divNum = this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100));
+                    const divNum = nd.tax ? this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100)) : 1;
                     nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : od.yf_tp, divNum, this.ctx.costStage.decimal.tp);
                     nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : od.sf_tp, divNum, this.ctx.costStage.decimal.tp);
                 }
@@ -234,32 +270,46 @@ module.exports = app => {
             }
         }
 
-        async _importContractByIds(ledgerId, costId, ids) {
+        async _importContractByIds(ledgerId, costId, ids, months) {
             if (!ledgerId || !costId) throw '导入明细数据,提交的数据错误';
             const user_id = this.ctx.session.sessionUser.accountId;
+            const times = months.split(' ~ ');
+            if (times.length < 0) throw '选择的时间无效';
+            const beginTime = this.ctx.moment(times[0], 'YYYY-MM').startOf('month').format('YYYY-MM-DD HH:mm:ss');
+            const endTime = this.ctx.moment(times[1] || times[0], 'YYYY-MM').endOf('month').format('YYYY-MM-DD HH:mm:ss');
+
+            const paySql = `SELECT cp.*, c.c_code, c.name, c.party_b FROM ${this.ctx.service.contractPay.tableName} cp LEFT JOIN ${this.ctx.service.contract.tableName} c ON cp.cid = c.id ` +
+                           `    WHERE cp.cid IN(${ids.map(x => { return `'${x}'`}).join(', ')}) AND cp.pay_time >= ? AND cp.pay_time <= ?`;
+            const validPays = await this.db.query(paySql, [beginTime, endTime]);
+
             const insertDetails = [];
-            const paySql = `SELECT cp.*, c.c_code, c.name, c.party_b FROM ${this.ctx.service.contractPay.tableName} cp LEFT JOIN ${this.ctx.service.contract.tableName} c ON cp.cid = c.id WHERE cp.cid IN(${ids.map(x => { return `'${x}'`}).join(', ')}) AND Date_FORMAT(cp.pay_time, '%Y-%m') = ?`;
-            const validPays = await this.db.query(paySql, [this.ctx.costStage.stage_date]);
-            const max = await this.db.queryOne(`SELECT Max(d_order) as max_order FROM ${this.tableName} WHERE stage_id = ?`, [this.ctx.costStage.id]);
-            let maxDetailOrder = max.max_order;
             for (const pay of validPays) {
-                const idetail = {
-                    id: this.uuid.v4(), tender_id: this.ctx.costStage.tid, stage_id: this.ctx.costStage.id, is_deal: 1,
-                    ledger_id: ledgerId, cost_id: costId, d_order: maxDetailOrder++,
-                    add_user_id: user_id, update_user_id: user_id,
-                    code: pay.c_code, name: pay.name, party_b: pay.party_b,
-                    pay_tp: pay.pay_price || 0, cut_tp: pay.debit_price || 0, yf_tp: pay.yf_price || 0, sf_tp: pay.sf_price || 0,
-                    tax: pay.tax || 0,
-                };
-                const divNum = this.ctx.helper.add(1, this.ctx.helper.div(idetail.tax, 100));
-                idetail.yf_excl_tax_tp = this.ctx.helper.div(idetail.yf_tp, divNum, this.ctx.costStage.decimal.tp);
-                idetail.sf_excl_tax_tp = this.ctx.helper.div(idetail.sf_tp, divNum, this.ctx.costStage.decimal.tp);
-                insertDetails.push(idetail);
+                let idetail = insertDetails.find(x => { return x.source_cid === pay.cid; });
+                if (!idetail) {
+                    idetail = {
+                        id: this.uuid.v4(), tender_id: this.ctx.costStage.tid, stage_id: this.ctx.costStage.id,
+                        stage_order: this.ctx.costStage.stage_order, is_deal: 1,
+                        ledger_id: ledgerId, cost_id: costId, d_order: insertDetails.length + 1, source_cid: pay.cid,
+                        add_user_id: user_id, update_user_id: user_id,
+                        code: pay.c_code, name: pay.name, party_b: pay.party_b,
+                    };
+                    insertDetails.push(idetail);
+                }
+                idetail.tax = pay.tax || 0;
+                idetail.pay_tp = this.ctx.helper.add(idetail.pay_tp, pay.pay_price);
+                idetail.cut_tp = this.ctx.helper.add(idetail.cut_tp, pay.debit_price);
+                idetail.yf_tp = this.ctx.helper.add(idetail.sf_tp, pay.yf_price);
+                idetail.sf_tp = this.ctx.helper.add(idetail.yf_tp, pay.sf_price);
+                const divNum = idetail.tax ? this.ctx.helper.add(1, this.ctx.helper.div(idetail.tax, 100)) : 1;
+                idetail.yf_excl_tax_tp = this.ctx.helper.add(idetail.yf_excl_tax_tp, this.ctx.helper.div(pay.yf_price, divNum, this.ctx.costStage.decimal.tp));
+                idetail.sf_excl_tax_tp = this.ctx.helper.add(idetail.sf_excl_tax_tp, this.ctx.helper.div(pay.sf_price, divNum, this.ctx.costStage.decimal.tp));
             }
-            const billsUpdate = await this._getLedgerUpdateData(insertDetails, ledgerId);
+            const detailDatas = await this.getAllDataByCondition({ columns: ['id'], where: { ledger_id: ledgerId, stage_id: this.ctx.costStage.id } });
+            const billsUpdate = await this._getLedgerUpdateData(insertDetails, ledgerId, []);
             billsUpdate.is_deal = 1;
             const conn = await this.db.beginTransaction();
             try {
+                await conn.delete(this.tableName, { stage_id: this.ctx.costStage.id, ledger_id: ledgerId });
                 await conn.insert(this.tableName, insertDetails);
                 await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
                 await conn.commit();
@@ -271,16 +321,15 @@ module.exports = app => {
                 where: { id: this.ctx.helper._.map(insertDetails, 'id') }
             });
             const ledgerData = await this.ctx.service.costStageLedger.getDataById(addData[0].ledger_id);
-            return { detail: { add: addData}, ledger: ledgerData };
+            return { detail: { add: addData, del: detailDatas.map(x => { return x.id; }) }, ledger: ledgerData };
         }
-        async _importContractByTypes(ledgerId, costId, types) {
+        async _importContractByTypes(ledgerId, costId, types, months) {
             const contracts = await this.ctx.service.contract.getAllDataByCondition({ where: { tid: this.ctx.costStage.tid, contract_type: 1, type: types } });
-            await this._importContractByIds(ledgerId, costId, contracts.map(x => { return x.id; }));
-
+            return await this._importContractByIds(ledgerId, costId, contracts.map(x => { return x.id; }), months);
         }
         async importContract(data) {
-            if (data.types) return this._importContractByTypes(data.ledger_id, data.cost_id, data.types);
-            if (data.ids) return this._importContractByIds(data.ledger_id, data.cost_id, data.ids);
+            if (data.types) return this._importContractByTypes(data.ledger_id, data.cost_id, data.types, data.months || [this.ctx.costStage.stage_date]);
+            if (data.ids) return this._importContractByIds(data.ledger_id, data.cost_id, data.ids, data.months || [this.ctx.costStage.stage_date]);
         }
 
         async deletePartData(transaction, tender_id, ledger_id) {

+ 3 - 3
app/service/cost_stage_ledger.js

@@ -151,9 +151,9 @@ module.exports = app => {
         }
         async initStageData(transaction, stage, preStage) {
             if (preStage) {
-                this.initByPre(stage, preStage, transaction);
+                await this.initByPre(stage, preStage, transaction);
             } else {
-                this.init(stage, transaction);
+                await this.init(stage, transaction);
             }
         }
 
@@ -444,7 +444,7 @@ module.exports = app => {
                 if (row.sf_tp !== undefined) nData.sf_tp = helper.round(row.sf_tp || 0, decimal.tp);
                 if (row.tax !== undefined || nData.sf_tp !== undefined || nData.yf_tp !== undefined) {
                     nData.tax = row.tax !== undefined ? helper.round(row.tax || 0, decimal.tax) : oData.tax;
-                    const divNum = helper.add(1, helper.div(nData.tax, 100));
+                    const divNum = nData.tax ? helper.add(1, helper.div(nData.tax, 100)) : 1;
                     nData.yf_excl_tax_tp = helper.div(nData.yf_tp !== undefined ? nData.yf_tp : oData.yf_tp, divNum, decimal.tp);
                     nData.sf_excl_tax_tp = helper.div(nData.sf_tp !== undefined ? nData.sf_tp : oData.sf_tp, divNum, decimal.tp);
                 }

+ 127 - 0
app/service/pos.js

@@ -413,6 +413,131 @@ module.exports = app => {
             return { pos: update }
         }
 
+        _calcExpr(data, field, expr, defaultValue, precision) {
+            if (expr) {
+                try {
+                    data[field] = this.ctx.helper.round(this.ctx.helper.calcExpr(expr), precision.value);
+                } catch (err) {
+                }
+            } else {
+                data[field] = this.ctx.helper.round(defaultValue, precision.value);
+            }
+        }
+        async pasteBlockData(tid, data) {
+            if (!(data.block instanceof Array)) throw '提交数据错误';
+
+            const result = { ledger: {}, pos: null };
+
+            const bills = await this.ctx.service.ledger.getDataById(data.lid);
+            const le = await this.ctx.service.ledgerExtra.getDataById(data.lid);
+            const calcTemplate = le.calc_template ? await this.ctx.service.calcTmpl.getTemplate(le.calc_template) : null;
+            const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, bills.unit);
+            const existPos = await this.ctx.service.pos.getAllDataByCondition({ where : { lid: bills.id }, orders: [['porder', 'DESC']]});
+            const maxOrder = existPos.length > 0 ? existPos[0].porder : 0;
+            const updateBills = existPos.length > 0
+                ? { id: bills.id, sgfh_qty: bills.sgfh_qty, sjcl_qty: bills.sjcl_qty, qtcl_qty: bills.qtcl_qty, quantity: bills.quantity, ex_qty1: bills.ex_qty1 }
+                : { id: bills.id, sgfh_qty: 0, sjcl_qty: 0, qtcl_qty: 0, quantity: 0, ex_qty1: 0 };
+
+            const insertPos = [], insertDetail = [];
+            for (const [i, b] of data.block.entries()) {
+                const nip = {};
+                insertPos.push(nip);
+                this._completeInsertPosData(tid, nip);
+                nip.lid = b.lid;
+                nip.name = b.name || '';
+                nip.drawing_code = b.drawing_code || '';
+                nip.porder = maxOrder + i + 1;
+                nip.position = b.position;
+                nip.sgfh_expr = b.sgfh_expr ? b.sgfh_expr : '';
+                nip.sjcl_expr = b.sjcl_expr ? b.sjcl_expr : '';
+                nip.qtcl_expr = b.qtcl_expr ? b.qtcl_expr : '';
+                nip.ex_memo1 = b.ex_memo1;
+                nip.ex_memo2 = b.ex_memo2;
+                nip.ex_memo3 = b.ex_memo3;
+                if (calcTemplate) {
+                    let sumQty = 0;
+                    for (const cd of b.calcDetail) {
+                        const newDetail = {
+                            id: this.uuid.v4(), tid: tid, lid: bills.id, pid: nip.id,
+                            create_user_id: this.ctx.session.sessionUser.accountId,
+                            update_user_id: this.ctx.session.sessionUser.accountId,
+                            pcd_order: cd.pcd_order,
+                        };
+                        if (data.calc_template === le.calc_template) {
+                            newDetail.str1 = cd.str1 || '';
+                            newDetail.str2 = cd.str2 || '';
+                            newDetail.str3 = cd.str3 || '';
+                            newDetail.str4 = cd.str4 || '';
+                            newDetail.num_a = cd.num_a || 0;
+                            newDetail.num_b = cd.num_b || 0;
+                            newDetail.num_c = cd.num_c || 0;
+                            newDetail.num_d = cd.num_d || 0;
+                            newDetail.num_e = cd.num_e || 0;
+                            newDetail.num_f = cd.num_f || 0;
+                            newDetail.num_g = cd.num_g || 0;
+                            newDetail.num_h = cd.num_h || 0;
+                            newDetail.num_i = cd.num_i || 0;
+                            newDetail.num_j = cd.num_j || 0;
+                            newDetail.num_k = cd.num_k || 0;
+                            newDetail.num_l = cd.num_l || 0;
+                            newDetail.num_m = cd.num_m || 0;
+                            newDetail.num_n = cd.num_n || 0;
+                            newDetail.num_o = cd.num_o || 0;
+                            newDetail.num_p = cd.num_p || 0;
+                            newDetail.num_q = cd.num_q || 0;
+                            newDetail.num_r = cd.num_r || 0;
+                            newDetail.num_s = cd.num_s || 0;
+                            newDetail.num_t = cd.num_t || 0;
+                            newDetail.num_u = cd.num_u || 0;
+                            newDetail.qty = cd.qty || 0;
+                            newDetail.expr = cd.expr || '';
+                            newDetail.spec = cd.spec || '';
+                        } else {
+                            this.ctx.service.posCalcDetail._loadDataAndCalc(newDetail, cd, {}, calcTemplate);
+                        }
+                        sumQty = this.ctx.helper.add(sumQty, newDetail.qty);
+                        insertDetail.push(newDetail);
+                    }
+                    this._calcExpr(nip, 'sgfh_qty', '', sumQty, precision);
+                } else {
+                    this._calcExpr(nip, 'sgfh_qty', b.sgfh_expr, b.sgfh_qty, precision);
+                }
+                this._calcExpr(nip, 'sjcl_qty', b.sjcl_expr, b.sjcl_qty, precision);
+                this._calcExpr(nip, 'qtcl_qty', b.qtcl_expr, b.qtcl_qty, precision);
+                nip.quantity = this.ctx.helper.add(nip.sgfh_qty, this.ctx.helper.add(nip.sjcl_qty, nip.qtcl_qty));
+                nip.ex_qty1 = this.ctx.helper.round(b.ex_qty1, precision.value);
+
+                updateBills.sgfh_qty = this.ctx.helper.add(updateBills.sgfh_qty, nip.sgfh_qty);
+                updateBills.sjcl_qty = this.ctx.helper.add(updateBills.sjcl_qty, nip.sjcl_qty);
+                updateBills.qtcl_qty = this.ctx.helper.add(updateBills.qtcl_qty, nip.qtcl_qty);
+                updateBills.ex_qty1 = this.ctx.helper.add(updateBills.ex_qty1, nip.ex_qty1);
+            }
+
+            const info = this.ctx.tender.info;
+            updateBills.quantity = this.ctx.helper.add(updateBills.sgfh_qty, this.ctx.helper.add(updateBills.sjcl_qty, updateBills.qtcl_qty));
+            updateBills.sgfh_tp = this.ctx.helper.mul(updateBills.sgfh_qty, bills.unit_price, info.decimal.tp);
+            updateBills.sjcl_tp = this.ctx.helper.mul(updateBills.sjcl_qty, bills.unit_price, info.decimal.tp);
+            updateBills.qtcl_tp = this.ctx.helper.mul(updateBills.qtcl_qty, bills.unit_price, info.decimal.tp);
+            updateBills.total_price = this.ctx.helper.mul(updateBills.quantity, bills.unit_price, info.decimal.tp);
+            updateBills.ex_tp1 = this.ctx.helper.mul(updateBills.ex_qty1, bills.unit_price, info.decimal.tp);
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.insert(this.tableName, insertPos);
+                if (insertDetail.length > 0) await transaction.insert(this.ctx.service.posCalcDetail.tableName, insertDetail);
+                await transaction.update(this.ctx.service.ledger.tableName, updateBills);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            result.pos = insertPos;
+            updateBills.ledger_id = bills.ledger_id;
+            result.ledger.update = [updateBills];
+            result.posCalcDetail = { add: insertDetail };
+            return result;
+        }
+
         /**
          * 保存部位明细数据
          * @param data
@@ -433,6 +558,8 @@ module.exports = app => {
                     return await this._deletePosData(tid, data.updateData);
                 case 'insert':
                     return await this._insertPosData(tid, data.updateData);
+                case 'paste-block':
+                    return await this.pasteBlockData(tid, data.updateData);
             }
         }
 

+ 1 - 0
app/service/pos_calc_detail.js

@@ -76,6 +76,7 @@ module.exports = app => {
                 const sv = template.specValue.find(x => { return x.spec === data.spec; });
                 data[template.spec_rela] = sv ? sv.value : 0;
             }
+            if (updateData[template.spec_rela] !== undefined) delete updateData[template.spec_rela];
             this.ctx.service.calcTmpl.calcByTemplate(data, updateData, orgData, template.calc_expr, { qty: 'expr' });
             return calc || (data.qty !== undefined && orgData.qty !== data.qty);
         }

+ 2 - 2
app/service/safe_stage_bills.js

@@ -134,9 +134,9 @@ module.exports = app => {
         }
         async initStageBills(transaction, stage, preStage) {
             if (preStage) {
-                this.initByPre(stage, preStage, transaction);
+                await this.initByPre(stage, preStage, transaction);
             } else {
-                this.init(stage, transaction);
+                await this.init(stage, transaction);
             }
         }
 

+ 84 - 0
app/view/cost/analysis.ejs

@@ -0,0 +1,84 @@
+<% include ./stage_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./stage_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+                <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId) { %>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" name="base-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="base-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="base-opr" type="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
+                </div>
+                <% } %>
+            </div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap row pr-46">
+        <div class="c-header p-0 col-12">
+        </div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body" id="left-view" style="width: 100%">
+                <div class="sjs-height-1" style="overflow: hidden" id="bills-spread">
+                </div>
+                <div class="bcontent-wrap">
+                    <div class="resize-y" id="detail-spr" r-Type="height" div1=".sjs-height-1" div2=".bcontent-wrap" r-parent="div2" title="调整大小"><!--调整上下高度条--></div>
+                    <div class="bc-bar mb-1">
+                        <ul class="nav nav-tabs">
+                            <li class="nav-item">
+                                <a class="nav-link active" href="javascript:void(0)">明细数据</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="sp-wrap" id="detail-spread">
+                    </div>
+                </div>
+            </div>
+            <div class="c-body" id="right-view" style="display: none; width: 33%;">
+                <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
+                <div class="tab-content">
+                    <div id="search" class="tab-pane tab-select-show">
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!--右侧菜单-->
+        <div class="side-menu">
+            <ul class="nav flex-column right-nav" id="side-menu">
+                <li class="nav-item">
+                    <a class="nav-link" content="#search" href="javascript: void(0);">查找定位</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
+<script>
+    const readOnly = <%- ctx.costStage.readOnly %>;
+    const tenderId = parseInt('<%- ctx.tender.id %>');
+    const stageId = parseInt('<%- ctx.costStage.id %>');
+    const stageDate = '<%- ctx.costStage.stage_date %>';
+    const bllsSpreadSetting = JSON.parse(unescape('<%- escape(JSON.stringify( ctx.costStage.calcTemplate.spread_cache )) %>'));
+    const dealSpreadSetting = JSON.parse(unescape('<%- escape(JSON.stringify( ctx.costStage.calcTemplate.sub_spread_cache.deal )) %>'));
+    const commonSpreadSetting = JSON.parse(unescape('<%- escape(JSON.stringify( ctx.costStage.calcTemplate.sub_spread_cache.common )) %>'));
+</script>

+ 11 - 14
app/view/cost/analysis_list.ejs

@@ -1,12 +1,12 @@
-<% include ./list_sub_menu.ejs %>
+<% include ./list_menu.ejs %>
 <div class="panel-content">
     <div class="panel-title">
         <div class="title-main d-flex">
-            <% include ./list_sub_mini_menu.ejs %>
+            <% include ./list_mini_menu.ejs %>
             <h2>
                 收支列表
             </h2>
-            <% if (ctx.permission.cost.analysis_add && (stagelist.length === 0 || stagelist[0].audit_status === auditConst.status.checked)) { %>
+            <% if (ctx.permission.cost.analysis_add && (stages.length === 0 || stages[0].audit_status === auditConst.status.checked) && validRelaStages.length > 0) { %>
             <div class="ml-auto">
                 <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">新建收支</a>
             </div>
@@ -26,25 +26,22 @@
                         <th>项目支出</th>
                         <th>利润</th>
                         <th>利润率</th>
-                        <th width="120px">审批进度</th>
+                        <th width="180px">审批进度</th>
                         <th width="100px">操作</th>
                     </tr>
                     </thead>
                     <tbody>
-                    <% for (const s of stagelist) { %>
+                    <% for (const s of stages) { %>
                     <tr>
                         <td>
                             <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>/stage" target="_blank">第 <%- s.stage_order %> 期</a>
                         </td>
                         <td class="text-center"><%- s.stage_date %></td>
-                        <td class="text-right"><%- s.pay_tp %></td>
-                        <td class="text-right"><%- s.cut_tp %></td>
-                        <td class="text-right"><%- s.yf_tp %></td>
-                        <td class="text-right"><%- s.sf_tp %></td>
-                        <td class="text-right"><%- s.end_pay_tp %></td>
-                        <td class="text-right"><%- s.end_cut_tp %></td>
-                        <td class="text-right"><%- s.end_yf_tp %></td>
-                        <td class="text-right"><%- s.end_sf_tp %></td>
+                        <td class="text-center"><%- s.user_name %></td>
+                        <td class="text-right"><%- s.stage_tp.in_tp %></td>
+                        <td class="text-right"><%- s.stage_tp.out_tp %></td>
+                        <td class="text-right"><%- s.stage_tp.profit %></td>
+                        <td class="text-right"><%- s.stage_tp.profit_percent %></td>
                         <td class="<%- auditConst.info[s.audit_status].class %>">
                             <% if (s.audit_status === auditConst.status.checked && s.final_auditor_str) { %>
                             <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.final_auditor_str %></a>
@@ -79,7 +76,7 @@
     </div>
 </div>
 <script>
-    const stageList = JSON.parse('<%- JSON.stringify(stageList) %>');
+    const stages = JSON.parse('<%- JSON.stringify(stages) %>');
     const auditType = JSON.parse('<%- JSON.stringify(auditType) %>');
     const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
 </script>

+ 9 - 16
app/view/cost/analysis_list_modal.ejs

@@ -1,34 +1,27 @@
 <div class="modal" id="add-qi" data-backdrop="static" aria-modal="true" role="dialog">
     <div class="modal-dialog" role="document">
-        <form class="modal-content" action="pay/add" method="POST" onsubmit="return checkAddValid();">
+        <form class="modal-content" action="addStage" method="POST" onsubmit="return checkAddValid();">
             <div class="modal-header">
-                <h5 class="modal-title">添加新一期</h5>
+                <h5 class="modal-title">新建报审</h5>
             </div>
             <div class="modal-body">
                 <div class="form-group form-group-sm">
-                    <label>支付期</label>
-                    <input class="form-control form-control-sm" value="第 <%- (phasePays.length + 1) %> 期" type="text" readonly="">
-                    <input type="hidden" value="<%- (phasePays.length + 1) %>" name="phase_order">
+                    <label>期</label>
+                    <input class="form-control form-control-sm" value="第 <%- (stages.length + 1) %> 期" type="text" readonly="">
+                    <input type="hidden" value="<%- (stages.length + 1) %>" name="stage_order">
                 </div>
                 <div class="form-group form-group-sm">
-                    <label>支付年月</label>
-                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" name="date" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
-                </div>
-                <div class="form-group form-group-sm">
-                    <label>计量期</label>
+                    <label>关联成本</label>
                     <select class="form-control form-control-sm" name="stage">
-                        <% for (const s of validStages) { %>
-                            <option value="<%- s.order %>">第 <%- s.order %> 期</option>
+                        <% for (const s of validRelaStages) { %>
+                        <option value="<%- s.stage_order %>">第 <%- s.stage_order %> 期</option>
                         <% } %>
                     </select>
                 </div>
-                <div class="form-group form-group-sm">
-                    <label>支付期备注</label>
-                    <textarea class="form-control form-control-sm" rows="3" name="memo"></textarea>
-                </div>
             </div>
             <div class="modal-footer">
                 <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <input type="hidden" name="stage_type" value="<%- stage_type %>" />
                 <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
                 <button type="submit" class="btn btn-sm btn-primary">确定</button>
             </div>

+ 2 - 1
app/view/cost/analysis_menu_list.ejs

@@ -1,2 +1,3 @@
 <nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
-<nav-menu title="成本分析" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- ctx.costStage.stage_order %>/stage" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="成本分析" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- ctx.costStage.stage_order %>/stage" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>
+<% include ./audit_btn.ejs %>

+ 30 - 0
app/view/cost/analysis_modal.ejs

@@ -0,0 +1,30 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% include ./audit_modal.ejs %>
+<div class="modal" id="import-pay" data-backdrop="static" aria-modal="true" role="dialog">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入计量数据</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group form-group-sm">
+                    <label>选择期数</label>
+                    <select class="form-control form-control-sm" id="pay-order">
+                        <% for (const ps of payStages) { %>
+                        <option value="<%- ps.order %>">第<%- ps.order %>期 (<%- ps.date %>)</option>
+                        <% } %>
+                    </select>
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>税率</label>
+                    <input class="form-control form-control-sm" id="pay-tax" placeholder="请输入税率">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="import-pay-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 4 - 4
app/view/cost/book_list.ejs

@@ -6,7 +6,7 @@
             <h2>
                 审批列表
             </h2>
-            <% if (ctx.permission.cost.book_add && (stages.length === 0 || stages[0].audit_status === auditConst.status.checked)) { %>
+            <% if (ctx.permission.cost.book_add && (stages.length === 0 || stages[0].audit_status === auditConst.status.checked) && validLedgerStages.length > 0) { %>
             <div class="ml-auto">
                 <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">新建报审</a>
             </div>
@@ -24,7 +24,7 @@
                         <th width="70px">创建人</th>
                         <th>本期入账金额(不含税)</th>
                         <th>截止本期入账金额(不含税)</th>
-                        <th width="120px">审批进度</th>
+                        <th width="180px">审批进度</th>
                         <th width="100px">操作</th>
                     </tr>
                     </thead>
@@ -36,8 +36,8 @@
                         </td>
                         <td class="text-center"><%- s.stage_date %></td>
                         <td class="text-center"><%- s.user_name %></td>
-                        <td class="text-right"><%- s.tp %></td>
-                        <td class="text-right"><%- s.end_tp %></td>
+                        <td class="text-right"><%- s.stage_tp.in_excl_tax_tp %></td>
+                        <td class="text-right"><%- s.stage_end_tp.in_excl_tax_tp %></td>
                         <td class="<%- auditConst.info[s.audit_status].class %>">
                             <% if (s.audit_status === auditConst.status.checked && s.final_auditor_str) { %>
                             <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.final_auditor_str %></a>

+ 2 - 1
app/view/cost/book_menu_list.ejs

@@ -1,2 +1,3 @@
 <nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
-<nav-menu title="财务账面" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- ctx.costStage.stage_order %>/stage" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="财务账面" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- ctx.costStage.stage_order %>/stage" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>
+<% include ./audit_btn.ejs %>

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

@@ -87,4 +87,5 @@
     const readOnly = <%- ctx.costStage.readOnly %>;
     const tenderId = parseInt('<%- ctx.tender.id %>');
     const stageId = parseInt('<%- ctx.costStage.id %>');
+    const stageDate = '<%- ctx.costStage.stage_date %>';
 </script>

+ 8 - 0
app/view/cost/ledger_modal.ejs

@@ -17,6 +17,10 @@
                         <% } %>
                     </select>
                 </div>
+                <div class="form-group form-group-sm">
+                    <label>支付年月</label>
+                    <input class="datepicker-here form-control form-control-sm" readonly autocomplete="off" id="type-pay-date" placeholder="点击选择年月" data-view="months" data-range="true"  data-multiple-dates-separator=" ~ " data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
+                </div>
             </div>
             <div class="modal-footer">
                 <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
@@ -34,6 +38,10 @@
             </div>
             <div class="modal-body">
                 <div class="modal-height-400" id="contract-spread"></div>
+                <div class="form-group form-group-sm">
+                    <label>支付年月</label>
+                    <input class="datepicker-here form-control form-control-sm" readonly autocomplete="off" id="select-pay-date" placeholder="点击选择年月" data-view="months" data-range="true"  data-multiple-dates-separator=" ~ " data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
+                </div>
             </div>
             <div class="modal-footer">
                 <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>

+ 35 - 15
app/view/template/pos_calc.ejs

@@ -3,7 +3,17 @@
     <div class="panel-title">
         <div class="title-main row">
             <div class="col-3">
-                <button class="btn btn-sm btn-light text-primary" id="addTemplate"><i class="fa fa-plus" aria-hidden="true"></i> 新增计算模板</button>
+                <div class="d-inline-block">模板分类</div>
+                <% if (ctx.session.sessionUser.is_admin) { %>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" name="base-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="base-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="base-opr" type="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
+                </div>
+                <% } %>
             </div>
             <div class="col-9">
                 <div class="d-flex">
@@ -13,6 +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>
                     </div>
                     <div id="detail-user-info" class="ml-auto"></div>
                 </div>
@@ -23,20 +35,26 @@
         <div class="c-body">
             <div class="row">
                 <div class="col-3">
-                    <div class="sjs-height-0">
-                        <dl class="list-group mt-2 ml-2" id="template-list">
-                            <% for (const template of templateList) { %>
-                            <dd class="list-group-item" templateId="<%- template.id %>">
-                                <div class="d-flex justify-content-between align-items-center table-file" templateId="<%- template.id %>">
-                                    <div><%- template.name %><%if (template.used_count > 0) { %><i class="ml-1 fa fa-lock text-danger"></i><% } %></div>
-                                    <div class="btn-group-table" style="display: none;">
-                                        <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameTemplate"><i class="fa fa-pencil fa-fw"></i></a>
-                                        <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delTemplate"><i class="fa fa-trash-o fa-fw text-danger"></i></a>
-                                    </div>
-                                </div>
-                            </dd>
-                            <% } %>
-                        </dl>
+                    <div id="folder-spread" class="sjs-height-1"></div>
+                    <div class="bcontent-wrap" id="main-bottom">
+                        <div class="bc-bar mb-1 d-flex">
+                            <div class="d-inline-block">
+                                <ul class="nav nav-tabs">
+                                    <li class="nav-item">
+                                        <a class="nav-link active" href="javascript:void(0)">模板列表</a>
+                                    </li>
+                                    <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>
+                                        <button class="btn btn-sm btn-light text-primary" id="importTemplateNoFolder"> 导入未分类模板</button>
+                                    </li>
+                                </ul>
+                            </div>
+                        </div>
+                        <div class="sp-wrap" style="display: flex; flex-wrap: wrap;">
+                            <div class="c-body" id="template-spread" style="width: 100%">
+                            </div>
+                        </div>
                     </div>
                 </div>
                 <div class="col-9" style="height: 100%">
@@ -48,6 +66,8 @@
     </div>
 </div>
 <script>
+    const readOnly = <%- (!ctx.session.sessionUser.is_admin) %>;
+    const folderList = JSON.parse('<%- JSON.stringify(folderList) %>');
     const templateList = JSON.parse('<%- JSON.stringify(templateList) %>');
     const validColInfo = JSON.parse('<%- JSON.stringify(validColInfo) %>');
     const specList = JSON.parse('<%- JSON.stringify(specList) %>');

+ 2 - 1
app/view/template/pos_calc_modal.ejs

@@ -1,2 +1,3 @@
 <% include ./preview_modal.ejs %>
-<% include ../shares/import_file_modal.ejs %>
+<% include ../shares/import_file_modal.ejs %>
+<% include ../shares/delete_hint_modal.ejs %>

+ 36 - 1
config/web.js

@@ -2505,7 +2505,9 @@ const JsFiles = {
                     '/public/js/zh_calc.js',
                     '/public/js/spreadjs_rela/spreadjs_zh.js',
                     '/public/js/shares/ct_preview.js',
+                    '/public/js/shares/sjs_setting.js',
                     '/public/js/pos_calc_tmpl.js',
+                    '/public/js/path_tree.js',
                 ],
                 mergeFile: 'pos_calc_tmpl',
             },
@@ -2563,6 +2565,9 @@ const JsFiles = {
                     '/public/js/axios/axios.min.js', '/public/js/js-xlsx/jszip.min.js',
                     '/public/js/file-saver/FileSaver.js',
                     '/public/js/component/menu.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/moment/moment.min.js',
                 ],
                 mergeFiles: [
                     '/public/js/sub_menu.js',
@@ -2606,7 +2611,37 @@ const JsFiles = {
                     '/public/js/cost_stage_book.js',
                 ],
                 mergeFile: 'cost_stage_book',
-            }
+            },
+            cost_stage_analysis: {
+                files: [
+                    '/public/js/js-xlsx/xlsx.full.min.js',
+                    '/public/js/js-xlsx/xlsx.utils.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/axios/axios.min.js', '/public/js/js-xlsx/jszip.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/shares/ali_oss.js',
+                    '/public/js/shares/new_tag.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tools_att.js',
+                    '/public/js/shares/common_audit.js',
+                    '/public/js/cost_stage_analysis.js',
+                ],
+                mergeFile: 'cost_stage_analysis',
+            },
         },
     },
 };

+ 142 - 4
sql/update.sql

@@ -65,6 +65,7 @@ CREATE TABLE `zh_cost_stage`  (
   `final_auditor_str` varchar(255) NOT NULL DEFAULT '' COMMENT '终审缓存信息',
   `stage_tp` json NOT NULL COMMENT '金额',
   `stage_pre_tp` json NOT NULL COMMENT '截止上期金额',
+  `calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '计算模板',
   PRIMARY KEY (`id`),
   INDEX `idx_tid_type`(`tid`, `stage_type`) USING BTREE
 );
@@ -162,8 +163,10 @@ CREATE TABLE `zh_cost_stage_detail`  (
   `id` varchar(36) NOT NULL COMMENT 'uuid',
   `tender_id` int(11) NOT NULL COMMENT '标段id',
   `stage_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '期id',
+  `stage_order` int(11) NOT NULL COMMENT '期序号',
   `ledger_id` varchar(36) NOT NULL COMMENT '所属台账id(zh_stage_ledger.id)',
   `cost_id` varchar(36) CHARACTER SET utf16le NOT NULL COMMENT '所属台账cost_id(不同期一致zh_stage_ledger.cost_id)',
+  `source_cid` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '导入的合同id',
   `d_order` int(11) NOT NULL COMMENT '排序',
   `code` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '编号',
   `name` varchar(255) CHARACTER SET utf16 NOT NULL DEFAULT '' COMMENT '名称',
@@ -218,10 +221,10 @@ CREATE TABLE `zh_cost_stage_tag`  (
 
 CREATE TABLE `zh_cost_stage_book` (
   `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(zh_cost_stage_ledger.id)',
-  `ledger_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '关联台账id(zh_stage_ledger.id)',
-  `cost_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '关联台账cost_id(不同期一致zh_stage_ledger.cost_id)',
   `tender_id` int(11) unsigned NOT NULL COMMENT '标段id',
   `stage_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期id',
+  `ledger_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '关联台账id(zh_stage_ledger.id)',
+  `cost_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '关联台账cost_id(不同期一致zh_stage_ledger.cost_id)',
   `pre_in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-入账金额',
   `pre_in_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '截止上期-入账金额不含税',
   `in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '入账金额',
@@ -240,9 +243,10 @@ CREATE TABLE `zh_cost_stage_book` (
 
 CREATE TABLE `zh_cost_stage_book_detail` (
   `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(zh_cost_stage_ledger.id)',
-  `detail_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '关联明细id(zh_stage_detail.id)',
   `tender_id` int(11) unsigned NOT NULL COMMENT '标段id',
-  `stage_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期id',
+  `stage_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL COMMENT '期id',
+  `ledger_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '关联台账id(zh_stage_detail.ledger_id)',
+  `detail_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '关联明细id(zh_stage_detail.id)',
   `in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '入账金额',
   `in_excl_tax_tp` decimal(24, 8) NOT NULL COMMENT '入账金额不含税',
   `postil` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
@@ -257,6 +261,140 @@ CREATE TABLE `zh_cost_stage_book_detail` (
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
 
+CREATE TABLE `zh_cost_stage_analysis`  (
+  `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid',
+  `cost_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(不同期保持统一)',
+  `tender_id` int(11) UNSIGNED NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期id',
+  `tree_id` int(11) NOT NULL COMMENT '节点id',
+  `tree_pid` int(11) NOT NULL COMMENT '父节点id',
+  `tree_level` tinyint(4) NOT NULL COMMENT '层级',
+  `tree_order` mediumint(4) NOT NULL COMMENT '同级排序',
+  `tree_full_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '层级定位辅助字段parent.full_path-tree_id',
+  `tree_is_leaf` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否叶子节点,界面显示辅助字段',
+  `node_type` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '节点类型',
+  `code` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '编号',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `unit` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '单位',
+  `tax` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '税率(%)',
+  `yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '应付金额-不含税',
+  `in_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '入账金额-不含税',
+  `sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付金额-不含税',
+  `sf_percent` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付比例',
+  `postil` varchar(1000) NOT NULL DEFAULT '' COMMENT '本期批注',
+  `str1` varchar(255) NOT NULL DEFAULT '' COMMENT '文本',
+  `str2` varchar(255) NOT NULL DEFAULT '' COMMENT '文本',
+  `str3` varchar(255) NOT NULL DEFAULT '' COMMENT '文本',
+  `str4` varchar(255) NOT NULL DEFAULT '' COMMENT '文本',
+  `num_a` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_b` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_c` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_d` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_e` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_f` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_g` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_h` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_i` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_j` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_k` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_l` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_m` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_n` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_o` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `num_p` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '数值',
+  `calc_read` json DEFAULT NULL COMMENT '本期计算-只读',
+  `calc_his` json DEFAULT NULL COMMENT '本期历史',
+  `add_user_id` int(11) NOT NULL COMMENT '新增人id',
+  `add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '新增时间',
+  `update_user_id` int(11) NOT NULL COMMENT '最后更新人id',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `calc_type` int(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '计算类型(收入1,支出2,其他0)',
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `zh_cost_stage_analysis_detail`  (
+  `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `tender_id` int(11) NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '期id',
+  `ledger_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '所属台账id(zh_stage_ledger.id)',
+  `cost_id` varchar(36) CHARACTER SET utf16le COLLATE utf16le_general_ci NOT NULL COMMENT '所属台账cost_id(不同期一致zh_stage_ledger.cost_id)',
+  `source_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '导入的合同id/明细id',
+  `d_order` int(11) NOT NULL COMMENT '排序',
+  `code` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '编号',
+  `name` varchar(255) CHARACTER SET utf16 COLLATE utf16_general_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `party_b` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '乙方',
+  `tax` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '税率(%)',
+  `yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '应付金额-不含税',
+  `in_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '入账金额-不含税',
+  `sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付金额-不含税',
+  `sf_percent` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付比例',
+  `postil` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
+  `str1` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '文本',
+  `str2` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '文本',
+  `str3` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '文本',
+  `str4` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '文本',
+  `num_a` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_b` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_c` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_d` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_e` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_f` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_g` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_h` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_i` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_j` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_k` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_l` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_m` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_n` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_o` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `num_p` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值',
+  `calc_read` json NULL COMMENT '本期计算-只读',
+  `calc_his` json NULL COMMENT '本期计算-历史',
+  `add_user_id` int(11) NOT NULL COMMENT '新增人id',
+  `add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '新增时间',
+  `update_user_id` int(11) NOT NULL COMMENT '最后更新人id',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `is_deal` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否为合同',
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `zh_calc_tmpl_folder`  (
+  `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `pid` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '项目id(zh_project.id)',
+  `type` varchar(20) NOT NULL COMMENT '模板类型(posCalc/...)',
+  `master_id` varchar(36) NOT NULL COMMENT 'pid-type',
+  `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `tree_id` int(11) NOT NULL COMMENT '节点id',
+  `tree_pid` int(11) NOT NULL COMMENT '父节点id',
+  `tree_level` tinyint(4) NOT NULL COMMENT '层级',
+  `tree_order` mediumint(4) NOT NULL COMMENT '同级排序',
+  `tree_full_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '层级定位辅助字段parent.full_path-tree_id',
+  `tree_is_leaf` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否叶子节点,界面显示辅助字段',
+  `add_user_id` int(11) NOT NULL COMMENT '新增人id',
+  `add_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '新增时间',
+  `update_user_id` int(11) NOT NULL COMMENT '最后更新人id',
+  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '最后更新时间',
+  PRIMARY KEY (`id`)
+);
+
+ALTER TABLE `zh_pos_calc_detail`
+ADD COLUMN `num_j` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值1' AFTER `num_i`,
+ADD COLUMN `num_k` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值2' AFTER `num_j`,
+ADD COLUMN `num_l` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值3' AFTER `num_k`,
+ADD COLUMN `num_m` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值4' AFTER `num_l`,
+ADD COLUMN `num_n` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值5' AFTER `num_m`,
+ADD COLUMN `num_o` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值6' AFTER `num_n`,
+ADD COLUMN `num_p` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值7' AFTER `num_o`,
+ADD COLUMN `num_q` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值8' AFTER `num_p`,
+ADD COLUMN `num_r` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值9' AFTER `num_q`,
+ADD COLUMN `num_s` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值5' AFTER `num_r`,
+ADD COLUMN `num_t` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值6' AFTER `num_s`,
+ADD COLUMN `num_u` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '数值7' AFTER `num_t`;
+
+ALTER TABLE `zh_calc_tmpl`
+ADD COLUMN `folder_id` varchar(36) NOT NULL DEFAULT '' COMMENT '分类id(zh_calc_tmpl_folder.id)' AFTER `tid`;
+
 ------------------------------------
 -- 表数据
 ------------------------------------