Pārlūkot izejas kodu

责任成本相关

MaiXinRong 13 stundas atpakaļ
vecāks
revīzija
5186d443c8

+ 1 - 0
app/const/audit.js

@@ -1656,6 +1656,7 @@ const pushType = {
     costStageLedger: 16,
     costStageBook: 17,
     costStageAnalysis: 18,
+    costStageDuty: 19,
 };
 
 module.exports = {

+ 1 - 1
app/const/shenpi.js

@@ -60,10 +60,10 @@ const sp_lc = [
     { code: 'contract', type: sp_type.contract, name: '标段合同审批' },
 ];
 const cost_sp_lc = [
+    { code: 'cost_stage_duty', type: cost_sp_type.cost_duty, name: '责任成本' },
     { code: 'cost_stage_ledger', type: cost_sp_type.cost_stage_ledger, name: '成本报审' },
     { code: 'cost_stage_book', type: cost_sp_type.cost_stage_book, name: '财务账面' },
     { code: 'cost_stage_analysis', type: cost_sp_type.cost_stage_analysis, name: '成本分析' },
-    { code: 'cost_duty', type: cost_sp_type.cost_duty, name: '责任成本' },
 ];
 
 const sp_status = {

+ 1 - 1
app/const/tender_info.js

@@ -200,7 +200,7 @@ const defaultInfo = {
         cost_stage_ledger: 1,
         cost_stage_book: 1,
         cost_stage_analysis: 1,
-        cost_duty: 1,
+        cost_stage_duty: 1,
         contract: 1,
     },
     ledger_check: {

+ 105 - 7
app/controller/cost_controller.js

@@ -87,6 +87,10 @@ module.exports = app => {
             }
         }
 
+        async checkDutyRela(ctx) {
+            ctx.tender.costDuty = await ctx.service.costStage.getStageByOrder(ctx.tender.id, 'duty', 1);
+        }
+
         async getTypeStages(ctx, stage_type, sort = 'DESC') {
             const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, sort);
             for (const s of stages) {
@@ -107,6 +111,7 @@ module.exports = app => {
         }
         async ledger(ctx) {
             try {
+                await this.checkDutyRela(ctx);
                 const stage_type = this.ctx.service.costStage.stageType.ledger.key;
                 const stages = await this.getTypeStages(ctx, stage_type);
                 const colSet = this.ctx.service.subProject.getColSet('cost_' + stage_type);
@@ -126,6 +131,7 @@ module.exports = app => {
         }
         async book(ctx) {
             try {
+                await this.checkDutyRela(ctx);
                 const stage_type = this.ctx.service.costStage.stageType.book.key;
                 const stages = await this.getTypeStages(ctx, stage_type);
                 for (const s of stages) {
@@ -152,6 +158,7 @@ module.exports = app => {
         }
         async analysis(ctx) {
             try {
+                await this.checkDutyRela(ctx);
                 const stage_type = this.ctx.service.costStage.stageType.analysis.key;
                 const stages = await this.getTypeStages(ctx, stage_type);
                 // todo 兼容以ledger做关联的情况,届时只需在标段或者项目上设置这个类型即可。
@@ -180,14 +187,17 @@ module.exports = app => {
 
         async addStage(ctx) {
             const stage_type = ctx.request.body.stage_type;
+            const hintType  = stage_type === 'duty' ? '数据' : '期'
             try {
-                if (!ctx.permission.cost[stage_type + '_add']) throw '您无权创建';
+                if (!ctx.permission.cost[stage_type + '_add']) throw '您无权创建' + hintType;
                 const stage_date = ctx.request.body.date;
                 if (stage_type === 'ledger' && !stage_date) throw '请选择年月';
 
-                const stages = await ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
-                const unCompleteCount = stages.filter(s => { return s.status !== audit.common.status.checked; }).length;
-                if (unCompleteCount.length > 0) throw '最新一期未审批通过,请审批通过后再新增';
+                if (stage_type !== 'duty') {
+                    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 '最新一期未审批通过,请审批通过后再新增';
+                }
 
                 let newStage;
                 if (stage_type === 'ledger') {
@@ -207,14 +217,21 @@ module.exports = app => {
                     if (!relaStage) throw '选择的关联期不存在';
                     newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, relaStage.stage_date, { sid: relaStage.id, sorder: relaStage.stage_order });
                     await this.ctx.service.costStageAnalysis.reCalcProfit(newStage.id);
+                } else if (stage_type === 'duty') {
+                    newStage = await ctx.service.costStage.getStageByOrder(ctx.tender.id, stage_type, 1);
+                    if (!newStage) newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, '');
                 }
 
-                if (!newStage) throw '新增期失败';
+                if (!newStage) throw `新增${hintType}失败`;
                 ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}/${newStage.stage_order}/stage`);
             } catch (err) {
                 ctx.log(err);
-                ctx.postError(err, '新增期失败');
-                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}`);
+                ctx.postError(err, `新增${hintType}失败`);
+                if (stage_type === 'duty') {
+                    ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/ledger`)
+                } else {
+                    ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}`);
+                }
             }
         }
         async delStage(ctx) {
@@ -252,6 +269,7 @@ module.exports = app => {
         async stage(ctx) {
             const stageTypeInfo = ctx.service.costStage.stageType[ctx.costStage.stage_type];
             try {
+                await this.checkDutyRela(ctx);
                 await this._getStageAuditViewData(ctx);
                 // 流程审批人相关数据
                 const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
@@ -483,6 +501,33 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
         }
+        async _dutyLoad(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const filter = data.filter.split(';');
+                const responseData = { err: 0, msg: '', data: {}, hpack: [] };
+                for (const f of filter) {
+                    switch (f) {
+                        case 'bills':
+                            responseData.data.bills = ctx.costStage.readOnly && !ctx.costStage.canCheck
+                                ? await ctx.service.costStageDuty.getReadData(ctx.costStage)
+                                : await ctx.service.costStageDuty.getEditData(ctx.costStage);
+                            break;
+                        case 'extra':
+                            responseData.data.extra = ctx.costStage.extra_info;
+                            break;
+                        default:
+                            responseData.data[f] = [];
+                            break;
+                    }
+                }
+
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
         async stageLoad(ctx) {
             const updateFun = `_${ctx.costStage.stage_type}Load`;
             if (this[updateFun]) {
@@ -492,6 +537,58 @@ module.exports = app => {
             }
         }
 
+        async _dutyBillsBase(stage, type, data) {
+            if (isNaN(data.id) || data.id <= 0) throw '数据错误';
+            if (type !== 'add') {
+                if (isNaN(data.count) || data.count <= 0) data.count = 1;
+            }
+            switch (type) {
+                case 'add':
+                    return await this.ctx.service.costStageDuty.addDutyNode(stage, data.id, data.count);
+                case 'delete':
+                    return await this.ctx.service.costStageDuty.delete(stage.id, data.id, data.count);
+                case 'up-move':
+                    return await this.ctx.service.costStageDuty.upMoveNode(stage.id, data.id, data.count);
+                case 'down-move':
+                    return await this.ctx.service.costStageDuty.downMoveNode(stage.id, data.id, data.count);
+                case 'up-level':
+                    return await this.ctx.service.costStageDuty.upLevelNode(stage.id, data.id, data.count);
+                case 'down-level':
+                    return await this.ctx.service.costStageDuty.downLevelNode(stage.id, data.id, data.count);
+            }
+        }
+        async _dutyUpdate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.target) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
+
+                if (data.target === 'ledger') {
+                    if (!data.postType || !data.postData) throw '数据错误';
+                    switch (data.postType) {
+                        case 'add':
+                        case 'delete':
+                        case 'up-move':
+                        case 'down-move':
+                        case 'up-level':
+                        case 'down-level':
+                            responseData.data = await this._dutyBillsBase(ctx.costStage, data.postType, data.postData);
+                            break;
+                        case 'update':
+                            responseData.data = await this.ctx.service.costStageDuty.updateCalc(ctx.costStage, data.postData);
+                            break;
+                        default:
+                            throw '未知操作';
+                    }
+                } else if (data.target === 'extra') {
+                    responseData.data = await this.ctx.service.costStage.saveExtraInfo(ctx.costStage, data.data);
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
         async _billsBase(stage, type, data) {
             if (isNaN(data.id) || data.id <= 0) throw '数据错误';
             if (type !== 'add') {
@@ -893,6 +990,7 @@ module.exports = app => {
         async compare(ctx) {
             const stageTypeInfo = ctx.service.costStage.stageType[ctx.costStage.stage_type];
             try {
+                await this.checkDutyRela(ctx);
                 await this._getStageAuditViewData(ctx);
                 // 流程审批人相关数据
                 const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);

+ 1 - 0
app/controller/tender_controller.js

@@ -1003,6 +1003,7 @@ module.exports = app => {
         }
         async shenpiSet(ctx) {
             try {
+                ctx.tender.costDuty = await this.ctx.service.costStage.getStageByOrder(ctx.tender.id, 'duty', 1);
                 const specShenpiType = this._getSpecShenpiUrl(ctx);
                 // 获取所有项目参与者
                 const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);

+ 549 - 0
app/public/js/cost_stage_duty.js

@@ -0,0 +1,549 @@
+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: [
+                    'total_price',
+                ],
+                keys: ['id', 'stage_id', 'tree_id'],
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.spreadSetting = {
+                cols: [
+                    {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 180, formatter: '@', cellType: 'tree'},
+                    {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 120, formatter: '@'},
+                    {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@'},
+                    {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit', comboEdit: true},
+                    {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '数量', colSpan: '1', rowSpan: '2', field: 'quantity', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '金额', colSpan: '1', rowSpan: '2', field: 'total_price', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '图册号', colSpan: '1', rowSpan: '2', field: 'drawing_code', hAlign: 0, width: 100, formatter: '@'},
+                    {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 100, formatter: '@'},
+                ],
+                emptyRows: 0,
+                headRows: 2,
+                headRowHeight: [25, 25],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: readOnly,
+                localCache: {
+                    key: 'cost_duty',
+                    colWidth: true,
+                },
+                headColWidth: [50],
+                tipsSum: true,
+            };
+            sjsSettingObj.setFxTreeStyle(this.spreadSetting, sjsSettingObj.FxTreeStyle.jz);
+
+            this.ckBillsSpread = window.location.pathname + '-billsSelect';
+
+            this.initSpread();
+            this.initOtherEvent();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            this.spread.bind(spreadNS.Events.topRowChanged, this.topRowChanged);
+            this.spread.bind(spreadNS.Events.ClipboardChanging, function (e, info) {
+                const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
+                SpreadJsObj.Clipboard.setCopyData(copyText);
+            });
+            if (readOnly) return;
+            this.spread.bind(spreadNS.Events.EditEnded, this.editEnded);
+            this.spread.bind(spreadNS.Events.EditStarting, this.editStarting);
+            this.spread.bind(spreadNS.Events.ClipboardPasting, this.clipboardPasting);
+            SpreadJsObj.addDeleteBind(this.spread, this.deletePress);
+        }
+        initOtherEvent() {
+            const self = this;
+            // 增删上下移升降级
+            $('a[name="base-opr"]').click(function () {
+                self.baseOpr(this.getAttribute('type'));
+            });
+            $('body').on('change', 'input[name=in_tp]', function() {
+                const data = {
+                    in_tp: parseFloat($('input[name=in_tp]').val()),
+                };
+                postData('update', { target: 'extra', data} , function(result) {
+                    billsObj.setExtra(result);
+                });
+            });
+        }
+        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;
+            if (sel.rowCount > 1 && first) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = tree.nodes[sel.row + r];
+                    if (!rNode) {
+                        sameParent = false;
+                        break;
+                    }
+                    if (rNode.tree_level > first.tree_level) continue;
+                    if ((rNode.tree_level < first.tree_level) || (rNode.tree_level === first.tree_level && rNode.tree_pid !== first.tree_pid)) {
+                        sameParent = false;
+                        break;
+                    }
+                    last = rNode;
+                }
+            }
+            const preNode = tree.getPreSiblingNode(first);
+            const valid = !this.sheet.zh_setting.readOnly;
+
+            setObjEnable($('a[name=base-opr][type=add]'), valid && first && first.tree_level > 1);
+            setObjEnable($('a[name=base-opr][type=delete]'), valid && first && first.tree_level > 1 && sameParent);
+            setObjEnable($('a[name=base-opr][type=up-move]'), valid && first && first.tree_level > 1 && sameParent && preNode);
+            setObjEnable($('a[name=base-opr][type=down-move]'), valid && first && first.tree_level > 1 && sameParent  && !tree.isLastSibling(last));
+            setObjEnable($('a[name=base-opr][type=up-level]'), valid && first && first.tree_level > 2 && sameParent && tree.getParent(first) && first.tree_level > 1);
+            setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && first.tree_level > 1 && sameParent && preNode);
+        }
+        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];
+        }
+        baseOpr(type, addCount = 1) {
+            const self = this;
+            const sheet = self.sheet;
+            const sel = sheet.getSelections()[0];
+            const [tree, node, count] = this.getDefaultSelectInfo();
+            if (!tree || !node || !count) return;
+
+            const updateData = {
+                target: 'ledger', postType: type,
+                postData: {
+                    id: node.tree_id,
+                    count: type === 'add' ? addCount : count,
+                }
+            };
+            if (type === 'delete') {
+                deleteAfterHint(function () {
+                    postData('update', updateData, function (result) {
+                        const refreshData = tree.loadPostData(result);
+                        self.refreshTree(refreshData);
+                        if (sel) {
+                            sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
+                        }
+                        self.refreshOperationValid();
+                    });
+                });
+            } else {
+                postData('update', updateData, function (result) {
+                    const refreshData = tree.loadPostData(result);
+                    self.refreshTree(refreshData);
+                    if (['up-move', 'down-move'].indexOf(type) > -1) {
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
+                        }
+                    } else if (type === 'add') {
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
+                        }
+                    }
+                    self.refreshOperationValid();
+                });
+            }
+        }
+        // 事件
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    billsObj.refreshOperationValid();
+                }
+            }
+        }
+        topRowChanged(e, info) {
+            SpreadJsObj.saveTopAndSelect(info.sheet, billsObj.ckBillsSpread);
+        }
+        editStarting(e, info) {
+            if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+            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 'quantity':
+                case 'unit_price':
+                    info.cancel = (node.children && node.children.length > 0);
+                    break;
+            }
+        }
+        editEnded(e, info) {
+            if (!info.sheet.zh_setting) return;
+
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            const data = { id: node.id, stage_id: node.stage_id, tree_id: node.tree_id };
+            // 未改变值则不提交
+            const col = info.sheet.zh_setting.cols[info.col];
+            const orgValue = node[col.field];
+            const newValue = trimInvalidChar(info.editingText);
+            if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) return;
+
+            if (info.editingText) {
+                const text = newValue;
+                if (col.type === 'Number') {
+                    const num = _.toNumber(text);
+                    if (_.isFinite(num)) {
+                        data[col.field] = num;
+                    } else {
+                        try {
+                            data[col.field] = ZhCalc.mathCalcExpr(transExpr(text));
+                        } catch(err) {
+                            toastr.error('输入的表达式非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                    }
+                } else {
+                    data[col.field] = text;
+                }
+            } else {
+                data[col.field] = col.type === 'Number' ? 0 : '';
+            }
+            // 更新至服务器
+            postData('update', {target: 'ledger', postType: 'update', postData: data}, function (result) {
+                const refreshNode = billsObj.tree.loadPostData(result);
+                billsObj.refreshTree(refreshNode);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            });
+        }
+        deletePress (sheet) {
+            if (!sheet.zh_setting) return;
+            const sel = sheet.getSelections()[0], datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                let bDel = false;
+                const node = sheet.zh_tree.nodes[iRow];
+                const data = sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    const col = sheet.zh_setting.cols[iCol];
+                    const style = sheet.getStyle(iRow, iCol);
+                    if (style.locked) continue;
+
+                    data[col.field] = col.type === 'Number' ? 0 : '';
+                    bDel = true;
+                }
+                if (bDel) datas.push(data);
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(sheet, sel.row, sel.rowCount);
+                });
+            }
+        }
+        clipboardPasting(e, info) {
+            info.cancel = true;
+            const tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
+            if (!setting || !tree) return;
+
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            const datas = [], filterNodes = [];
+
+            let level, filterRow = 0;
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = tree.nodes[curRow];
+                if (!node) continue;
+
+                if (!level) level = node.level;
+                if (node.level < level) break;
+
+                let bPaste = false;
+                const data = info.sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    if (datas.length > 0) if (datas[0][colSetting.field] === undefined) continue;
+
+                    const newText = trimInvalidChar(pasteData[iRow-filterRow][iCol]);
+                    if (colSetting.type === 'Number') {
+                        const num = _.toNumber(newText);
+                        if (!_.isFinite(num)) {
+                            try {
+                                data[colSetting.field] = ZhCalc.mathCalcExpr(transExpr(newText));
+                            } catch (err) {
+                                toastr.error('粘贴的计算式非法');
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                                return;
+                            }
+                        } else {
+                            data[colSetting.field] = num;
+                        }
+                    } else {
+                        data[colSetting.field] = newText;
+                    }
+
+                    bPaste = true;
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
+                }
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) refreshNode.update = refreshNode.update.concat(filterNodes);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        }
+        // 附加信息
+        getSum() {
+            return ZhCalc.sum(this.tree.children.map(x => { return x.total_price; }));
+        }
+        refreshExtraTable() {
+            const html = [];
+            const in_tp = this.extra.in_tp || 0;
+            const out_tp = this.getSum();
+            const profit = ZhCalc.sub(in_tp, out_tp);
+            const profit_rate = in_tp ? ZhCalc.mul(ZhCalc.div(profit, in_tp), 100, 2) : 0;
+            html.push(`<tr><td>项目收入</td><td><input type="number" class="form-control form-control-sm nospin" name="in_tp" value="${in_tp}" ${readOnly ? 'readOnly' : ''}></td></tr>`);
+            html.push(`<tr><td>项目支出</td><td>${out_tp}</td></tr>`);
+            html.push(`<tr><td>利润</td><td>${profit}</td></tr>`);
+            html.push(`<tr><td>利润率</td><td>${profit_rate}</td></tr>`);
+            $('#duty-extra').html(html.join(''));
+        }
+        setExtra(extra) {
+            this.extra = extra;
+            this.refreshExtraTable();
+        }
+    }
+    const billsObj = new BillsObj();
+
+    const searchObj = $.billsSearch({
+        selector: '#search',
+        searchRangeStr: '项目节编号/清单编号/名称',
+        searchSpread: billsObj.spread,
+        keyId: 'tree_id',
+        resultSpreadSetting: {
+            cols: [
+                {title: '项目节编号', field: 'code', hAlign: 0, width: 120, formatter: '@'},
+                {title: '清单编号', field: 'b_code', hAlign: 0, width: 80, 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,
+        },
+    });
+    // 加载数据
+    postData('load', { filter: 'bills;extra' }, function(result) {
+        billsObj.loadData(result.bills);
+        billsObj.setExtra(result.extra);
+    });
+
+    // 展开收起标准清单
+    $('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();
+    });
+
+    // 工具栏spr
+    $.divResizer({
+        select: '#right-spr',
+        callback: function () {
+            billsObj.spread.refresh();
+            searchObj.spread.refresh();
+        }
+    });
+    // 导航Menu
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+            billsObj.spread.refresh();
+        }
+    });
+    // 显示层次
+    (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);
+});

