瀏覽代碼

1. 成本报审,审批流程相关

MaiXinRong 3 月之前
父節點
當前提交
1b18d3808e

+ 235 - 25
app/controller/cost_controller.js

@@ -119,10 +119,12 @@ module.exports = app => {
                     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 renderData = {
                     stage_type,
                     auditType: audit.auditType,
                     stages,
+                    validLedgerStages,
                     auditConst: audit.common,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_stage)
                 };
@@ -160,13 +162,23 @@ module.exports = app => {
             try {
                 if (!ctx.permission.cost[stage_type + '_add']) throw '您无权创建期';
                 const stage_date = ctx.request.body.date;
-                if (!stage_date) throw '请选择年月';
+                if (stage_type !== 'book' && !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;
                 if (unCompleteCount.length > 0) throw '最新一期未审批通过,请审批通过后再新增';
 
-                const newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, stage_date);
+                let newStage;
+                if (stage_type === 'ledger') {
+                    newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, stage_date);
+                } 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 });
+                }
+
                 if (!newStage) throw '新增期失败';
                 ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}/${newStage.stage_order}`);
             } catch (err) {
@@ -193,23 +205,6 @@ module.exports = app => {
                 ctx.redirect(ctx.request.header.referer);
             }
         }
-        /**
-         * 期审批流程(POST)
-         * @param ctx
-         * @return {Promise<void>}
-         */
-        async loadAuditors(ctx) {
-            try {
-                const stageId = JSON.parse(ctx.request.body.data).id;
-                const stage = await ctx.service.costStage.get(stageId);
-                await ctx.service.costStage.loadUser(stage);
-                await ctx.service.costStage.loadAuditViewData(stage);
-                ctx.body = { err: 0, msg: '', data: stage };
-            } catch (error) {
-                ctx.log(error);
-                ctx.body = { err: 1, msg: error.toString(), data: null };
-            }
-        }
 
         async _getStageAuditViewData(ctx) {
             await this.ctx.service.costStage.loadAuditViewData(ctx.costStage);
@@ -305,7 +300,71 @@ module.exports = app => {
             }
         }
         async _bookLoad(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':
+                            const ledger = await ctx.service.costStageLedger.getReadData(ctx.costStage.relaStage);
+                            ledger.forEach(l => { delete l.postil; delete l.memo; });
+                            const book = ctx.costStage.readOnly
+                                ? await ctx.service.costStageBook.getReadData(ctx.costStage)
+                                : await ctx.service.costStageBook.getEditData(ctx.costStage);
+                            ctx.helper.assignRelaData(ledger, [
+                                { data: book, fields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'in_tp', 'in_excl_tax_tp', 'end_in_tp', 'end_in_excl_tax_tp', 'postil', 'memo'], prefix: '', relaId: 'ledger_id' },
+                                { data: book, fields: ['id'], prefix: 'book_', relaId: 'ledger_id' },
+                            ]);
+                            responseData.data[f] = ledger;
+                            break;
+                        case 'billsCompare':
+                            const ledgerC = await ctx.service.costStageLedger.getReadData(ctx.costStage.relaStage);
+                            ledgerC.forEach(l => { delete l.postil; delete l.memo; });
+                            const bookC = await ctx.service.costStageBook.getCompareData(ctx.costStage);
+                            ctx.helper.assignRelaData(ledgerC, [
+                                { data: bookC, fields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'calc_his', 'postil', 'memo'], prefix: '', relaId: 'ledger_id' },
+                            ]);
+                            responseData.data[f] = ledgerC;
+                            break;
+                        case 'detail':
+                            const detail = await ctx.service.costStageDetail.getReadData(ctx.costStage.relaStage);
+                            detail.forEach(l => { delete l.postil; delete l.memo; });
+                            const bookDetail = ctx.costStage.readOnly
+                                ? await ctx.service.costStageBookDetail.getReadData(ctx.costStage)
+                                : await ctx.service.costStageBookDetail.getEditData(ctx.costStage);
+                            ctx.helper.assignRelaData(detail, [
+                                { data: bookDetail, fields: ['in_tp', 'in_excl_tax_tp', 'postil', 'memo'], prefix: '', relaId: 'detail_id' },
+                                { data: bookDetail, fields: ['id'], prefix: 'book_', relaId: 'detail_id' },
+                            ]);
+                            responseData.data[f] = detail;
+                            break;
+                        case 'detailCompare':
+                            const detailC = await ctx.service.costStageLedger.getReadData(ctx.costStage.relaStage);
+                            detailC.forEach(l => { delete l.postil; delete l.memo; });
+                            const bookDetailC = await ctx.service.costStageBook.getCompareData(ctx.costStage);
+                            ctx.helper.assignRelaData(detailC, [
+                                { data: bookDetailC, fields: ['calc_his', 'postil', 'memo'], prefix: '', relaId: 'detail_id' },
+                            ]);
+                            responseData.data[f] = detailC;
+                            break;
+                        case 'auditFlow':
+                            responseData.data[f] = await ctx.service.costStageAudit.getViewFlow(ctx.costStage);
+                            break;
+                        case 'att':
+                            responseData.data[f] = await ctx.service.costStageFile.getData(ctx.costStage.id, 'DESC');
+                            break;
+                        default:
+                            responseData.data[f] = [];
+                            break;
+                    }
+                }
 
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
         }
         async _analysisLoad(ctx) {
 
@@ -377,12 +436,15 @@ module.exports = app => {
         async _bookUpdate(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
-                if (!data.postType || !data.postData) throw '数据错误';
+                if (!data.target || !data.update) throw '数据错误';
                 const responseData = { err: 0, msg: '', data: {} };
 
-                switch (data.postType) {
-                    case 'update':
-                        responseData.data = await this.ctx.service.costStageBook.updateCalc(ctx.costStage, data.postData);
+                switch (data.target) {
+                    case 'ledger':
+                        responseData.data = await this.ctx.service.costStageBook.updateDatas(data.update);
+                        break;
+                    case 'detail':
+                        responseData.data = await this.ctx.service.costStageBookDetail.updateDatas(data.update);
                         break;
                     default:
                         throw '未知操作';
@@ -459,7 +521,7 @@ module.exports = app => {
                 let stream = await parts();
                 const user = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
                 const rela_id = parts.field.rela_id;
-                const rela_sub_id = parts.field.rela_sub_id;
+                const rela_sub_id = parts.field.rela_sub_id || '';
 
                 const uploadfiles = [];
                 while (stream !== undefined) {
@@ -518,11 +580,159 @@ module.exports = app => {
                 const result = await ctx.service.costStageTag.update(data);
                 ctx.body = { err: 0, msg: '', data: result };
             } catch (err) {
-                console.log(err);
                 this.log(err);
                 ctx.body = this.ajaxErrorBody(err, '书签数据错误');
             }
         }
+
+        /**
+         * 添加审批人
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async addStageAudit(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const id = this.app._.toInteger(data.auditorId);
+                if (isNaN(id) || id <= 0) throw '参数错误';
+
+                // 检查权限等
+                if (ctx.costStage.create_user_id !== ctx.session.sessionUser.accountId) throw '您无权添加审核人';
+                if (ctx.costStage.audit_status !== audit.common.status.uncheck && ctx.costStage.audit_status !== audit.common.status.checkNo) {
+                    throw '当前不允许添加审核人';
+                }
+
+                // 检查审核人是否已存在
+                const exist = await ctx.service.costStageAudit.getDataByCondition({ stage_id: ctx.costStage.id, audit_times: ctx.costStage.audit_times, audit_id: id });
+                if (exist) throw '该审核人已存在,请勿重复添加';
+
+                const auditorInfo = await this.ctx.service.projectAccount.getDataById(id);
+                if (!auditorInfo) throw '添加的审批人不存在';
+
+                const shenpiInfo = await ctx.service.shenpiAudit.getDataByCondition({ tid: ctx.tender.id, sp_type: shenpiConst.sp_type.safe_payment, sp_status: shenpiConst.sp_status.gdzs });
+                const is_gdzs = shenpiInfo && ctx.tender.info.shenpi.safe_payment === shenpiConst.sp_status.gdzs ? 1 : 0;
+                const result = await ctx.service.costStageAudit.addAuditor(ctx.costStage.id, auditorInfo, ctx.costStage.audit_times, is_gdzs);
+                if (!result) throw '添加审核人失败';
+
+                const auditors = await ctx.service.costStageAudit.getAuditorGroup(ctx.costStage.id, ctx.costStage.audit_times);
+                ctx.body = { err: 0, msg: '', data: auditors };
+            } catch (err) {
+                ctx.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+        /**
+         * 移除审批人
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async deleteStageAudit(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const id = data.auditorId instanceof Number ? data.auditorId : this.app._.toNumber(data.auditorId);
+                if (isNaN(id) || id <= 0) throw '参数错误';
+
+                const result = await ctx.service.costStageAudit.deleteAuditor(ctx.costStage.id, id, ctx.costStage.audit_times);
+                if (!result) throw '移除审核人失败';
+
+                const auditors = await ctx.service.costStageAudit.getAuditors(ctx.costStage.id, ctx.costStage.audit_times);
+                ctx.body = { err: 0, msg: '', data: auditors };
+            } catch (err) {
+                ctx.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+        async stageAuditStart(ctx) {
+            try {
+                if (ctx.costStage.create_user_id !== ctx.session.sessionUser.accountId) throw '您无权上报该期数据';
+                if (ctx.costStage.revising) throw '台账修订中,不可上报';
+                if (ctx.costStage.audit_status !== audit.common.status.uncheck && ctx.costStage.audit_status !== audit.common.status.checkNo) throw '该期数据当前无法上报';
+
+                await ctx.service.costStageAudit.start(ctx.costStage);
+                ctx.redirect(ctx.request.header.referer);
+            } 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`);
+            }
+        }
+        async stageAuditCheck(ctx) {
+            try {
+                if (!ctx.costStage || (ctx.costStage.audit_status !== audit.common.status.checking && ctx.costStage.audit_status !== audit.common.status.checkNoPre)) {
+                    throw '当前期数据有误';
+                }
+                if (ctx.costStage.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) < 0) {
+                    throw '您无权进行该操作';
+                }
+                if (ctx.costStage.revising) throw '台账修订中,不可审批';
+
+                const checkType = parseInt(ctx.request.body.checkType);
+                const opinion = ctx.request.body.opinion.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+                await ctx.service.costStageAudit.check(ctx.costStage, checkType, opinion);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '审批失败');
+            }
+            ctx.redirect(ctx.request.header.referer);
+        }
+        async stageAuditCheckAgain(ctx) {
+            try {
+                if (!ctx.costStage.isLatest) throw '非最新一期,不可重新审批';
+                if (ctx.costStage.audit_status !== audit.common.status.checked) throw '未审批完成,不可重新审批';
+                if (ctx.costStage.revising) throw '台账修订中,不可重审';
+
+                if (ctx.session.sessionUser.loginStatus === 0) {
+                    const user = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                    if (!user.auth_mobile) throw '未绑定手机号';
+
+                    const code = ctx.request.body.code;
+                    const cacheKey = 'smsCode:' + ctx.session.sessionUser.accountId;
+                    const cacheCode = await app.redis.get(cacheKey);
+                    if (cacheCode === null || code === undefined || cacheCode !== (code + pa.auth_mobile)) {
+                        throw '验证码不正确!';
+                    }
+                }
+
+                const adminCheckAgain = ctx.request.body.confirm === '确认设置终审审批' && ctx.session.sessionUser.is_admin;
+                if (ctx.costStage.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) < 0 && !adminCheckAgain) throw '您无权重新审批';
+
+                await ctx.service.costStageAudit.checkAgain(ctx.costStage, adminCheckAgain);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '重新审批失败');
+            }
+            ctx.redirect(ctx.request.header.referer);
+        }
+        async stageAuditCheckCancel(ctx) {
+            try {
+                if (ctx.costStage.revising) throw '台账修订中,不可撤回';
+                if (!ctx.costStage.cancancel) throw '您无权进行该操作';
+
+                await ctx.service.costStageAudit.checkCancel(ctx.costStage);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '撤回失败');
+            }
+            ctx.redirect(ctx.request.header.referer);
+        }
+        /**
+         * 期审批流程(POST)
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async loadAuditors(ctx) {
+            try {
+                const stageOrder = JSON.parse(ctx.request.body.data).order;
+                const stageType = ctx.params.stype;
+                const stage = await ctx.service.costStage.getStageByOrder(ctx.tender.id, stageType, stageOrder);
+                await ctx.service.costStage.loadUser(stage);
+                await ctx.service.costStage.loadAuditViewData(stage);
+                ctx.body = { err: 0, msg: '', data: stage };
+            } catch (error) {
+                ctx.log(error);
+                ctx.body = { err: 1, msg: error.toString(), data: null };
+            }
+        }
     }
 
     return CostController;

+ 1 - 0
app/middleware/cost_stage_check.js

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

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

@@ -137,7 +137,7 @@ $(document).ready(() => {
     };
     // 获取审批流程
     $('a[data-target="#sp-list" ]').on('click', function () {
-        postData('stage/auditors', { order: $(this).attr('stage-order') }, function (result) {
+        postData(window.location.pathname + '/auditors', { order: $(this).attr('stage-order') }, function (result) {
             $('#auditor-list').html(getAuditorsHtml(result.hisUserGroup));
             $('#audit-list').html(getAuditHistroyHtml(result.auditHistory));
         });

+ 782 - 0
app/public/js/cost_stage_book.js

@@ -0,0 +1,782 @@
+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: ['cur_tp', 'pre_tp', 'end_tp'],
+                keys: ['id', 'stage_id', 'tree_id'],
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.spreadSetting = {
+                cols: [
+                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree', readOnly: true},
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip', readOnly: true},
+                    {title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit', readOnly: true},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {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', readOnly: true},
+                    {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
+                    {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: readOnly,
+                localCache: {
+                    key: 'cost_book',
+                    colWidth: true,
+                },
+            };
+            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;
+            $('#bills-spread').focusin(this.focusIn);
+        }
+        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;
+
+            setObjEnable($('a[name=base-opr][type=add]'), valid && first);
+            setObjEnable($('a[name=base-opr][type=delete]'), valid && first && sameParent && !nodeUsed);
+            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) && !nodeUsed && first.tree_level > 1);
+            setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && sameParent && preNode && !this.checkNodeUsed(tree, preNode));
+        }
+        loadRelaData(loadAtt = true) {
+            this.refreshOperationValid();
+            SpreadJsObj.saveTopAndSelect(this.sheet, this.ckBillsSpread);
+            const select = SpreadJsObj.getSelectObject(this.sheet);
+            detailObj.loadDetailData(select);
+            if (loadAtt) costFile.getCurAttHtml(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;
+        }
+        // 事件
+        focusIn() {
+            const select = SpreadJsObj.getSelectObject(billsObj.sheet);
+            costFile.getCurAttHtml(select);
+        }
+        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;
+            }
+
+            switch (col.field) {
+                case 'in_tp':
+                    info.cancel = (node.children && node.children.length > 0) || node.had_detail;
+                    if (!info.cancel) {
+                        const detailRange = detailObj.data.getPartData(node.id);
+                        if (detailRange && detailRange.length > 0) info.cancel = true;
+                    }
+                    break;
+            }
+        }
+        editEnded(e, info) {
+            if (!info.sheet.zh_setting) return;
+
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            const data = { book_id: node.book_id, ledger_id: node.id, cost_id: node.cost_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', update: 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 = { book_id: node.book_id, ledger_id: node.id, cost_id: node.cost_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: 'ledger', update: 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 = { book_id: node.book_id, ledger_id: node.id, cost_id: node.cost_id };
+                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', update: 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 = {
+                cols: [
+                    {title: '合同编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@', readOnly: true},
+                    {title: '合同名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@', readOnly: true},
+                    {title: '乙方', colSpan: '1', rowSpan: '1', field: 'party_b', hAlign: 0, width: 150, formatter: '@', readOnly: true},
+                    {title: '合同金额', colSpan: '1', rowSpan: '1', field: 'deal_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {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', readOnly: true},
+                    {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
+                    {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: readOnly,
+                localCache: {
+                    key: 'cost_book_deal',
+                    colWidth: true,
+                },
+            };
+            this.commonSpreadSetting = {
+                cols: [
+                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@', readOnly: true},
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@', readOnly: true},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {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', readOnly: true},
+                    {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
+                    {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: readOnly,
+                localCache: {
+                    key: 'cost_book_detail',
+                    colWidth: true,
+                },
+            };
+
+            this.initSpread();
+            this.initOtherEvent();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.commonSpreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            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);
+        }
+        initOtherEvent() {
+            const self = this;
+            $('#detail-spread').focusin(this.focusIn);
+        }
+        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 spreadSetting = bills && bills.is_deal ? this.dealSpreadSetting : this.commonSpreadSetting;
+            if (!bills || (bills.children && bills.children.length > 0)) {
+                spreadSetting.readOnly = true;
+            } else {
+                spreadSetting.readOnly = readOnly;
+            }
+            SpreadJsObj.initSheet(this.sheet, spreadSetting);
+            this.reloadDetailData();
+        }
+        loadRelaData() {
+            const select = SpreadJsObj.getSelectObject(this.sheet);
+            if (select) costFile.getCurAttHtml(this.billsNode, select);
+        }
+        // 事件
+        focusIn() {
+            detailObj.loadRelaData();
+        }
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    detailObj.loadRelaData();
+                }
+            }
+        }
+        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 = { book_id: detailData.book_id, detail_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();
+                result.ledger.tree_id = relaBills.tree_id;
+                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 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 = { book_id: node.book_id, detail_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) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    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 = tree.nodes[curRow];
+                if (!node) continue;
+
+                let bPaste = false;
+                const data = { book_id: node.book_id, detail_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();
+                }, 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 costFile = $.ledger_att({
+        selector: '#fujian',
+        key: 'id',
+        subKey: 'id',
+        masterKey: 'rela_id',
+        subMasterKey: 'rela_sub_id',
+        uploadUrl: 'file/upload',
+        deleteUrl: 'file/delete',
+        checked: false,
+        zipName: `附件.zip`,
+        readOnly: false,
+        fileIdType: 'string',
+        fileInfo: {
+            user_name: 'user_name',
+            user_id: 'user_id',
+            create_time: 'create_time',
+        },
+        getCurHint: function(node) {
+            return`${node.name || ''}`;
+        },
+        locate: function (att) {
+            if (!att) return;
+            SpreadJsObj.locateTreeNode(billsObj.sheet, att.node.tree_id, true);
+            detailObj.loadDetailData(att.node);
+            if (att.sub_node) SpreadJsObj.locateDataBy(detailObj.sheet, n => { return n.id === att.sub_node.id; });
+            costFile.getCurAttHtml(att.node, att.sub_node);
+        }
+    });
+    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' }, function(result) {
+        billsObj.loadData(result.bills);
+        detailObj.loadData(result.detail);
+
+        for (const f of result.att) {
+            f.node = billsObj.tree.datas.find(x => { return x.id === f.rela_id; });
+            f.sub_node = f.rela_sub_id ? detailObj.data.getItem(f.rela_sub_id) : null;
+        }
+        costFile.loadDatas(result.att);
+        costFile.getCurAttHtml(SpreadJsObj.getSelectObject(billsObj.sheet));
+    });
+
+    // 展开收起标准清单
+    $('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);
+});

+ 21 - 21
app/public/js/cost_stage_ledger.js

@@ -36,8 +36,8 @@ $(document).ready(function() {
                     {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', readOnly: true},
                     {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
-                    {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    // {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    // {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -318,7 +318,7 @@ $(document).ready(function() {
             };
             if (type === 'delete') {
                 deleteAfterHint(function () {
-                    postData(window.location.pathname + '/update', updateData, function (result) {
+                    postData('update', updateData, function (result) {
                         const refreshData = tree.loadPostData(result);
                         self.refreshTree(refreshData);
                         if (sel) {
@@ -329,7 +329,7 @@ $(document).ready(function() {
                     });
                 });
             } else {
-                postData(window.location.pathname + '/update', updateData, function (result) {
+                postData('update', updateData, function (result) {
                     const refreshData = tree.loadPostData(result);
                     self.refreshTree(refreshData);
                     if (['up-move', 'down-move'].indexOf(type) > -1) {
@@ -420,7 +420,7 @@ $(document).ready(function() {
                 data[col.field] = col.type === 'Number' ? 0 : '';
             }
             // 更新至服务器
-            postData(window.location.pathname + '/update', {target: 'ledger', postType: 'update', postData: data}, function (result) {
+            postData('update', {target: 'ledger', postType: 'update', postData: data}, function (result) {
                 const refreshNode = billsObj.tree.loadPostData(result);
                 billsObj.refreshTree(refreshNode);
             }, function () {
@@ -445,7 +445,7 @@ $(document).ready(function() {
                 if (bDel) datas.push(data);
             }
             if (datas.length > 0) {
-                postData(window.location.pathname + '/update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                postData('update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
                     const refreshNode = sheet.zh_tree.loadPostData(result);
                     billsObj.refreshTree(refreshNode);
                 }, function () {
@@ -489,7 +489,7 @@ $(document).ready(function() {
                 }
             }
             if (datas.length > 0) {
-                postData(window.location.pathname + '/update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                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);
@@ -518,8 +518,8 @@ $(document).ready(function() {
                     {title: '扣款金额', colSpan: '1', rowSpan: '1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '应付金额', colSpan: '1', rowSpan: '1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
-                    {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
-                    {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    // {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    // {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -575,8 +575,8 @@ $(document).ready(function() {
                     {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', readOnly: true},
                     {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
-                    {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    // {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    // {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -688,7 +688,7 @@ $(document).ready(function() {
                 if (updateData.del.length === 0) return;
 
                 deleteAfterHint(function () {
-                    postData(window.location.pathname + '/update', updateData, function (result) {
+                    postData('update', updateData, function (result) {
                         billsTag.afterDeletePos(result.detail.del.map(x => { return detailObj.data.getItem(x); }));
                         costFile.deleteFileBySubNodeId(result.detail.del);
                         self.data.updateDatas(result.detail);
@@ -761,7 +761,7 @@ $(document).ready(function() {
                 data.add = [addData];
             }
             // 更新至服务器
-            postData(window.location.pathname + '/update', data, function (result) {
+            postData('update', data, function (result) {
                 detailObj.data.updateDatas(result.detail);
                 detailObj.refreshSheet();
                 result.ledger.tree_id = relaBills.tree_id;
@@ -789,7 +789,7 @@ $(document).ready(function() {
                 if (bDel) datas.push(data);
             }
             if (datas.length > 0) {
-                postData(window.location.pathname + '/update', {target: 'detail', update: datas}, function (result) {
+                postData('update', {target: 'detail', update: datas}, function (result) {
                     const refreshNode = sheet.zh_tree.loadPostData(result);
                     billsObj.refreshTree(refreshNode);
                 }, function () {
@@ -861,7 +861,7 @@ $(document).ready(function() {
                 }
             }
             if (datas.length > 0) {
-                postData(window.location.pathname + '/update', {target: 'detail', update: datas}, function (result) {
+                postData('update', {target: 'detail', update: datas}, function (result) {
                     detailObj.data.updateDatas(result.detail);
                     detailObj.refreshSheet();
                 }, function () {
@@ -966,7 +966,7 @@ $(document).ready(function() {
                     toastr.warning('请至少选择一种类型');
                     return;
                 }
-                postData(window.location.pathname + '/update', updateData, function(result) {
+                postData('update', updateData, function(result) {
                     detailObj.data.updateDatas(result.detail);
                     detailObj.reloadDetailData();
                     result.ledger.tree_id = curNode.tree_id;
@@ -980,7 +980,7 @@ $(document).ready(function() {
                 $('#import-deal-type').modal('show');
             };
             const loadContractTree = function() {
-                postData(window.location.pathname + '/load', { filter: 'contract'}, function(result) {
+                postData('load', { filter: 'contract'}, function(result) {
                     contractTree = createNewPathTree('base', { id: 'contract_id', pid: 'contract_pid', order: 'order', level: 'level', fullPath: 'full_path', rootId: -1 });
                     contractTree.loadDatas(result.contract);
                     SpreadJsObj.loadSheetData(contractSelectSheet, SpreadJsObj.DataType.Tree, contractTree);
@@ -999,7 +999,7 @@ $(document).ready(function() {
                     toastr.warning('请至少选择一项合同数据');
                     return;
                 }
-                postData(window.location.pathname + '/update', updateData, function(result) {
+                postData('update', updateData, function(result) {
                     detailObj.data.updateDatas(result.detail);
                     detailObj.refreshSheet();
                     result.ledger.tree_id = curNode.tree_id;
@@ -1216,8 +1216,8 @@ $(document).ready(function() {
         subKey: 'id',
         masterKey: 'rela_id',
         subMasterKey: 'rela_sub_id',
-        uploadUrl: window.location.pathname + '/file/upload',
-        deleteUrl: window.location.pathname + '/file/delete',
+        uploadUrl: 'file/upload',
+        deleteUrl: 'file/delete',
         checked: false,
         zipName: `附件.zip`,
         readOnly: false,
@@ -1274,7 +1274,7 @@ $(document).ready(function() {
         },
     });
     // 加载数据
-    postData(window.location.pathname + '/load', { filter: 'bills;detail;att;tags' }, function(result) {
+    postData('load', { filter: 'bills;detail;att;tags' }, function(result) {
         billsObj.loadData(result.bills);
         detailObj.loadData(result.detail);
 

+ 8 - 2
app/router.js

@@ -541,14 +541,20 @@ module.exports = app => {
     app.get('/sp/:id/cost/tender/:tid/analysis', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.analysis');
     app.post('/sp/:id/cost/tender/:tid/addStage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.addStage');
     app.post('/sp/:id/cost/tender/:tid/delStage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.delStage');
-    app.post('/sp/:id/cost/tender/:tid/auditors', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.loadAuditors');
-    app.get('/sp/:id/cost/tender/:tid/:stype/:sorder', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stage');
+    app.post('/sp/:id/cost/tender/:tid/:stype/auditors', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.loadAuditors');
+    app.get('/sp/:id/cost/tender/:tid/:stype/:sorder/stage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stage');
     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');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.deleteStageFile');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/tag', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.tag');
     app.get('/sp/:id/cost/tender/:tid/flow', sessionAuth, subProjectCheck, tenderCheck, projectManagerCheck, 'tenderController.shenpiSet');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/add', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.addStageAudit');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.deleteStageAudit');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/start', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditStart');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/check', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditCheck');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/checkAgain', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditCheckAgain');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/checkCancel', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditCheckCancel');
 
     // 安全管理
     // 安全计量

+ 13 - 7
app/service/cost_stage.js

@@ -25,9 +25,9 @@ module.exports = app => {
             super(ctx);
             this.tableName = 'cost_stage';
             this.stageType = {
-                ledger: { key: 'ledger', decimal: {tp: 6}, shenpi_status: 'cost_ledger', push_type: 'costStageLedger', dataService: 'costStageLedger' },
-                book: { key: 'book', decimal: {tp: 6}, shenpi_status: 'cost_book', push_type: 'costStageBook', dataService: 'costStageBook' },
-                analysis: { key: 'analysis', decimal: {tp: 6}, shenpi_status: 'cost_analysis', push_type: 'costStageAnalysis', dataService: 'costStageAnalysis' },
+                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' },
             };
         }
 
@@ -38,7 +38,11 @@ module.exports = app => {
 
             const typeInfo = this.stageType[stages[0].stage_type];
             stages.forEach(s => {
-                s.decimal = s.decimal ? JSON.parse(s.decimal) : typeInfo.decimal;
+                if (s.decimal) {
+                    s.decimal = this.ctx.helper._.assignIn(typeInfo.decimal, JSON.parse(s.decimal));
+                } else {
+                    s.decimal = typeInfo.decimal;
+                }
                 s.stage_tp = s.stage_tp ? JSON.parse(s.stage_tp) : {};
                 s.stage_pre_tp = s.stage_pre_tp ? JSON.parse(s.stage_pre_tp) : {};
                 s.stage_end_tp = {};
@@ -46,6 +50,7 @@ module.exports = app => {
                     s.stage_end_tp[prop] = this.ctx.helper.add(s.stage_tp[prop], s.stage_pre_tp[prop]);
                 }
                 s.typeInfo = this.stageType[s.stage_type];
+                s.rela_stage = s.rela_stage ? JSON.parse(s.rela_stage) : null;
             });
         }
         /**
@@ -78,7 +83,7 @@ module.exports = app => {
             return result.max_order || 0;
         }
 
-        async add(tid, stage_type, stage_date) {
+        async add(tid, stage_type, stage_date, rela_stage) {
             const typeInfo = this.stageType[stage_type];
             if (!tid) throw '数据错误';
             const user_id = this.ctx.session.sessionUser.accountId;
@@ -90,6 +95,7 @@ module.exports = app => {
                 audit_times: 1, audit_status: audit.status.uncheck,
                 decimal: JSON.stringify({ tp: 6 }),
             };
+            if (rela_stage) data.rela_stage = JSON.stringify(rela_stage);
 
             const preStage = maxOrder > 0 ? await this.getStageByOrder(tid, stage_type, maxOrder) : null;
             if (preStage) {
@@ -269,7 +275,7 @@ module.exports = app => {
                 const auditList = await this.ctx.service.costStageAudit.getAllDataByCondition({ where: { stage_id: stage.id, audit_times: stage.audit_times }, orders: [['audit_order', 'asc']] });
                 auditList.shift();
                 if (shenpi_status === shenpiConst.sp_status.gdspl) {
-                    const shenpiList = await this.ctx.service.shenpiAudit.getAllDataByCondition({ where: { tid: stage.tid, sp_type: shenpiConst.sp_type[typeInfo.shenpi_status], sp_status: shenpi_status } });
+                    const shenpiList = await this.ctx.service.shenpiAudit.getAllDataByCondition({ where: { tid: stage.tid, sp_type: shenpiConst.cost_sp_type[typeInfo.shenpi_status], sp_status: shenpi_status } });
                     // 判断2个id数组是否相同,不同则删除原审批流,切换成固定的审批流
                     let sameAudit = auditList.length === shenpiList.length;
                     if (sameAudit) {
@@ -286,7 +292,7 @@ module.exports = app => {
                         await this.loadUser(stage);
                     }
                 } else if (shenpi_status === shenpiConst.sp_status.gdzs) {
-                    const shenpiInfo = await this.ctx.service.shenpiAudit.getDataByCondition({ tid: stage.tid, sp_type: shenpiConst.sp_type[typeInfo.shenpi_status], sp_status: shenpi_status });
+                    const shenpiInfo = await this.ctx.service.shenpiAudit.getDataByCondition({ tid: stage.tid, sp_type: shenpiConst.cost_sp_type[typeInfo.shenpi_status], sp_status: shenpi_status });
                     // 判断最后一个id是否与固定终审id相同,不同则删除原审批流中如果存在的id和添加终审
                     const lastAuditors = auditList.filter(x => { x.active_order === auditList.active_order; });
                     if (shenpiInfo && (lastAuditors.length === 0 || (lastAuditors.length > 1 || shenpiInfo.audit_id !== lastAuditors[0].audit_id))) {

+ 11 - 6
app/service/cost_stage_audit.js

@@ -460,6 +460,11 @@ module.exports = app => {
             await this.ctx.service[stage.typeInfo.dataService].auditCache(transaction, stage.id, {
                 user_id: audit.audit_id, audit_times: audit.audit_times, audit_order: audit.audit_order, active_order: audit.active_order,
             });
+            if (stage.typeInfo.detailService) {
+                await this.ctx.service[stage.typeInfo.detailService].auditCache(transaction, stage.id, {
+                    user_id: audit.audit_id, audit_times: audit.audit_times, audit_order: audit.audit_order, active_order: audit.active_order,
+                });
+            }
         }
         async _cachePreAuditData(transaction, stage, audit) {
             await this.ctx.service[stage.typeInfo.dataService].auditCachePre(transaction, stage.id, {
@@ -488,7 +493,7 @@ module.exports = app => {
                 // 计算 安全生产费
                 const billsSum = await this.ctx.service[stage.typeInfo.dataService].getSum(stage);
                 await transaction.update(this.ctx.service.costStage.tableName, {
-                    id: stage.id, audit_status: auditConst.costStage.status.checking, audit_max_sort: 1, audit_begin_time: audit_time, ...billsSum
+                    id: stage.id, audit_status: auditConst.costStage.status.checking, audit_max_sort: 1, audit_begin_time: audit_time, stage_tp: JSON.stringify(billsSum)
                 });
                 // 缓存 本流程数据
                 await this._cacheAuditData(transaction, stage, { audit_id: stage.user_id, audit_times: stage.audit_times, audit_order: 0, active_order: 0 });
@@ -579,7 +584,7 @@ module.exports = app => {
                         const updateData = nextAudits.map(x => { return { id: x.id, audit_status: auditConst.costStage.status.checking }; });
                         await transaction.updateRows(this.tableName, updateData);
                         await transaction.update(this.ctx.service.costStage.tableName, {
-                            id: stage.id, audit_max_sort: selfAudit.active_order + 1, audit_status: auditConst.costStage.status.checking, ...billsSum
+                            id: stage.id, audit_max_sort: selfAudit.active_order + 1, audit_status: auditConst.costStage.status.checking, stage_tp: JSON.stringify(billsSum)
                         });
 
                         // todo 添加短信通知-需要审批提醒功能
@@ -611,7 +616,7 @@ module.exports = app => {
                             ? `${selfAudit.name}${(selfAudit.role ? '-' + selfAudit.role : '')}`
                             : this.ctx.helper.transFormToChinese(selfAudit.audit_order) + '审';
                         await transaction.update(this.ctx.service.costStage.tableName, {
-                            id: stage.id, audit_end_time: time, audit_status: auditConst.costStage.status.checked, final_auditor_str, ...billsSum
+                            id: stage.id, audit_end_time: time, audit_status: auditConst.costStage.status.checked, final_auditor_str, stage_tp: JSON.stringify(billsSum)
                         });
                         // // 添加短信通知-审批通过提醒功能
                         // const users = this._.uniq(stage.userIds);
@@ -634,7 +639,7 @@ module.exports = app => {
                     // await this.ctx.service.specMsg.addCostStageMsg(transaction, this.ctx.session.sessionProject.id, stage, pushOperate.stage.flow);
                 } else {
                     // 同步 期信息
-                    await transaction.update(this.ctx.service.costStage.tableName, { id: stage.id, audit_status: auditConst.costStage.status.checking, ...billsSum });
+                    await transaction.update(this.ctx.service.costStage.tableName, { id: stage.id, audit_status: auditConst.costStage.status.checking, stage_tp: JSON.stringify(billsSum) });
                 }
                 await transaction.commit();
             } catch (err) {
@@ -683,7 +688,7 @@ module.exports = app => {
                 // 更新 期信息
                 const billsSum = await this.ctx.service[stage.typeInfo.dataService].getSum(stage);
                 await transaction.update(this.ctx.service.costStage.tableName, {
-                    id: stage.id, audit_status: auditConst.costStage.status.checkNo, audit_times: stage.audit_times + 1, audit_max_sort: 0, ...billsSum
+                    id: stage.id, audit_status: auditConst.costStage.status.checkNo, audit_times: stage.audit_times + 1, audit_max_sort: 0, stage_tp: JSON.stringify(billsSum)
                 });
                 // 缓存 本流程计量数据
                 await this._cacheAuditData(transaction, stage, selfAudit);
@@ -782,7 +787,7 @@ module.exports = app => {
                 // 同步 期信息
                 const billsSum = await this.ctx.service[stage.typeInfo.dataService].getSum(stage);
                 await transaction.update(this.ctx.service.costStage.tableName, {
-                    id: stage.id, audit_max_sort: selfAudit.active_order + 1, ...billsSum,
+                    id: stage.id, audit_max_sort: selfAudit.active_order + 1, stage_tp: JSON.stringify(billsSum),
                 });
                 // 缓存 本流程数据
                 await this._cacheAuditData(transaction, stage, selfAudit);

+ 158 - 0
app/service/cost_stage_book.js

@@ -0,0 +1,158 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const costFields = {
+    textFields: ['postil', 'memo'],
+    preFields: ['pre_in_tp', 'pre_in_excl_tax_tp'],
+    curFields: ['in_tp', 'in_excl_tax_tp'],
+    readFields: ['read_in_tp', 'read_in_excl_tax_tp'],
+    baseFields: ['id', 'tender_id', 'stage_id', 'ledger_id', 'cost_id'],
+};
+costFields.calcFields = [...costFields.curFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, 'calc_his'];
+
+module.exports = app => {
+    class CostStageBook extends app.BaseService {
+
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'cost_stage_book';
+        }
+
+        async getEditData(stage) {
+            const helper = this.ctx.helper;
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.editQueryFields
+            });
+            result.forEach(x => {
+                for(const prop of costFields.curFields) {
+                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
+                }
+            });
+            return result;
+        }
+        async getReadData(stage) {
+            const helper = this.ctx.helper;
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.readQueryFields
+            });
+            result.forEach(x => {
+                for(const prop of costFields.curFields) {
+                    x[prop] = x['read_' + prop];
+                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
+                }
+            });
+            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 initStageData(transaction, stage, preStage) {
+            if (!preStage) return;
+
+            if (!stage || !transaction) throw '财务账面数据错误';
+
+            const preBooks = await this.getAllDataByCondition({
+                where: { stage_id: preStage.id },
+                columns: costFields.editQueryFields
+            });
+            const insertData = [];
+            for (const b of preBooks) {
+                b.id = this.uuid.v4();
+                b.stage_id = stage.id;
+                delete b.postil;
+                for(const prop of costFields.curFields) {
+                    b['pre_' + prop] = this.ctx.helper.add(b['pre_' + prop], b[prop]);
+                    b[prop] = 0;
+                }
+                insertData.push(b);
+            }
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === insertData.length;
+        }
+
+        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 orgLedger = await this.ctx.service.costStageLedger.getAllDataByCondition({
+                where: {
+                    stage_id: this.ctx.costStage.relaStage.id,
+                    id: this.ctx.helper._.map(datas, 'ledger_id')
+                }
+            });
+            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 updateDetail = [], insertDetail = [];
+            for (const d of datas) {
+                const od = orgDatas.find(x => { return x.id === d.book_id; });
+                const cl = orgLedger.find(x => { return x.id === d.ledger_id });
+                if (!cl) throw '关联期台账错误';
+
+                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, cost_id: d.cost_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.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(cl.tax, 100));
+                    nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp, divNum, this.ctx.costStage.decimal.tp);
+                }
+                if (od) {
+                    updateDetail.push(nd);
+                } else {
+                    insertDetail.push(nd);
+                }
+            }
+
+            const conn = await this.db.beginTransaction();
+            try {
+                if (updateDetail.length > 0) await conn.updateRows(this.tableName, updateDetail);
+                if (insertDetail.length > 0) await conn.insert(this.tableName, insertDetail);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            insertDetail.forEach(i => {
+                i.book_id = i.id;
+                delete i.id;
+                const cl = orgLedger.find(x => { return x.id === i.ledger_id });
+                i.tree_id = cl.tree_id;
+            });
+            updateDetail.forEach(u => {
+                u.book_id = u.id;
+                delete u.id;
+                const od = orgDatas.find(x => { return x.id === u.book_id; });
+                const cl = orgLedger.find(x => { return x.id === od.ledger_id; });
+                u.tree_id = cl.tree_id;
+            });
+            return { update: [...insertDetail, ...updateDetail] };
+        }
+    }
+
+    return CostStageBook;
+};

+ 184 - 0
app/service/cost_stage_book_detail.js

@@ -0,0 +1,184 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const costFields = {
+    textFields: ['postil', 'memo'],
+    curFields: ['in_tp', 'in_excl_tax_tp'],
+    readFields: ['read_in_tp', 'read_in_excl_tax_tp'],
+    baseFields: ['id', 'tender_id', 'stage_id', 'detail_id'],
+};
+costFields.calcFields = [...costFields.curFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.curFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.readFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.curFields, 'calc_his'];
+
+module.exports = app => {
+    class CostStageBookDetail extends app.BaseService {
+
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'cost_stage_book_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 => {
+                for(const prop of costFields.curFields) {
+                    x[prop] = x['read_' + prop];
+                }
+            });
+            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 _addDatas(data) {
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            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] || '';
+                }
+                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;
+            }
+            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]
+        }
+        async _updateDatas (data) {
+            if (!data || data.length === 0) throw '提交数据错误';
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const datas = data instanceof Array ? data : [data];
+            const orgDatas = await this.getAllDataByCondition({
+                where: { id: this.ctx.helper._.map(datas, 'id') }
+            });
+            if (!orgDatas || orgDatas.length === 0) throw '修改的明细不存在';
+
+            const uDatas = [];
+            for (const d of datas) {
+                const od = orgDatas.find(x => { return x.id === d.id; });
+                if (!od) continue;
+
+                const nd = { id: od.id, update_user_id: user_id };
+                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.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);
+                }
+                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 [];
+            }
+        }
+        async updateDatas(data) {
+            const result = { detail: { add: [], del: [], update: [] }, ledger: {} };
+            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);
+                }
+                return result;
+            } catch (err) {
+                if (err.stack) {
+                    throw err;
+                } else {
+                    result.err = err.toString();
+                    return result;
+                }
+            }
+        }
+    }
+
+    return CostStageBookDetail;
+};

+ 34 - 15
app/service/cost_stage_detail.js

@@ -10,16 +10,15 @@
 
 const costFields = {
     textFields: ['code', 'name', 'party_b', 'postil', 'memo'],
-    preFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp', 'pre_yf_excl_tax_tp', 'pre_sf_excl_tax_tp'],
     curFields: ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp'],
     readFields: ['read_pay_tp', 'read_cut_tp', 'read_yf_tp', 'read_sf_tp', 'read_yf_excl_tax_tp', 'read_sf_excl_tax_tp'],
     taxFields: ['tax'],
     baseFields: ['id', 'tender_id', 'stage_id', 'ledger_id', 'cost_id', 'd_order', 'is_deal', 'is_used'],
 };
 costFields.calcFields = [...costFields.curFields];
-costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, ...costFields.taxFields];
-costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields, ...costFields.taxFields];
-costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, ...costFields.taxFields, 'calc_his'];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.curFields, ...costFields.taxFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.readFields, ...costFields.taxFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.curFields, ...costFields.taxFields, 'calc_his'];
 
 module.exports = app => {
     class CostStageDetail extends app.BaseService {
@@ -27,24 +26,16 @@ module.exports = app => {
         constructor(ctx) {
             super(ctx);
             this.tableName = 'cost_stage_detail';
-            this.taxDecimal = 2;
         }
 
         async getEditData(stage) {
-            const helper = this.ctx.helper;
             const result = await this.getAllDataByCondition({
                 where: { stage_id: stage.id },
                 columns: costFields.editQueryFields
             });
-            result.forEach(x => {
-                for(const prop of costFields.curFields) {
-                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
-                }
-            });
             return result;
         }
         async getReadData(stage) {
-            const helper = this.ctx.helper;
             const result = await this.getAllDataByCondition({
                 where: { stage_id: stage.id },
                 columns: costFields.readQueryFields
@@ -52,7 +43,6 @@ module.exports = app => {
             result.forEach(x => {
                 for(const prop of costFields.curFields) {
                     x[prop] = x['read_' + prop];
-                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
                 }
             });
             return result;
@@ -108,7 +98,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.taxDecimal) : 0;
+                    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);
@@ -198,7 +188,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.taxDecimal) : od.tax;
+                    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);
@@ -296,6 +286,35 @@ module.exports = app => {
         async deletePartData(transaction, tender_id, ledger_id) {
             await transaction.delete(this.tableName, { tid, ledger_id });
         }
+
+        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);
+        }
     }
 
     return CostStageDetail;

+ 6 - 8
app/service/cost_stage_ledger.js

@@ -48,8 +48,6 @@ module.exports = app => {
             });
             // this.depart = ctx.app.config.table_depart.light;
             this.tableName = 'cost_stage_ledger';
-            this.decimal = { tp: 6 };
-            this.taxDecimal = 2;
         }
 
         // 继承方法
@@ -108,7 +106,7 @@ module.exports = app => {
 
         async init(stage, transaction) {
             if (!this.ctx.subProject.cost_ledger_template) throw '未设置台账模板,请联系管理员设置';
-            if (!stage || !transaction) throw '安全生产费数据错误';
+            if (!stage || !transaction) throw '成本报审数据错误';
 
             const templateData = await this.ctx.service.tenderNodeTemplate.getData(this.ctx.subProject.cost_ledger_template);
             if (templateData.length === 0) throw '台账模板无数据,连请联系管理员修改';
@@ -127,7 +125,7 @@ module.exports = app => {
             return operate.affectedRows === insertData.length;
         }
         async initByPre(stage, preStage, transaction) {
-            if (!stage || !preStage || !transaction) throw '安全生产费数据错误';
+            if (!stage || !preStage || !transaction) throw '成本报审数据错误';
 
             const preBills = await this.getAllDataByCondition({
                 where: { stage_id: preStage.id },
@@ -307,7 +305,7 @@ module.exports = app => {
             if (!targetData) throw '粘贴数据错误';
 
             const newParentPath = targetData.full_path.replace(targetData.tree_id, '');
-            const tpDecimal = this.ctx.costStage && this.ctx.costStage.decimal ? this.ctx.costStage.decimal : this.decimal;
+            const tpDecimal = this.ctx.costStage.decimal;
 
             const pasteBillsData = [], leafBillsId = [];
             let maxId = await this._getMaxLid(this.ctx.tender.id);
@@ -424,7 +422,7 @@ module.exports = app => {
             const helper = this.ctx.helper;
             // 简单验证数据
             if (!stage) throw '成本报审不存在';
-            const decimal = stage.decimal || this.decimal;
+            const decimal = stage.decimal;
             if (!data) throw '提交数据错误';
             const datas = data instanceof Array ? data : [data];
             const ids = datas.map(x => { return x.id; });
@@ -445,7 +443,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, this.taxDecimal) : oData.tax;
+                    nData.tax = row.tax !== undefined ? helper.round(row.tax || 0, decimal.tax) : oData.tax;
                     const divNum = helper.add(1, helper.div(nData.tax, 100));
                     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);
@@ -493,7 +491,7 @@ module.exports = app => {
             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 ? this.ctx.costStage.decimal : this.decimal;
+            const orgDecimal = this.ctx.costStage.decimal;
 
             const calcTp = decimal.tp < orgDecimal.tp;
             this.ctx.costStage.decimal = { up: decimal.up, tp: decimal.tp, qty: decimal.qty };

+ 6 - 6
app/view/cost/analysis_list.ejs

@@ -34,7 +34,7 @@
                     <% for (const s of stagelist) { %>
                     <tr>
                         <td>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank">第 <%- s.stage_order %> 期</a>
+                            <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>
@@ -61,11 +61,11 @@
                         </td>
                         <td class="text-center">
                             <% if (s.audit_status === auditConst.status.uncheck && s.create_user_id === ctx.session.sessionUser.accountId) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
-                            <% } else if (s.status === auditConst.status.checkNo && s.user_id === ctx.session.sessionUser.accountId) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
-                            <% } else if ((s.status === auditConst.status.checking || s.status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if (s.audit_status === auditConst.status.checkNo && s.create_user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if ((s.audit_status === auditConst.status.checking || s.audit_status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.audit_id === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
                             <% } else { %>
                             <span class="<%- auditConst.info[s.audit_status].class %>"><%- auditConst.info[s.audit_status].title %></span>
                             <% } %>

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

@@ -1,2 +1,2 @@
 <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 %>" 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>

+ 75 - 0
app/view/cost/book.ejs

@@ -0,0 +1,75 @@
+<% 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 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="fujian" class="tab-pane tab-select-show">
+                    </div>
+                    <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>
+                <li class="nav-item">
+                    <a class="nav-link" content="#fujian" 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 %>');
+</script>

+ 11 - 11
app/view/cost/book_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.book_add && (stagelist.length === 0 || stagelist[0].audit_status === auditConst.status.checked)) { %>
+            <% if (ctx.permission.cost.book_add && (stages.length === 0 || stages[0].audit_status === auditConst.status.checked)) { %>
             <div class="ml-auto">
                 <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">新建报审</a>
             </div>
@@ -29,10 +29,10 @@
                     </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 %>/book/<%- s.stage_order %>" target="_blank">第 <%- s.stage_order %> 期</a>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>/stage" target="_blank">第 <%- s.stage_order %> 期</a>
                         </td>
                         <td class="text-center"><%- s.stage_date %></td>
                         <td class="text-center"><%- s.user_name %></td>
@@ -54,11 +54,11 @@
                         </td>
                         <td class="text-center">
                             <% if (s.audit_status === auditConst.status.uncheck && s.create_user_id === ctx.session.sessionUser.accountId) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
-                            <% } else if (s.status === auditConst.status.checkNo && s.user_id === ctx.session.sessionUser.accountId) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
-                            <% } else if ((s.status === auditConst.status.checking || s.status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if (s.audit_status === auditConst.status.checkNo && s.create_user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if ((s.audit_status === auditConst.status.checking || s.audit_status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.audit_id === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
                             <% } else { %>
                             <span class="<%- auditConst.info[s.audit_status].class %>"><%- auditConst.info[s.audit_status].title %></span>
                             <% } %>
@@ -72,7 +72,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/book_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 validLedgerStages) { %>
+                        <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>

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

@@ -1,2 +1,2 @@
 <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 %>" 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>

+ 4 - 0
app/view/cost/book_modal.ejs

@@ -0,0 +1,4 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% include ./audit_modal.ejs %>
+<% include ../shares/upload_att.ejs %>
+<% include ../shares/new_tag_modal.ejs %>

+ 14 - 14
app/view/cost/ledger_list.ejs

@@ -41,17 +41,17 @@
                     <% for (const s of stages) { %>
                     <tr>
                         <td>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank">第 <%- s.stage_order %> 期</a>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- 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-right"><%- s.stage_tp.pay_tp %></td>
+                        <td class="text-right"><%- s.stage_tp.cut_tp %></td>
+                        <td class="text-right"><%- s.stage_tp.yf_tp %></td>
+                        <td class="text-right"><%- s.stage_tp.sf_tp %></td>
+                        <td class="text-right"><%- s.stage_end_tp.pay_tp %></td>
+                        <td class="text-right"><%- s.stage_end_tp.cut_tp %></td>
+                        <td class="text-right"><%- s.stage_end_tp.yf_tp %></td>
+                        <td class="text-right"><%- s.stage_end_tp.sf_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>
@@ -68,11 +68,11 @@
                         </td>
                         <td class="text-center">
                             <% if (s.audit_status === auditConst.status.uncheck && s.create_user_id === ctx.session.sessionUser.accountId) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
-                            <% } else if (s.status === auditConst.status.checkNo && s.user_id === ctx.session.sessionUser.accountId) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
-                            <% } else if ((s.status === auditConst.status.checking || s.status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
-                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if (s.audit_status === auditConst.status.checkNo && s.create_user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if ((s.audit_status === auditConst.status.checking || s.audit_status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.audit_id === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>/stage" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
                             <% } else { %>
                             <span class="<%- auditConst.info[s.audit_status].class %>"><%- auditConst.info[s.audit_status].title %></span>
                             <% } %>

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

@@ -1,3 +1,3 @@
 <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 %>" 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 %>/ledger/<%- ctx.costStage.stage_order %>/stage" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>
 <% include ./audit_btn.ejs %>

+ 27 - 0
config/web.js

@@ -2579,6 +2579,33 @@ const JsFiles = {
                     '/public/js/cost_stage_ledger.js',
                 ],
                 mergeFile: 'cost_stage_ledger',
+            },
+            cost_stage_book: {
+                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',
+                ],
+                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_book.js',
+                ],
+                mergeFile: 'cost_stage_book',
             }
         },
     },

+ 25 - 1
sql/update.sql

@@ -50,6 +50,7 @@ CREATE TABLE `zh_cost_stage`  (
   `tid` int(11) UNSIGNED NOT NULL COMMENT '标段id(zh_tender.id)',
   `stage_type` varchar(20) NULL COMMENT '期类型(台账ledger/账面book/分析analysis)',
   `stage_order` int(11) UNSIGNED NOT NULL COMMENT '期序号',
+  `rela_stage` json NULL COMMENT '关联期信息(json)--[{sid: varchar, sorder: int}]',
   `create_user_id` int(11) NOT NULL COMMENT '创建人id',
   `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_user_id` int(11) UNSIGNED NOT NULL COMMENT '最后修改人id',
@@ -216,11 +217,14 @@ 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)',
-  `cost_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(不同期保持统一zh_cost_stage_ledger.cost_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',
   `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 '入账金额',
+  `in_excl_tax_tp` decimal(24, 8) NOT NULL COMMENT '入账金额不含税',
   `postil` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
   `memo` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
   `add_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人',
@@ -228,6 +232,26 @@ CREATE TABLE `zh_cost_stage_book` (
   `update_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后编辑人',
   `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
   `read_in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期入账-只读',
+  `read_in_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '本期入账不含税-只读',
+  `calc_his` json DEFAULT NULL COMMENT '本期历史',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+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',
+  `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 '本期批注',
+  `memo` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
+  `add_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人',
+  `add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后编辑人',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
+  `read_in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期入账-只读',
+  `read_in_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '本期入账不含税-只读',
   `calc_his` json DEFAULT NULL COMMENT '本期历史',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;