Quellcode durchsuchen

成本报审、财务账面,成本分析,审核比较相关

MaiXinRong vor 1 Monat
Ursprung
Commit
8495ca6348

+ 34 - 2
app/controller/cost_controller.js

@@ -327,7 +327,6 @@ module.exports = app => {
                                 : await ctx.service.costStageDetail.getEditData(ctx.costStage);
                             break;
                         case 'detailCompare':
-                            // todo
                             responseData.data.detailCompare = await ctx.service.costStageDetail.getCompareData(ctx.costStage);
                             break;
                         case 'auditFlow':
@@ -450,7 +449,6 @@ module.exports = app => {
                                 : await ctx.service.costStageAnalysisDetail.getEditData(ctx.costStage);
                             break;
                         case 'detailCompare':
-                            // todo
                             responseData.data.detailCompare = await ctx.service.costStageAnalysisDetail.getCompareData(ctx.costStage);
                             break;
                         case 'auditFlow':
@@ -861,6 +859,40 @@ module.exports = app => {
             }
         }
 
+        async compare(ctx) {
+            const stageTypeInfo = ctx.service.costStage.stageType[ctx.costStage.stage_type];
+            try {
+                await this._getStageAuditViewData(ctx);
+                // 流程审批人相关数据
+                const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                const accountGroup = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                }).filter(x => { return x.groupList.length > 0; });
+                // 是否已验证手机短信
+                const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const calcFields = ctx.service[stageTypeInfo.dataService].calcFields;
+
+                const renderData = {
+                    auditConst: audit.common,
+                    auditType: audit.auditType,
+                    accountList,
+                    accountGroup,
+                    shenpiConst,
+                    authMobile: pa.auth_mobile,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_compare),
+                    shenpi_status: ctx.tender.info.shenpi[stageTypeInfo.shenpi_status] || 1,
+                    calcFields,
+                };
+                await this.layout(`cost/compare.ejs`, renderData, `cost/compare_modal.ejs`);
+            } catch(err) {
+                ctx.log(err);
+                ctx.postError(err, '读取成本报审错误');
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stageTypeInfo.key}`);
+            }
+        }
+
         async gather(ctx) {
             try {
                 const ledgerStages = await this.getTypeStages(ctx, 'ledger', 'ASC');

+ 340 - 0
app/public/js/cost_compare.js

@@ -0,0 +1,340 @@
+$(document).ready(() => {
+    autoFlashHeight();
+    const compareUtils = {
+        refreshSpreadSetting(roles, spreadSetting) {
+            spreadSetting.cols = [];
+            spreadSetting.fieldSufs = [];
+            for (const col of spreadSetting.baseCols) {
+                spreadSetting.cols.push(col);
+            }
+            for (const role of roles) {
+                spreadSetting.fieldSufs.push(role.active_order);
+                for (const ec of spreadSetting.extraCols) {
+                    const col = JSON.parse(JSON.stringify(ec));
+                    col.title = _.replace(col.title, '%s', role.name);
+                    col.field = col.field + '_' + role.active_order;
+                    spreadSetting.cols.push(col);
+                }
+            }
+        },
+        analysisCompareData(datas, roles, extraCols){
+            const findHis = function (role, history) {
+                let his = null;
+                for (const h of history) {
+                    if (h.audit_times < role.audit_times || (h.audit_times === role.audit_times && h.active_order <= role.active_order)) {
+                        his = h;
+                    } else {
+                        break;
+                    }
+                }
+                return his;
+            };
+            for (const d of datas) {
+                if (d.tree_id && !d.tree_is_leaf) continue;
+                if (!d.calc_his) d.calc_his = [];
+                d.calc_his.sort((x, y) => { return x.audit_times === y.audit_times ? x.active_order - y.active_order : x.audit_times - y.audit_times; });
+                for (const r of roles) {
+                    if (r.latest) {
+                        for (const col of extraCols){
+                            d[col.field + '_' + r.active_order] = d[col.field];
+                        }
+                    } else {
+                        const rHis = findHis(r, d.calc_his);
+                        if (rHis) {
+                            for (const col of extraCols){
+                                d[col.field + '_' + r.active_order] = rHis[col.field];
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        filterSameRoles(roles) {
+            const result = [];
+            for (const af of roles) {
+                const naf = roles.find(x => { return x.audit_order === af.audit_order && x.active_order > af.active_order; });
+                if (!naf) result.push(af);
+            }
+            return result;
+        }
+    };
+    const converseSpreadSetting = function(setting) {
+        setting.baseCols = [];
+        setting.extraCols = [];
+        let firstExtraCol;
+        for (const col of setting.cols) {
+            if (calcFields.indexOf(col.field) < 0) {
+                if (firstExtraCol) continue;
+                col.rowSpan = (setting.headRows + 1) + '';
+                setting.baseCols.push(col);
+            } else {
+                if (!firstExtraCol) {
+                    firstExtraCol = col;
+                    col.title = '%s|' + col.title;
+                    col.rowSpan = '1|' + col.rowSpan;
+                } else {
+                    col.title = '|' + col.title;
+                    col.colSpan = '|' + col.colSpan;
+                    col.rowSpan = '|' + col.rowSpan;
+                }
+                setting.extraCols.push(col);
+            }
+        }
+        firstExtraCol.colSpan = setting.extraCols.length + '|' + firstExtraCol.colSpan;
+        setting.headRows = setting.headRows + 1;
+        setting.headRowHeight.unshift(setting.headRowHeight[0]);
+        setting.cols = [...setting.baseCols];
+        return setting;
+    };
+    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_tp', 'sf_tp', 'pay_tp', 'cut_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp', 'end_yf_tp', 'end_sf_tp', 'end_pay_tp', 'end_cut_tp', 'end_yf_excl_tax_tp', 'end_sf_excl_tax_tp'],
+                keys: ['id', 'stage_id', 'tree_id'],
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.spreadSetting = converseSpreadSetting(bllsSpreadSetting);
+            sjsSettingObj.setFxTreeStyle(this.spreadSetting, sjsSettingObj.FxTreeStyle.phasePay);
+            this.ckBillsSpread = window.location.pathname + '-billsSelect';
+
+            this.initSpread();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            this.spread.bind(spreadNS.Events.topRowChanged, this.topRowChanged);
+        }
+        loadRelaData() {
+            SpreadJsObj.saveTopAndSelect(this.sheet, this.ckBillsSpread);
+            const select = SpreadJsObj.getSelectObject(this.sheet);
+            detailObj.loadDetailData(select);
+        }
+        initSpreadSetting(roles = []) {
+            compareUtils.refreshSpreadSetting(roles, this.spreadSetting);
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+        }
+        loadData(datas, roles) {
+            this.initSpreadSetting(roles);
+            compareUtils.analysisCompareData(datas, roles, this.spreadSetting.extraCols);
+            this.tree.loadDatas(datas);
+            this.tree.setting.calcFields = [];
+            for (const r of roles) {
+                for (const col of this.spreadSetting.extraCols) {
+                    this.tree.setting.calcFields.push(col.field + '_' + r.active_order);
+                }
+            }
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.tree);
+            SpreadJsObj.loadTopAndSelect(this.sheet, this.ckBillsSpread);
+        }
+        // 事件
+        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);
+        }
+    }
+    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 = converseSpreadSetting(dealSpreadSetting);
+            this.commonSpreadSetting = converseSpreadSetting(commonSpreadSetting);
+
+            this.initSpread();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.commonSpreadSetting);
+        }
+        refreshSpreadSetting(roles, spreadSetting) {
+            spreadSetting.cols = [];
+            spreadSetting.fieldSufs = [];
+            for (const col of spreadSetting.baseCols) {
+                spreadSetting.cols.push(col);
+            }
+            for (const role of roles) {
+                spreadSetting.fieldSufs.push(role.active_order);
+                for (const ec of spreadSetting.extraCols) {
+                    const col = JSON.parse(JSON.stringify(ec));
+                    col.title = _.replace(col.title, '%s', role.name);
+                    col.field = col.field + '_' + role.active_order;
+                    spreadSetting.cols.push(col);
+                }
+            }
+        }
+        initSpreadSetting(roles = []) {
+            this.refreshSpreadSetting(roles, this.commonSpreadSetting);
+            this.refreshSpreadSetting(roles, this.dealSpreadSetting);
+            SpreadJsObj.initSheet(this.sheet, this.commonSpreadSetting);
+        }
+        loadData(datas, roles) {
+            this.initSpreadSetting(roles);
+            compareUtils.analysisCompareData(datas, roles, this.dealSpreadSetting.extraCols);
+            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 spreadSetting = bills && bills.is_deal ? this.dealSpreadSetting : this.commonSpreadSetting;
+            SpreadJsObj.initSheet(this.sheet, spreadSetting);
+            this.reloadDetailData();
+            SpreadJsObj.locateRow(this.sheet, 0, false);
+        }
+    }
+    const detailObj = new DetailObj();
+
+    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; });
+                if (pIndex >= 0) SpreadJsObj.locateRow(detailObj.sheet, pIndex);
+            }
+        },
+    });
+    // 加载数据
+    postData('load', { filter: 'billsCompare;detailCompare;auditFlow' }, function(result) {
+        const filter = true;
+        const roles = filter ? compareUtils.filterSameRoles(result.auditFlow) : result.auditFlow;
+        billsObj.loadData(result.billsCompare, roles);
+        detailObj.loadData(result.detailCompare, roles);
+    });
+    // 展开收起标准清单
+    $('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();
+            searchObj.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);
+});

+ 1 - 0
app/router.js

@@ -559,6 +559,7 @@ module.exports = app => {
     app.post('/sp/:id/cost/tender/:tid/:stype/auditors', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.loadAuditors');
     app.post('/sp/:id/cost/tender/:tid/colSet', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.colSet');
     app.get('/sp/:id/cost/tender/:tid/:stype/:sorder/stage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stage');
+    app.get('/sp/:id/cost/tender/:tid/:stype/:sorder/compare', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.compare');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/load', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageLoad');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/update', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageUpdate');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/file/upload', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.uploadStageFile');

+ 2 - 1
app/service/cost_stage_analysis.js

@@ -23,7 +23,7 @@ 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];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.sumFields, ...costFields.taxFields, ...costFields.hisFields];
 const calcType = { other: 0, in: 1, out: 2 };
 
 const auditConst = require('../const/audit').costStage;
@@ -52,6 +52,7 @@ module.exports = app => {
             });
             // this.depart = ctx.app.config.table_depart.light;
             this.tableName = 'cost_stage_analysis';
+            this.calcFields = costFields.sumFields;
         }
 
         // 继承方法

+ 2 - 2
app/service/cost_stage_analysis_detail.js

@@ -20,9 +20,9 @@ const costFields = {
 };
 costFields.sumFields = [...costFields.calcFields, ...costFields.selfCalcFields];
 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.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.sumFields, ...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];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.sumFields, ...costFields.taxFields, ...costFields.hisFields];
 
 module.exports = app => {
     class CostStageDetail extends app.BaseService {

+ 1 - 1
app/service/cost_stage_audit.js

@@ -213,7 +213,7 @@ module.exports = app => {
                     result.push(x);
                 }
             });
-            return result;
+            return latestGroup;
         }
         // ***** 查询审批人相关
 

+ 1 - 0
app/service/cost_stage_book.js

@@ -26,6 +26,7 @@ module.exports = app => {
         constructor(ctx) {
             super(ctx);
             this.tableName = 'cost_stage_book';
+            this.calcFields = costFields.calcFields;
         }
 
         async getEditData(stage) {

+ 1 - 1
app/service/cost_stage_detail.js

@@ -374,7 +374,7 @@ module.exports = app => {
                 const diff = costFields.curFields.find(x => {
                     return l[x] !== l['read_' + x];
                 });
-                if (diff) {
+                if (diff || !l.calc_his) {
                     const data = { id: l.id };
                     // cache read
                     for (const f of costFields.curFields) {

+ 2 - 1
app/service/cost_stage_ledger.js

@@ -48,6 +48,7 @@ module.exports = app => {
             });
             // this.depart = ctx.app.config.table_depart.light;
             this.tableName = 'cost_stage_ledger';
+            this.calcFields = costFields.calcFields;
         }
 
         // 继承方法
@@ -480,7 +481,7 @@ module.exports = app => {
                 const diff = costFields.curFields.find(x => {
                     return l[x] !== l['read_' + x];
                 });
-                if (diff) {
+                if (diff || !l.calc_his) {
                     const data = { id: l.id };
                     // cache read
                     for (const f of costFields.curFields) {

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

@@ -1,3 +1,4 @@
 <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('stage') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="审核比较" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- ctx.costStage.stage_order %>/compare" ml="3" active="<%= (ctx.url.indexOf('compare') >= 0 ? 1 : -1) %>"></nav-menu>
 <% include ./audit_btn.ejs %>

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

@@ -1,3 +1,4 @@
 <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('stage') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="审核比较" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- ctx.costStage.stage_order %>/compare" ml="3" active="<%= (ctx.url.indexOf('compare') >= 0 ? 1 : -1) %>"></nav-menu>
 <% include ./audit_btn.ejs %>

+ 205 - 0
app/view/cost/compare.ejs

@@ -0,0 +1,205 @@
+<% 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>
+            </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 tenderId = parseInt('<%- ctx.tender.id %>');
+    const stageId = parseInt('<%- ctx.costStage.id %>');
+    const stageType = parseInt('<%- ctx.costStage.stage_type %>');
+    const stageDate = '<%- ctx.costStage.stage_date %>';
+    <% if (ctx.costStage.stage_type === 'ledger') { %>
+    const bllsSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
+            {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        headColWidth: [50],
+        tipsSum: true,
+        readOnly: true,
+    };
+    const dealSpreadSetting = {
+        cols: [
+            {title: '合同编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '合同名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '乙方', colSpan: '1', rowSpan: '2', field: 'party_b', hAlign: 0, width: 150, formatter: '@'},
+            {title: '支付年月', colSpan: '1', rowSpan: '2', field: 'pay_date', hAlign: 1, width: 120, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    const commonSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 3,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    <% } %>
+    <% if (ctx.costStage.stage_type === 'book') { %>
+    const bllsSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
+            {title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        headColWidth: [50],
+        tipsSum: true,
+        readOnly: true,
+    };
+    const dealSpreadSetting = {
+        cols: [
+            {title: '合同编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '合同名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '乙方', colSpan: '1', rowSpan: '1', field: 'party_b', hAlign: 0, width: 150, formatter: '@'},
+            {title: '支付年月', colSpan: '1', rowSpan: '1', field: 'pay_date', hAlign: 1, width: 120, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    const commonSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 3,
+        headRows: 1,
+        headRowHeight: [25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    <% } %>
+    <% if (ctx.costStage.stage_type === 'analysis') { %>
+    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 )) %>'));
+    <% } %>
+    const calcFields = JSON.parse('<%- JSON.stringify(calcFields) %>');
+</script>

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

@@ -0,0 +1 @@
+<% include ./audit_modal.ejs %>

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

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

+ 24 - 2
config/web.js

@@ -2661,14 +2661,36 @@ const JsFiles = {
                 ],
                 mergeFile: 'cost_stage_analysis',
             },
+            cost_compare: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.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_compare.js',
+                ],
+                mergeFile: 'cost_compare',
+            },
             cost_gather: {
                 files: [
                     '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
                     '/public/js/decimal.min.js',
                     '/public/js/math.min.js',
                     '/public/js/component/menu.js',
-                    '/public/js/datepicker/datepicker.min.js',
-                    '/public/js/datepicker/datepicker.zh.js',
                     '/public/js/moment/moment.min.js',
                 ],
                 mergeFiles: [