+ 19 - 6
app/service/cost_stage.js

@@ -25,13 +25,14 @@ module.exports = app => {
             super(ctx);
             this.tableName = 'cost_stage';
             this.stageType = {
+                duty: { key: 'duty', name: '责任成本', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_duty', push_type: 'costStageDuty', dataService: 'costStageDuty' },
                 ledger: { key: 'ledger', name: '成本报审', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_ledger', push_type: 'costStageLedger', dataService: 'costStageLedger', detailService: 'costStageDetail' },
                 book: { key: 'book', name: '财务账面', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_book', push_type: 'costStageBook', dataService: 'costStageBook', detailService: 'costStageBookDetail' },
                 analysis: { key: 'analysis', name: '成本分析', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_analysis', push_type: 'costStageAnalysis', dataService: 'costStageAnalysis', detailService: 'costStageAnalysisDetail' },
             };
         }
 
-        _analysisstage(stage) {
+        _analysisStage(stage) {
             if (!stage) return;
             const stages = stage instanceof Array ? stage : [stage];
             if (stage.length === 0) return;
@@ -51,6 +52,7 @@ module.exports = app => {
                 }
                 s.typeInfo = this.stageType[s.stage_type];
                 s.rela_stage = s.rela_stage ? JSON.parse(s.rela_stage) : null;
+                s.extra_info = s.extra_info ? JSON.parse(s.extra_info) : {};
             });
         }
         async calcRealtime(stage) {
@@ -71,24 +73,24 @@ module.exports = app => {
             const sql = `SELECT cls.*, pa.name AS user_name FROM ${this.tableName} cls LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON cls.create_user_id = pa.id` +
                         `  WHERE cls.tid = ? and cls.stage_type = ? ORDER BY cls.stage_order ${sort}`;
             const result = await this.db.query(sql, [tid, stage_type]);
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
         async getAllCheckedStages(tid, stage_type, sort = 'ASC') {
             const sql = `SELECT cls.*, pa.name AS user_name FROM ${this.tableName} cls LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON cls.create_user_id = pa.id` +
                 `  WHERE cls.tid = ? AND cls.stage_type = ? AND audit_status = ? ORDER BY cls.stage_order ${sort}`;
             const result = await this.db.query(sql, [tid, stage_type, audit.status.checked]);
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
         async getStage(id) {
             const result = await this.getDataById(id);
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
         async getStageByOrder(tid, stage_type, stage_order) {
             const result = await this.getDataByCondition({ tid, stage_type, stage_order });
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
 
@@ -294,6 +296,7 @@ module.exports = app => {
             const info = this.ctx.tender.info;
             const typeInfo = this.stageType[stage.stage_type];
             const shenpi_status = info.shenpi[typeInfo.shenpi_status];
+            console.log(typeInfo.shenpi_status, info.shenpi);
             if ((stage.audit_status === status.uncheck || stage.audit_status === status.checkNo) && shenpi_status !== shenpiConst.sp_status.sqspr) {
                 // 进一步比较审批流是否与审批流程设置的相同,不同则替换为固定审批流或固定的终审
                 const auditList = await this.ctx.service.costStageAudit.getAllDataByCondition({ where: { stage_id: stage.id, audit_times: stage.audit_times }, orders: [['audit_order', 'asc']] });
@@ -336,7 +339,7 @@ module.exports = app => {
                 orders: [['stage_order', 'desc']],
             });
             if (lastCheckedStage && lastCheckedStage.length > 0) {
-                this._analysisstage(lastCheckedStage[0]);
+                this._analysisStage(lastCheckedStage[0]);
                 return lastCheckedStage[0];
             }
             return null;
@@ -346,6 +349,16 @@ module.exports = app => {
             const num = await this.db.queryOne(`SELECT COUNT(*) as num From ${this.tableName} WHERE tid = ? and stage_type = ? and audit_status = ?`, [tenderId, stage_type, audit.status.checked]);
             return num ? num.num : 0;
         }
+
+        async saveExtraInfo(stage, data) {
+            const extra_info = this.ctx.helper._.assign(stage.extra_info, data);
+            try {
+                await this.defaultUpdate({ id: stage.id, extra_info: JSON.stringify(extra_info) });
+                return extra_info;
+            } catch (err) {
+                throw '保存附加信息错误';
+            }
+        }
     }
 
     return CostStage;

+ 403 - 0
app/service/cost_stage_duty.js

@@ -0,0 +1,403 @@
+'use strict';
+
+/**
+ *
+ * 支付审批-安全生产
+ * @author Mai
+ * @date
+ * @version
+ */
+const billsUtils = require('../lib/bills_utils');
+const costFields = {
+    textFields: ['code', 'b_code', 'name', 'unit', 'memo', 'drawing_code'],
+    curFields: ['quantity', 'unit_price', 'total_price'],
+    readFields: ['read_quantity', 'read_unit_price', 'read_total_price'],
+    treeFields: ['tree_id', 'tree_pid', 'tree_level', 'tree_order', 'tree_full_path', 'tree_is_leaf'],
+    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id',],
+};
+costFields.calcFields = [...costFields.curFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.curFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.readFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.curFields, 'calc_his'];
+
+const auditConst = require('../const/audit').costStage;
+
+module.exports = app => {
+
+    class CostStageLedger extends app.BaseTreeService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, {
+                mid: 'stage_id',
+                kid: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                keyPre: 'cost_duty_maxTreeId:',
+                uuid: true,
+            });
+            this.tableName = 'cost_stage_duty';
+            this.calcFields = costFields.calcFields;
+        }
+
+        // 继承方法
+        clearParentingData(data) {
+            for (const f of costFields.calcFields) {
+                data[f] = 0;
+            }
+        }
+
+        _getDefaultData(data, stage) {
+            data.id = this.uuid.v4();
+            data.cost_id = data.id;
+            data.tender_id = stage.tid;
+            data.stage_id = stage.id;
+            data.add_user_id = this.ctx.session.sessionUser.accountId;
+            data.update_user_id = data.add_user_id;
+        }
+
+        async getEditData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.editQueryFields
+            });
+            return result;
+        }
+        async getReadData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.readQueryFields
+            });
+            result.forEach(x => {
+                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 init(stage, transaction) {
+            if (!stage || !transaction) throw '成本报审数据错误';
+
+            if (!this.ctx.tender.data.measure_type) throw '未设置台账模板,请联系管理员设置';
+            const templateId = await this.ctx.service.valuation.getValuationTemplate(
+                this.ctx.tender.data.valuation, this.ctx.tender.data.measure_type);
+            const tempData = await this.ctx.service.tenderNodeTemplate.getData(templateId, true);
+            if (tempData.length === 0) throw '台账模板无数据,连请联系管理员修改';
+
+            const insertData = [];
+            for (const b of tempData) {
+                const bills = {
+                    tree_id: b.template_id, tree_pid: b.pid, tree_order: b.order, tree_level: b.level, tree_full_path: b.full_path, tree_is_leaf: b.is_leaf,
+                    code: b.code || '', b_code: '', name: b.name || '', unit: b.unit || '',
+                };
+                this._getDefaultData(bills, stage);
+                insertData.push(bills);
+            }
+
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === insertData.length;
+        }
+        async initStageData(transaction, stage, preStage) {
+            await this.init(stage, transaction);
+        }
+
+        /**
+         * 新增数据(供内部或其他service类调用, controller不可直接使用)
+         * @param {Array|Object} data - 新增数据
+         * @param {Number} stageId - 期id
+         * @param {Object} transaction - 新增事务
+         * @return {Promise<boolean>} - {Promise<是否正确新增成功>}
+         */
+        async innerAdd(data, stageId, transaction) {
+            const datas = data instanceof Array ? data : [data];
+            if (!stageId) {
+                throw '期id错误';
+            }
+            if (datas.length <= 0) {
+                throw '插入数据为空';
+            }
+            if (!transaction) {
+                throw '内部错误';
+            }
+            // 整理数据
+            const insertData = [];
+            for (const tmp of datas) {
+                tmp[this.setting.id] = tmp.template_id;
+                tmp[this.setting.pid] = tmp.pid;
+                tmp[this.setting.mid] = stageId;
+                delete tmp.template_id;
+                delete tmp.pid;
+                tmp.id = this.uuid.v4();
+                insertData.push(tmp);
+            }
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === datas.length;
+        }
+        /**
+         * 新增数据
+         *
+         * @param {Object} data - 新增的数据(可批量)
+         * @param {Number} stageId - 安全计量期id
+         * @return {Boolean} - 返回新增的结果
+         */
+        async add(data, stageId) {
+            this.transaction = await this.db.beginTransaction();
+            let result = false;
+            try {
+                result = await this.innerAdd(data, stageId, this.transaction);
+                if (!result) {
+                    throw '新增数据错误';
+                }
+                await this.transaction.commit();
+            } catch (error) {
+                await this.transaction.rollback();
+                result = false;
+            }
+
+            return result;
+        }
+
+        /**
+         * 根据节点Id获取数据
+         *
+         * @param {Number} stageId - 安全计量期id
+         * @param {Number} nodeId - 项目节/工程量清单节点id
+         * @return {Object} - 返回查询到的节点数据
+         */
+        async getDataByNodeId(stageId, nodeId) {
+            if ((nodeId <= 0) || (stageId <= 0)) {
+                return undefined;
+            }
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodeId;
+            const data = await this.db.getDataByCondition(where);
+
+            return data;
+        }
+        /**
+         * 根据节点Id获取数据
+         * @param {Number} stageId - 期Id
+         * @param {Array} nodesIds - 节点Id
+         * @return {Array}
+         */
+        async getDataByNodeIds(stageId, nodesIds) {
+            if (stageId <= 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodesIds;
+            const data = await this.db.getAllDataByCondition({ where });
+
+            return this._.sortBy(data, function(d) {
+                return nodesIds.indexOf(d[this.setting.id]);
+            });
+        }
+        /**
+         * 根据主键id获取数据
+         * @param {Array|Number} id - 主键id
+         * @return {Promise<*>}
+         */
+        async getDataByIds(id) {
+            if (!id) return [];
+            const ids = id instanceof Array ? id : [id];
+            if (ids.length === 0) return [];
+
+            const data = await this.db.getAllDataByCondition({ where: { id: ids } });
+            return data;
+        }
+
+        /**
+         * 根据 父节点id 获取子节点
+         * @param stageId
+         * @param nodeId
+         * @return {Promise<*>}
+         */
+        async getChildrenByParentId(stageId, nodeId) {
+            if (stageId <= 0 || !nodeId) {
+                return undefined;
+            }
+            const nodeIds = nodeId instanceof Array ? nodeId : [nodeId];
+            if (nodeIds.length === 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.pid] = nodeIds;
+            const data = await this.getAllDataByCondition({ where, orders: [[this.setting.order, 'ASC']] });
+
+            return data;
+        }
+
+        async addDutyNode(stage, targetId, count) {
+            if (!stage) return null;
+
+            const select = targetId ? await this.getDataByKid(stage.id, targetId) : null;
+            if (targetId && !select) throw '新增节点数据错误';
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                if (select) await this._updateChildrenOrder(stage.id, select[this.setting.pid], select[this.setting.order] + 1, count);
+                const newDatas = [];
+                const maxId = await this._getMaxLid(stage.id);
+                for (let i = 1; i < count + 1; i++) {
+                    const newData = {};
+                    newData[this.setting.kid] = maxId + i;
+                    newData[this.setting.pid] = select ? select[this.setting.pid] : this.rootId;
+                    newData[this.setting.mid] = stage.id;
+                    newData[this.setting.level] = select ? select[this.setting.level] : 1;
+                    newData[this.setting.order] = select ? select[this.setting.order] + i : i;
+                    newData[this.setting.fullPath] = newData[this.setting.level] > 1
+                        ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
+                        : newData[this.setting.kid] + '';
+                    newData[this.setting.isLeaf] = true;
+                    this._getDefaultData(newData, stage);
+                    newDatas.push(newData);
+                }
+                const insertResult = await this.transaction.insert(this.tableName, newDatas);
+                this._cacheMaxLid(stage.id, maxId + count);
+
+                if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+                await this.transaction.commit();
+                this.transaction = null;
+            } catch (err) {
+                await this.transaction.rollback();
+                this.transaction = null;
+                throw err;
+            }
+
+            if (select) {
+                const createData = await this.getChildBetween(stage.id, select[this.setting.pid], select[this.setting.order], select[this.setting.order] + count + 1);
+                const updateData = await this.getNextsData(stage.id, select[this.setting.pid], select[this.setting.order] + count);
+                return {create: createData, update: updateData};
+            } else {
+                const createData = await this.getChildBetween(stage.id, -1, 0, count + 1);
+                return {create: createData};
+            }
+        }
+
+        async updateCalc(stage, data) {
+            const helper = this.ctx.helper;
+            // 简单验证数据
+            if (!stage) throw '成本报审不存在';
+            const decimal = stage.decimal;
+            if (!data) throw '提交数据错误';
+            const datas = data instanceof Array ? data : [data];
+            const ids = datas.map(x => { return x.id; });
+            const orgData = await this.getAllDataByCondition({ where: { id: ids }});
+            const updateData = [];
+            for (const row of datas) {
+                const oData = orgData.find(x => { return x.id === row.id });
+                if (!oData || oData.stage_id !== stage.id || oData.tree_id !== row.tree_id) throw '提交数据错误';
+
+                let nData = { id: oData.id, tree_id: oData.tree_id, update_user_id: this.ctx.session.sessionUser.accountId };
+
+                // calc
+                if (row.quantity !== undefined || row.unit_price !== undefined) {
+                    nData.quantity = row.quantity !== undefined ? helper.round(row.quantity || 0, decimal.qty) : oData.quantity || 0;
+                    nData.unit_price = row.unit_price !== undefined ? helper.round(row.unit_price || 0, decimal.up) : oData.unit_price || 0;
+                    nData.total_price = helper.mul(nData.quantity, nData.unit_price, decimal.tp);
+                }
+                for (const field of costFields.textFields) {
+                    if (row[field] !== undefined) nData[field] = row[field] || '';
+                }
+                updateData.push(nData);
+            }
+
+            await this.db.updateRows(this.tableName, updateData);
+            return { update: updateData };
+        }
+
+        async auditCache(transaction, stageId, auditInfo) {
+            const leaf = await this.getAllDataByCondition({ where: { stage_id: stageId, tree_is_leaf: true} });
+            const updateData = [];
+            for (const l of leaf) {
+                const diff = costFields.curFields.find(x => {
+                    return l[x] !== l['read_' + x];
+                });
+                if (diff || !l.calc_his) {
+                    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] = l[f];
+                    }
+                    his.push(newHis);
+                    data.calc_his = JSON.stringify(his);
+                    updateData.push(data);
+                }
+            }
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+        }
+
+        async setDecimal(decimal) {
+            if (!this.ctx.costStage) throw '读取数据错误';
+            if (this.ctx.costStage.stage_order !== this.ctx.costStage.latestOrder) throw '往期不可修改小数位数';
+            if (this.ctx.costStage.audit_status !== auditConst.status.uncheck && this.ctx.costStage.audit_status !== auditConst.status.checkNo) throw '仅原报可修改小数位数';
+            const orgDecimal = this.ctx.costStage.decimal;
+
+            const calc = decimal.tp < orgDecimal.tp || decimal.qty !== orgDecimal.qty || decimal.up !== orgDecimal.up;
+            this.ctx.costStage.decimal = this.ctx.helper._.assignIn(this.ctx.costStage.typeInfo.decimal, decimal);
+
+            const updateData = [];
+            if (calc) {
+                const calcData = await this.getAllDataByCondition({ where: {stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
+                for (const cd of calcData) {
+                    const nd = { id: cd.id, tree_id: cd.tree_id };
+                    nd.quantity = this.ctx.helper.round(cd.quantity, decimal.qty);
+                    nd.unit_price = this.ctx.helper.round(cd.unit_price, decimal.up);
+                    nd.total_price = this.ctx.helper.mul(cd.quantity, cd.unit_price, decimal.tp);
+                    updateData.push(nd);
+                }
+            }
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.ctx.service.costStage.tableName, { id: this.ctx.costStage.id, decimal: JSON.stringify(this.ctx.costStage.decimal)});
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                await conn.commit();
+                return { calc, bills: updateData };
+            } catch(err) {
+                await conn.rollback();
+                return { calc: false, bills: [] };
+            }
+        }
+
+        async getSum(stage) {
+            const sumFields = costFields.curFields.map(f => { return `SUM(${f}) AS ${f}`});
+            const result = await this.db.queryOne(`SELECT ${sumFields.join(', ')} FROM ${this.tableName} where stage_id = ?`, [stage.id]);
+            return result;
+        }
+    }
+
+    return CostStageLedger;
+};

+ 1 - 0
app/service/tender_permission.js

@@ -48,6 +48,7 @@ module.exports = app => {
                 },
                 cost: {
                     view: { title: '查看', value: 1, isDefault: 1 },
+                    duty_add: { title: '责任成本上报', value: 6 },
                     ledger_add: { title: '成本台账上报', value: 2 },
                     book_add: { title: '财务账面上报', value: 3 },
                     analysis_add: { title: '成本分析上报', value: 4 },

+ 5 - 4
app/view/cost/audit_btn.ejs

@@ -27,15 +27,16 @@
         <% } %>
     <% } %>
 
-    <% if (ctx.costStage.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0 && ctx.costStage.audit_status === auditConst.status.checked && ctx.costStage.isLatest) { %>
-        <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-back" class="btn btn-warning btn-sm btn-block">重新审批</a>
-    <% } %>
-
     <% if (ctx.costStage.cancancel) { %>
         <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-cancel" class="btn btn-danger btn-sm btn-block">撤回</a>
     <% } %>
 
+    <% if (ctx.costStage.stage_type !== 'duty') { %>
+    <% if (ctx.costStage.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0 && ctx.costStage.audit_status === auditConst.status.checked && ctx.costStage.isLatest) { %>
+    <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-back" class="btn btn-warning btn-sm btn-block">重新审批</a>
+    <% } %>
     <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId && ctx.costStage.isLatest && (ctx.costStage.audit_status === auditConst.status.checkNo || ctx.costStage.audit_status === auditConst.status.uncheck)) { %>
         <a href="#del-qi" data-toggle="modal" data-target="#del-qi" class="btn btn-outline-danger btn-sm btn-block mt-5">删除本期</a>
     <% } %>
+    <% } %>
 </div>

+ 82 - 0
app/view/cost/duty.ejs

@@ -0,0 +1,82 @@
+<% include ./stage_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./stage_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+                <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId) { %>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" name="base-opr" type="add" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="delete" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
+                </div>
+                <% } %>
+            </div>
+        </div>
+    </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-0" style="overflow: hidden" id="bills-spread">
+                </div>
+            </div>
+            <div class="c-body" id="right-view" style="display: none; width: 33%;">
+                <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
+                <div class="tab-content">
+                    <div id="search" class="tab-pane tab-select-show">
+                    </div>
+                    <div id="extra" class="tab-pane tab-select-show">
+                        <div class="sjs-height-0" style="height: 518px;">
+                            <table class="table table-sm table-bordered">
+                                <thead>
+                                <tr class="text-center"><th>分类</th><th>金额</th></tr>
+                                </thead>
+                                <tbody id="duty-extra"></tbody>
+                            </table>
+                        </div>
+                    </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="#extra" href="javascript: void(0);">附加信息</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
+<script>
+    const readOnly = <%- ctx.costStage.readOnly %>;
+    const tenderId = parseInt('<%- ctx.tender.id %>');
+    const stageId = parseInt('<%- ctx.costStage.id %>');
+    const stageDate = '<%- ctx.costStage.stage_date %>';
+    const costStageComplete = <%- ctx.costStage.audit_status === auditConst.status.checked %>;
+    const colSet = JSON.parse('<%- JSON.stringify(colSet) %>');
+</script>

+ 2 - 0
app/view/cost/duty_menu_list.ejs

@@ -0,0 +1,2 @@
+<% include ./list_menu_list.ejs %>
+<% include ./audit_btn.ejs %>

+ 2 - 0
app/view/cost/duty_modal.ejs

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

+ 12 - 0
app/view/cost/list_menu_list.ejs

@@ -1,4 +1,7 @@
 <nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<% if (ctx.tender.costDuty) { %>
+<nav-menu title="责任成本" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/duty/1/stage" ml="3" active="<%= (ctx.url.indexOf('duty') >= 0 ? 1 : -1) %>"></nav-menu>
+<% } %>
 <nav-menu title="成本报审" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger" 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" ml="3" active="<%= (ctx.url.indexOf('book') >= 0 ? 1 : -1) %>"></nav-menu>
 <nav-menu title="成本分析" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis" ml="3" active="<%= (ctx.url.indexOf('analysis') >= 0 ? 1 : -1) %>"></nav-menu>
@@ -7,3 +10,12 @@
 <% if (ctx.session.sessionUser.is_admin) { %>
 <nav-menu title="审批流程" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/flow" ml="3" active="<%= (ctx.url.indexOf('flow') >= 0 ? 1 : -1) %>"></nav-menu>
 <% } %>
+<% if (!ctx.tender.costDuty && ctx.permission.cost.duty_add) { %>
+<div class="contarl-box">
+    <form action="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/addStage" method="post">
+        <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+        <input type="hidden" name="stage_type" value="duty" />
+        <button class="btn btn-primary btn-sm btn-block" type="submit"><i class="fa fa-plus"></i> 责任成本</button>
+    </form>
+</div>
+<% } %>

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

@@ -7,6 +7,7 @@
         <% if (ctx.costStage.stage_type === 'ledger') { %><% include ./ledger_menu_list.ejs %><% } %>
         <% if (ctx.costStage.stage_type === 'book') { %><% include ./book_menu_list.ejs %><% } %>
         <% if (ctx.costStage.stage_type === 'analysis') { %><% include ./analysis_menu_list.ejs %><% } %>
+        <% if (ctx.costStage.stage_type === 'duty') { %><% include ./duty_menu_list.ejs %><% } %>
         <div class="side-fold"><a href="javascript: void(0)" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu"><i class="fa fa-upload fa-rotate-270"></i></a></div>
     </div>
     <script>

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

@@ -8,6 +8,7 @@
         <% if (ctx.costStage.stage_type === 'ledger') { %><% include ./ledger_menu_list.ejs %><% } %>
         <% if (ctx.costStage.stage_type === 'book') { %><% include ./book_menu_list.ejs %><% } %>
         <% if (ctx.costStage.stage_type === 'analysis') { %><% include ./analysis_menu_list.ejs %><% } %>
+        <% if (ctx.costStage.stage_type === 'duty') { %><% include ./duty_menu_list.ejs %><% } %>
         <div class="side-fold"><a href="javascript: void(0);" data-toggle="tooltip" data-placement="top" data-original-title="展开侧栏" id="to-menu"><i class="fa fa-upload fa-rotate-90"></i></a></div>
     </div>
 </div>

+ 24 - 0
config/web.js

@@ -2576,6 +2576,30 @@ const JsFiles = {
                 ],
                 mergeFile: 'cost_stage',
             },
+            cost_stage_duty: {
+                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/component/menu.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tools_att.js',
+                    '/public/js/shares/common_audit.js',
+                    '/public/js/cost_stage_duty.js',
+                ],
+                mergeFile: 'cost_stage_duty',
+            },
             cost_stage_ledger: {
                 files: [
                     '/public/js/js-xlsx/xlsx.full.min.js',

+ 34 - 0
sql/update.sql

@@ -75,6 +75,40 @@ ADD COLUMN `ctrl_profit` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '控制目标
 ADD COLUMN `real_optimize_rate` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '实际投资-优化率' AFTER `ctrl_profit`,
 ADD COLUMN `real_profit` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '实际投资-收益金额' AFTER `real_optimize_rate`;
 
+CREATE TABLE `zh_cost_stage_duty` (
+  `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid',
+  `cost_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(不同期保持统一)',
+  `tender_id` int(11) unsigned NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期id',
+  `tree_id` int(11) NOT NULL COMMENT '节点id',
+  `tree_pid` int(11) NOT NULL COMMENT '父节点id',
+  `tree_level` tinyint(4) NOT NULL COMMENT '层级',
+  `tree_order` mediumint(4) NOT NULL COMMENT '同级排序',
+  `tree_full_path` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '层级定位辅助字段parent.full_path-tree_id',
+  `tree_is_leaf` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '是否叶子节点,界面显示辅助字段',
+  `code` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '编号',
+  `b_code` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '清单编号',
+  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `unit` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '单位',
+  `unit_price` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '单价',
+  `quantity` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '数量',
+  `total_price` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '金额',
+  `drawing_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL 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_unit_price` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '单价-只读',
+  `read_quantity` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '数量-只读',
+  `read_total_price` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '金额-只读',
+  `calc_his` json DEFAULT NULL COMMENT '本期历史',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+ALTER TABLE `zh_cost_stage`
+ADD COLUMN `extra_info` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '附加信息' AFTER `calc_template`;
+
 ------------------------------------
 -- 表数据
 ------------------------------------