Przeglądaj źródła

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

Tony Kang 10 godzin temu
rodzic
commit
a4d2c4746f

+ 1 - 1
app/const/material.js

@@ -43,7 +43,7 @@ const decimal = {
     tp: 2, // 金额
 };
 const exponent_decimal = {
-    up: 3, // 单价, 基准价格指数,现行价格
+    up: 3, // 单价、基准价格指数、现行价格指数、有效价格
     tp: 0, // 金额
     qty: 3, // 计算值
 };

+ 29 - 1
app/controller/advance_controller.js

@@ -6,6 +6,7 @@ const typeConsts = require('../const/advance');
 const sendToWormhole = require('stream-wormhole');
 const path = require('path');
 const fs = require('fs');
+const auditType = require('../const/audit').auditType;
 module.exports = app => {
     class advanceController extends app.BaseController {
         constructor(ctx) {
@@ -81,9 +82,11 @@ module.exports = app => {
                 jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.advance.info),
                 preUrl: `/tender/${ctx.tender.id}/advance/${ctx.advance.id}`,
                 whiteList: ctx.app.config.multipart.whitelist,
+                adminAuditors: [],
             };
             // 获取所有项目参与者
-            if ((ctx.advance.status === auditConst.status.uncheck || ctx.advance.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.advance.uid) {
+            if (((ctx.advance.status === auditConst.status.uncheck || ctx.advance.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.advance.uid) ||
+                (ctx.session.sessionUser.is_admin && ctx.advance.status !== auditConst.status.uncheck && ctx.advance.status !== auditConst.status.checked)) {
                 // data.accountGroup = accountGroup;
                 // 获取所有项目参与者
                 const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
@@ -94,6 +97,9 @@ module.exports = app => {
                     return { groupName: item.name, groupList };
                 }).filter(x => { return x.groupList.length > 0; });
                 data.auditorList = await ctx.service.advanceAudit.getAuditors(ctx.advance.id, ctx.advance.times);
+                if (ctx.session.sessionUser.is_admin) {
+                    data.adminAuditors = await ctx.service.advanceAudit.getAdminAuditGroups(ctx.advance.id, ctx.advance.times);
+                }
             }
             // data.fileList = await ctx.service.advanceFile.getAdvanceFiles({ vid: ctx.advance.id });
             // 获取审批流程中右边列表
@@ -187,6 +193,7 @@ module.exports = app => {
                 renderData.typeColMap = typeConsts.typeColMap;
                 renderData.advancePayTotal = advancePayTotal;
                 renderData.prevAdvance = prevAdvance;
+                renderData.auditType = auditType;
                 await this.layout('advance/detail.ejs', renderData, 'advance/modal_audit.ejs');
             } catch (error) {
                 this.log(error);
@@ -444,6 +451,27 @@ module.exports = app => {
             }
         }
 
+        /**
+         * 管理员修改当前预付款审批流程
+         * @param {Object} ctx 全局上下文
+         */
+        async saveAudit(ctx) {
+            try {
+                if (!ctx.session.sessionUser.is_admin ||
+                    ctx.advance.status === auditConst.status.uncheck ||
+                    ctx.advance.status === auditConst.status.checked) {
+                    throw '您无权进行该操作';
+                }
+                const data = JSON.parse(ctx.request.body.data);
+                await ctx.service.advanceAudit.saveAudit(ctx.advance, data);
+                const auditors = await ctx.service.advanceAudit.getAdminAuditGroups(ctx.advance.id, ctx.advance.times);
+                ctx.body = { err: 0, msg: '', data: auditors };
+            } catch (err) {
+                ctx.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
         _checkAdvanceFileCanModify(ctx) {
             // 检查登录用户,是否可操作
             const accountId = ctx.session.sessionUser.accountId;

+ 1 - 1
app/controller/change_controller.js

@@ -1151,7 +1151,7 @@ module.exports = app => {
                 const pid = this.ctx.session.sessionProject.id;
                 switch (status) {
                     case 3:// 审批通过
-                        const revising = await ctx.service.change.checkRevising(changeData.tid, changeData.cid);
+                        const revising = await ctx.service.change.checkRevising(changeData.tid, changeData.cid, changeData.order_by);
                         if (revising) {
                             throw '台账修订中并本变更令存在新增清单,无法审批通过';
                         }

+ 1 - 1
app/controller/contract_controller.js

@@ -412,7 +412,7 @@ module.exports = app => {
                 if (!ctx.session.sessionUser.is_admin) throw '无权进行列设置';
                 const colType = ctx.request.body.col_type;
                 const colSet = JSON.parse(ctx.request.body.col_set);
-                const contractType = ctx.request.body.type;
+                const contractType = Number(ctx.request.body.type);
                 if ([contractConst.type.expenses, contractConst.type.income].indexOf(contractType) < 0) throw '合同类型错误';
                 await ctx.service.contractColSet.setContractColSet(ctx.subProject.id, ctx.contractOptions.tid, contractType, colType, colSet);
                 ctx.redirect(ctx.request.header.referer);

+ 4 - 4
app/controller/dashboard_controller.js

@@ -149,7 +149,7 @@ module.exports = app => {
                 noticeList.push(...ctx.helper.addKeyValue4ObjArray(spCostStageDuty, 'shenpi_type', 'costStageDuty'));
                 const spContracts = subProject.page_show.openContract ? ctx.helper._.filter(auditShenpiContracts, c => c.spid === subProject.id || c.tender_spid === subProject.id) : [];
                 noticeList.push(...ctx.helper.addKeyValue4ObjArray(spContracts, 'shenpi_type', 'contract'));
-                const spContractPays = (subProject.page_show.openContractTenderShenpi || subProject.page_show.openContractSubProjectShenpi) ? ctx.helper._.filter(auditShenpiContractPays, cp => cp.spid === subProject.id || cp.tender_spid === subProject.id) : [];
+                const spContractPays = (subProject.page_show.openContractPayTenderShenpi || subProject.page_show.openContractPaySubProjectShenpi) ? ctx.helper._.filter(auditShenpiContractPays, cp => cp.spid === subProject.id || cp.tender_spid === subProject.id) : [];
                 for (const pay of spContractPays) {
                     pay.shenpi_type = pay.contract_type === 2 ? 'contractReceipt' : 'contractPay';
                 }
@@ -387,7 +387,7 @@ module.exports = app => {
                             break;
                         case 'contractPay':
                         case 'contractReceipt':
-                            if (!sp.page_show.openContractTenderShenpi && !sp.page_show.openContractSubProjectShenpi) closeType = true;
+                            if (!sp.page_show.openContractPayTenderShenpi && !sp.page_show.openContractPaySubProjectShenpi) closeType = true;
                             calcTime = t.begin_time;
                             break;
                         default:
@@ -446,8 +446,8 @@ module.exports = app => {
             // 获取待审批的合同列表
             const auditContracts = ctx.subProject.page_show.openContract ? await ctx.service.contractSpAudit.getAuditContract(ctx.session.sessionUser.accountId, ctx.subProject.id) : [];
             // 获取待审批的合同支付(回款)列表
-            const auditContractPays = (ctx.subProject.page_show.openContractTenderShenpi || ctx.subProject.page_show.openContractSubProjectShenpi) ? await ctx.service.contractSpAudit.getAuditContractPay(ctx.session.sessionUser.accountId, ctx.subProject.id) : [];
-            
+            const auditContractPays = (ctx.subProject.page_show.openContractPayTenderShenpi || ctx.subProject.page_show.openContractPaySubProjectShenpi) ? await ctx.service.contractSpAudit.getAuditContractPay(ctx.session.sessionUser.accountId, ctx.subProject.id) : [];
+
             const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
             const noticeList = await ctx.service.noticePush.getNotice(ctx.session.sessionProject.id, pa.id, ctx.subProject.id);
             const projectData = await ctx.service.project.getDataById(ctx.session.sessionProject.id);

+ 22 - 0
app/controller/material_controller.js

@@ -1238,6 +1238,28 @@ module.exports = app => {
                 // 转换为整型,不然会无法获取
                 if (data.ms_id) data.ms_id = parseInt(data.ms_id);
                 if (data.mn_id) data.mn_id = parseInt(data.mn_id);
+                const isPastAdminRiskEdit = ctx.material.readOnly && ctx.session.sessionUser.is_admin && ctx.material.order < ctx.material.highOrder && (data.type === 'update' || data.type === 'paste');
+                if (isPastAdminRiskEdit) {
+                    const updateList = data.type === 'paste' ? data.updateData : [data.updateData];
+                    const riskUpdates = updateList.map(item => ({
+                        id: ctx.material.showStageExponent || ctx.material.exponent_node ? item.me_id : item.id,
+                        m_up_risk: item.m_up_risk,
+                        m_down_risk: item.m_down_risk,
+                    }));
+                    if (riskUpdates.some(item => !item.id)) throw '往期指数工料参数有误';
+                    if (ctx.material.showStageExponent || ctx.material.exponent_node) {
+                        responseData.data = data.type === 'paste' ?
+                            await ctx.service.materialExponent.saveDatas(riskUpdates, data.ms_id, data.mn_id) :
+                            await ctx.service.materialExponent.save(riskUpdates[0], data.ms_id, data.mn_id);
+                    } else {
+                        responseData.data = await ctx.service.materialExponent.saveHistoryRisks(riskUpdates);
+                    }
+                    ctx.body = responseData;
+                    return;
+                }
+                if (ctx.material.readOnly && ['add', 'del', 'update', 'paste', 'ex_calc', 'update_node'].includes(data.type)) {
+                    throw '无权操作';
+                }
                 switch (data.type) {
                     case 'add':
                         responseData.data = await ctx.service.materialExponent.add();

+ 24 - 0
app/controller/stage_controller.js

@@ -490,6 +490,11 @@ module.exports = app => {
                             const exprData = await ctx.service.expr.getAllDataByCondition({ where: { tid: ctx.stage.tid, calc_module: 'stage' } });
                             responseData.data.expr = exprData;
                             break;
+                        case 'auditLocked':
+                            responseData.data.auditLocked = ctx.session.sessionUser.is_admin
+                                ? ctx.stage.auditLocked
+                                : ctx.stage.auditLocked.filter(x => { return x.aid === ctx.session.sessionUser.accountId; });
+                            break;
                     }
                 }
 
@@ -499,6 +504,25 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
         }
+        async unlock(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.updateType || !data.updateData) throw '数据错误';
+
+                const responseData = { err: 0, msg: '' };
+                switch(data.updateType) {
+                    case 'audit_locked_lid':
+                        responseData.data = await ctx.service.stageAudit.updateUnlockLid(ctx.stage, data.updateData);
+                        break;
+                    default:
+                        throw '数据错误';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '解锁数据失败');
+            }
+        }
 
         async _innerCheckChangeBillsOver(ctx, checkData) {
             const change = await this.ctx.service.change.getAllDataByCondition({ where: { tid: ctx.tender.id } });

+ 3 - 2
app/lib/rm/material.js

@@ -314,7 +314,8 @@ class rptMemChange extends RptMemBase {
             });
         } else {
             const sql = 'SELECT me.id, me.tid, me.mid, me.type, me.symbol, me.symbol_desc, me.code, me.remark, me.in_time,' +
-                '    meh.type, meh.weight_num, meh.basic_price, meh.basic_times, meh.m_price, meh.calc_num, meh.is_summary' +
+                '    meh.type, meh.weight_num, meh.basic_price, meh.basic_times, meh.m_up_risk, meh.m_down_risk, ' +
+                '    meh.m_price, meh.effective_price, meh.calc_num, meh.is_summary' +
                 '  FROM ' + this.ctx.service.materialExponentHistory.tableName + ' meh ' +
                 '  LEFT JOIN ' + this.ctx.service.materialExponent.tableName + ' me ON meh.me_id = me.id ' +
                 '  WHERE meh.tid = ? And meh.mid = ?'+
@@ -516,4 +517,4 @@ class rptMemChange extends RptMemBase {
     }
 }
 
-module.exports = rptMemChange;
+module.exports = rptMemChange;

+ 3 - 2
app/lib/rm/tender_material.js

@@ -464,7 +464,8 @@ class ReportMemoryMaterial {
             });
         } else {
             const sql = 'SELECT me.id, me.tid, me.mid, me.type, me.symbol, me.symbol_desc, me.code, me.remark, me.in_time,' +
-                '    meh.type, meh.weight_num, meh.basic_price, meh.basic_times, meh.m_price, meh.calc_num, meh.is_summary' +
+                '    meh.type, meh.weight_num, meh.basic_price, meh.basic_times, meh.m_up_risk, meh.m_down_risk, ' +
+                '    meh.m_price, meh.effective_price, meh.calc_num, meh.is_summary' +
                 '  FROM ' + this.ctx.service.materialExponentHistory.tableName + ' meh ' +
                 '  LEFT JOIN ' + this.ctx.service.materialExponent.tableName + ' me ON meh.me_id = me.id ' +
                 '  WHERE meh.tid = ? And meh.mid = ?'+
@@ -485,4 +486,4 @@ class ReportMemoryMaterial {
     }
 }
 
-module.exports = ReportMemoryMaterial;
+module.exports = ReportMemoryMaterial;

+ 1 - 0
app/middleware/stage_check.js

@@ -151,6 +151,7 @@ module.exports = options => {
             // 根据状态判断是否需要更新审批人列表
             yield this.service.stage.doCheckStageCanCancel(stage);
             stage.readySettle = yield this.service.settle.getReadySettle(stage.tid);
+            stage.auditLocked = yield this.service.stageAudit.getAuditLockedId(stage);
             this.stage = stage;
             // 根据状态判断是否需要更新审批人列表
             if ((stage.status === status.uncheck || stage.status === status.checkNo) && this.tender.info.shenpi.stage !== shenpiConst.sp_status.sqspr) {

+ 104 - 4
app/public/js/advance_audit.js

@@ -34,6 +34,104 @@ $(document).ready(function () {
     let oldSearchVal = null
     handleFileList(fileList)
 
+    if ($('#admin-edit-shenpi').length) {
+        let adminAuditors = advanceAdminAuditors
+        let adminAuditChanged = false
+
+        function makeAdminSelect(code, aid, operate) {
+            let groups = ''
+            accountGroup.forEach((group, idx) => {
+                if (!group) return
+                const users = group.groupList.map(item => `<dd class="border-bottom p-2 mb-0" data-id="${item.id}">
+                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span class="ml-auto">${item.mobile || ''}</span></p>
+                    <span class="text-muted">${item.role || ''}</span></dd>`).join('')
+                groups += `<dt><a href="javascript:void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i></a> ${group.groupName}</dt>
+                    <div class="dd-content" data-toggleid="${idx}">${users}</div>`
+            })
+            return `<div class="dropdown-menu dropdown-menu-right" id="${code}_dropdownMenu" aria-labelledby="${code}_dropdownMenuButton" style="width:220px">
+                <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search" placeholder="姓名/手机 检索" autocomplete="off" data-code="${code}"></div>
+                <dl class="list-unstyled book-list" data-aid="${aid}" data-operate="${operate}">${groups}</dl></div>`
+        }
+
+        function renderAdminAuditors(groups) {
+            const html = []
+            groups.forEach((group, i) => {
+                if (i === 0) return
+                group.forEach((auditor, j) => {
+                    html.push('<tr>')
+                    html.push(`<td class="text-left">${j === 0 ? i + '审' : ''}</td>`)
+                    html.push(`<td>${auditor.name} <small class="text-muted">${auditor.role || ''}</small></td>`)
+                    const statusText = auditor.status === auditConst.status.uncheck ? '待审批' : (auditConst.auditString[auditor.status] || '')
+                    html.push(`<td class="text-center"><span class="${auditConst.auditStringClass[auditor.status] || ''}">${statusText}</span></td>`)
+                    html.push('<td class="text-center">')
+                    if ((auditor.status === auditConst.status.checking || auditor.status === auditConst.status.uncheck) && j === group.length - 1) {
+                        html.push(`<span class="dropdown mr-2"><a href="javascript:void(0)" id="${auditor.audit_id}_add_dropdownMenuButton" data-toggle="dropdown">增加</a>${makeAdminSelect(auditor.audit_id + '_add', auditor.audit_id, 'add')}</span>`)
+                    }
+                    if (auditor.status === auditConst.status.uncheck) {
+                        html.push(`<span class="dropdown mr-2"><a href="javascript:void(0)" id="${auditor.audit_id}_change_dropdownMenuButton" data-toggle="dropdown">更换</a>${makeAdminSelect(auditor.audit_id + '_change', auditor.audit_id, 'change')}</span>`)
+                        html.push(`<span class="dropdown"><a href="javascript:void(0)" class="text-danger" data-toggle="dropdown">移除</a>
+                            <div class="dropdown-menu"><span class="dropdown-item">确认移除审批人?</span><div class="dropdown-divider"></div>
+                            <div class="px-2 py-1 text-center"><button class="remove-audit btn btn-sm btn-danger" data-id="${auditor.audit_id}">移除</button> <button class="btn btn-sm btn-secondary">取消</button></div></div></span>`)
+                    }
+                    html.push('</td></tr>')
+                })
+            })
+            $('#admin-edit-shenpi-list').html(html.join(''))
+        }
+
+        renderAdminAuditors(adminAuditors)
+
+        $('body').on('click', '#admin-edit-shenpi .book-list dt', function () {
+            const idx = $(this).find('.acc-btn').attr('data-groupid')
+            const button = $(this).find('.acc-btn')
+            const content = $(this).parent().find(`div[data-toggleid="${idx}"]`)
+            const showing = button.attr('data-type') === 'hide'
+            content.toggle(showing)
+            button.attr('data-type', showing ? 'show' : 'hide')
+            button.find('i').toggleClass('fa-plus-square', !showing).toggleClass('fa-minus-square-o', showing)
+            return false
+        })
+
+        let adminSearchTimer = null
+        $('body').on('input propertychange', '#admin-edit-shenpi .gr-search', function () {
+            const input = $(this)
+            clearTimeout(adminSearchTimer)
+            adminSearchTimer = setTimeout(() => {
+                const keyword = input.val()
+                const list = input.closest('.dropdown-menu').find('.book-list')
+                if (!keyword) {
+                    list.html($(makeAdminSelect(input.data('code'), list.data('aid'), list.data('operate'))).find('.book-list').html())
+                    return
+                }
+                list.html(accountList.filter(item => item && (item.name.indexOf(keyword) !== -1 || (item.mobile && item.mobile.indexOf(keyword) !== -1)))
+                    .map(item => `<dd class="border-bottom p-2 mb-0" data-id="${item.id}"><p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span class="ml-auto">${item.mobile || ''}</span></p><span class="text-muted">${item.role || ''}</span></dd>`).join(''))
+            }, 400)
+        })
+
+        $('body').on('click', '#admin-edit-shenpi dl dd', function () {
+            const newAid = Number($(this).data('id'))
+            const list = $(this).closest('.book-list')
+            if (adminAuditors.some(group => group.some(auditor => auditor.audit_id === newAid))) return toastr.warning('该审核人已存在,请勿重复添加')
+            postData(preUrl + '/audit/save', { operate: list.data('operate'), old_aid: Number(list.data('aid')), new_aid: newAid }, datas => {
+                adminAuditors = datas
+                adminAuditChanged = true
+                renderAdminAuditors(datas)
+            })
+        })
+
+        $('body').on('click', '#admin-edit-shenpi .remove-audit', function () {
+            postData(preUrl + '/audit/save', { operate: 'del', old_aid: Number($(this).data('id')) }, datas => {
+                adminAuditors = datas
+                adminAuditChanged = true
+                renderAdminAuditors(datas)
+            })
+        })
+
+        $('#sub-sp2').on('hidden.bs.modal', function () {
+            if (adminAuditChanged) window.location.reload()
+        })
+    }
+
     // 控制上报弹窗的文案
     function checkModal(isHide) {
         $('#start-modal').empty()
@@ -61,8 +159,8 @@ $(document).ready(function () {
                         <span class="text-muted">${item.role || ''}</span>
                     </dd>`
                 })
-                $('.book-list').empty()
-                $('.book-list').append(html)
+                $('.book-list').not('#admin-edit-shenpi .book-list').empty()
+                $('.book-list').not('#admin-edit-shenpi .book-list').append(html)
             } else {
                 if (!$('.acc-btn').length) {
                     accountGroup.forEach((group, idx) => {
@@ -81,8 +179,8 @@ $(document).ready(function () {
                         });
                         html += '</div>'
                     })
-                    $('.book-list').empty()
-                    $('.book-list').append(html)
+                    $('.book-list').not('#admin-edit-shenpi .book-list').empty()
+                    $('.book-list').not('#admin-edit-shenpi .book-list').append(html)
                 }
             }
         }, 400);
@@ -90,6 +188,7 @@ $(document).ready(function () {
 
     // 添加审批流程按钮逻辑
     $('.book-list').on('click', 'dt', function () {
+        if ($(this).closest('#admin-edit-shenpi').length) return
         const idx = $(this).find('.acc-btn').attr('data-groupid')
         const type = $(this).find('.acc-btn').attr('data-type')
         if (type === 'hide') {
@@ -109,6 +208,7 @@ $(document).ready(function () {
 
     // 添加到审批流程中
     $('dl').on('click', 'dd', function () {
+        if ($(this).closest('#admin-edit-shenpi').length) return
         const id = parseInt($(this).data('id'))
         if (id !== 0) {
             postData(preUrl + '/audit/add', { auditorId: id }, (datas) => {

+ 68 - 12
app/public/js/material_exponent.js

@@ -14,6 +14,7 @@ function getPasteHint (str, row = '') {
 }
 $(document).ready(() => {
     autoFlashHeight();
+    const canAdminEditHistoryRisk = isAdmin && materialOrder < materialHighOrder;
     const materialExponentSpread = SpreadJsObj.createNewSpread($('#material-exponent-spread')[0]);
     const materialExponentSpreadSetting = {
         cols: [
@@ -23,7 +24,10 @@ $(document).ready(() => {
             {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 1, width: 60, formatter: '@', readOnly: 'readOnly.isEdit'},
             {title: '基本价格指数(F0)', colSpan: '1', rowSpan: '2', field: 'basic_price', hAlign: 2, width: 120, readOnly: 'readOnly.isEdit'},
             {title: '基准时间', colSpan: '1', rowSpan: '2', field: 'basic_times', hAlign: 0, width: 80, formatter: '@', readOnly: 'readOnly.isEdit'},
-            {title: '现行价格指数(Ft)', colSpan: '1', rowSpan: '2', field: 'm_price', hAlign: 2, width: 120, type: 'Number', readOnly: 'readOnly.remark'},
+            {title: '上涨幅度(%)', colSpan: '1', rowSpan: '2', field: 'm_up_risk', hAlign: 2, width: 85, type: 'Number', readOnly: 'readOnly.risk'},
+            {title: '下跌幅度(%)', colSpan: '1', rowSpan: '2', field: 'm_down_risk', hAlign: 2, width: 85, type: 'Number', readOnly: 'readOnly.risk'},
+            {title: '现行价格指数(Fto)', colSpan: '1', rowSpan: '2', field: 'm_price', hAlign: 2, width: 125, type: 'Number', readOnly: 'readOnly.remark'},
+            {title: '有效价格指数(Ft)', colSpan: '1', rowSpan: '2', field: 'effective_price', hAlign: 2, width: 125, type: 'Number', readOnly: true, getValue: 'getValue.effective_price'},
             {title: '加权系数(B)', colSpan: '1', rowSpan: '2', field: 'weight_num', hAlign: 2, width: 120, formatter: '@', readOnly: 'readOnly.isConstant'},
             {title: '计算值(Ft/F0*B)', colSpan: '1', rowSpan: '2', field: 'calc_num', hAlign: 2, width: 100, formatter: '@', readOnly: true, getValue: 'getValue.calc_num'},
             {title: '备注', colSpan: '1', rowSpan: '2', field: 'remark', hAlign: 0, width: 60, formatter: '@', readOnly: 'readOnly.remark'},
@@ -35,7 +39,7 @@ $(document).ready(() => {
         defaultRowHeight: 21,
         headerFont: '12px 微软雅黑',
         font: '12px 微软雅黑',
-        readOnly: readOnly,
+        readOnly: readOnly && !canAdminEditHistoryRisk,
     };
     const materialExponentBase = {
         isUsed: function (data) {
@@ -54,9 +58,28 @@ $(document).ready(() => {
     };
     const materialExponentCol = {
         getValue: {
+            effective_price: function (data) {
+                if (data.type === 1 || data.m_price === null || data.m_price === undefined) return null;
+                const upRisk = Number(data.m_up_risk) || 0;
+                const downRisk = Number(data.m_down_risk) || 0;
+                let effectivePrice = data.m_price;
+                if ((upRisk !== 0 || downRisk !== 0) && data.basic_price > 0) {
+                    const upperLimit = ZhCalc.mul(data.basic_price, ZhCalc.add(1, ZhCalc.div(upRisk, 100)));
+                    const lowerLimit = ZhCalc.mul(data.basic_price, ZhCalc.sub(1, ZhCalc.div(downRisk, 100)));
+                    if (data.m_price > upperLimit) {
+                        effectivePrice = ZhCalc.sub(data.m_price, ZhCalc.mul(data.basic_price, ZhCalc.div(upRisk, 100)));
+                    } else if (data.m_price < lowerLimit) {
+                        effectivePrice = ZhCalc.add(data.m_price, ZhCalc.mul(data.basic_price, ZhCalc.div(downRisk, 100)));
+                    } else {
+                        effectivePrice = data.basic_price;
+                    }
+                }
+                return ZhCalc.round(effectivePrice, materialExponentDecimal.up);
+            },
             calc_num : function (data) {
                 if (data.type === 1) return null;
-                const calc_num = data.basic_price > 0 ? ZhCalc.mul(data.weight_num, ZhCalc.div(data.m_price, data.basic_price)) : 0;
+                const effectivePrice = materialExponentCol.getValue.effective_price(data);
+                const calc_num = data.basic_price > 0 ? ZhCalc.mul(data.weight_num, ZhCalc.div(effectivePrice, data.basic_price)) : 0;
                 return calc_num > 0 ? ZhCalc.round(calc_num, materialExponentDecimal.qty) : 0;
             },
         },
@@ -70,6 +93,9 @@ $(document).ready(() => {
             remark: function (data) {
                 return !(!readOnly && data.type === 2);
             },
+            risk: function (data) {
+                return !(data.type === 2 && (!readOnly || canAdminEditHistoryRisk));
+            },
             isConstant: function (data) {
                 // return !(!readOnly && materialExponentBase.isConstant(data));
                 return readOnly;
@@ -99,7 +125,7 @@ $(document).ready(() => {
     };
     SpreadJsObj.initSheet(stageTjSpread.getActiveSheet(), stageTjSpreadSetting);
 
-    const needUpdateArray = ['weight_num', 'm_price', 'remark'];
+    const needUpdateArray = ['weight_num', 'm_up_risk', 'm_down_risk', 'm_price', 'effective_price', 'remark'];
 
     const materialExponentSpreadObj = {
         getMaterialExponentData: function (msid = null, mnid = null) {
@@ -322,6 +348,15 @@ $(document).ready(() => {
                         validText = ZhCalc.round(num, materialExponentDecimal.up);
                     }
                 }
+                if (col.field === 'm_up_risk' || col.field === 'm_down_risk') {
+                    validText = validText === null ? 0 : validText;
+                    if (!Number.isInteger(Number(validText)) || Number(validText) < 0 || Number(validText) > 100) {
+                        toastr.error('上涨、下跌幅度必须为0~100的整数');
+                        SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                        return;
+                    }
+                    validText = Number(validText);
+                }
                 if (col.field === 'weight_num') {
                     if (isNaN(validText)) {
                         toastr.error('不能输入其它非数字类型字符');
@@ -349,6 +384,7 @@ $(document).ready(() => {
                     }
                 }
                 select[col.field] = validText;
+                select.effective_price = materialExponentCol.getValue.effective_price(select);
                 select.calc_num = materialExponentCol.getValue.calc_num(select);
                 delete select.waitingLoading;
                 // console.log(select);
@@ -407,7 +443,7 @@ $(document).ready(() => {
             }
         },
         deletePress: function (sheet) {
-            if (!sheet.zh_setting || readOnly || sheet.zh_setting.readOnly) return;
+            if (!sheet.zh_setting || (readOnly && !canAdminEditHistoryRisk) || sheet.zh_setting.readOnly) return;
             // materialExponentBase.isUsed(select)
             if (sheet.zh_setting && sheet.zh_data) {
                 const sel = sheet.getSelections()[0];
@@ -436,12 +472,17 @@ $(document).ready(() => {
                             // bPaste = false;
                             continue;
                         }
-                        materialExData[colSetting.field] = null;
-                        sortData[iRow][colSetting.field] = null;
+                        const clearValue = colSetting.field === 'm_up_risk' || colSetting.field === 'm_down_risk' ? 0 : null;
+                        materialExData[colSetting.field] = clearValue;
+                        sortData[iRow][colSetting.field] = clearValue;
                     }
-                    if (exData.type !== materialType.ex_type[0].value) materialExData.calc_num = materialExponentCol.getValue.calc_num(sortData[iRow]);
                     const arr = Object.keys(materialExData);
                     if (arr.length !== 1) {
+                        if (exData.type !== materialType.ex_type[0].value) {
+                            materialExData.effective_price = materialExponentCol.getValue.effective_price(sortData[iRow]);
+                            materialExData.calc_num = materialExponentCol.getValue.calc_num(sortData[iRow]);
+                        }
+                        if (sortData[iRow].me_id) materialExData.me_id = sortData[iRow].me_id;
                         data.push(materialExData);
                     }
                 }
@@ -486,7 +527,7 @@ $(document).ready(() => {
                 SpreadJsObj.reLoadSheetData(materialExponentSpread.getActiveSheet());
                 return;
             }
-            if (sortData.length > 0 && range.col + range.colCount > 10) {
+            if (sortData.length > 0 && range.col + range.colCount > info.sheet.zh_setting.cols.length) {
                 toastMessageUniq(hint.cellError);
                 SpreadJsObj.reLoadSheetHeader(materialExponentSpread.getActiveSheet());
                 SpreadJsObj.reLoadSheetData(materialExponentSpread.getActiveSheet());
@@ -499,12 +540,15 @@ $(document).ready(() => {
                 const curRow = range.row + iRow;
                 // const materialData = JSON.parse(JSON.stringify(sortData[curRow]));
                 const materialExData = { id: sortData[curRow].id };
+                if (sortData[curRow].me_id) materialExData.me_id = sortData[curRow].me_id;
                 const hintRow = range.rowCount > 1 ? curRow : '';
                 let sameCol = 0;
                 for (let iCol = 0; iCol < range.colCount; iCol++) {
                     const curCol = range.col + iCol;
                     const colSetting = info.sheet.zh_setting.cols[curCol];
                     if (!colSetting) continue;
+                    const colReadOnly = colSetting.readOnly === true || (typeof colSetting.readOnly === 'function' && colSetting.readOnly(sortData[curRow]));
+                    if (colReadOnly) continue;
 
                     let validText = info.sheet.getText(curRow, curCol);
                     validText = is_numeric(validText) ? parseFloat(validText) : (validText ? trimInvalidChar(validText) : null);
@@ -540,6 +584,15 @@ $(document).ready(() => {
                             validText = ZhCalc.round(num, materialExponentDecimal.up);
                         }
                     }
+                    if (colSetting.field === 'm_up_risk' || colSetting.field === 'm_down_risk') {
+                        validText = validText === null ? 0 : validText;
+                        if (!Number.isInteger(Number(validText)) || Number(validText) < 0 || Number(validText) > 100) {
+                            toastMessageUniq(getPasteHint({type: 'error', msg: '上涨、下跌幅度必须为0~100的整数'}, hintRow));
+                            bPaste = false;
+                            continue;
+                        }
+                        validText = Number(validText);
+                    }
                     if (colSetting.field === 'weight_num') {
                         if (isNaN(validText)) {
                             toastMessageUniq(getPasteHint(hint.numberExpr, hintRow));
@@ -570,6 +623,7 @@ $(document).ready(() => {
                     sortData[curRow][colSetting.field] = validText;
                 }
                 if (bPaste) {
+                    materialExData.effective_price = materialExponentCol.getValue.effective_price(sortData[curRow]);
                     materialExData.calc_num = materialExponentCol.getValue.calc_num(sortData[curRow]);
                     data.push(materialExData);
                     // rowData.push(curRow);
@@ -782,15 +836,17 @@ $(document).ready(() => {
         $('#calc_basic_select').html(html);
     }
 
+    if (!readOnly || canAdminEditHistoryRisk) {
+        materialExponentSpread.bind(spreadNS.Events.EditEnded, materialExponentSpreadObj.editEnded);
+        materialExponentSpread.bind(spreadNS.Events.ClipboardPasted, materialExponentSpreadObj.clipboardPasted);
+        if (!editForAudit) SpreadJsObj.addDeleteBind(materialExponentSpread, materialExponentSpreadObj.deletePress);
+    }
     if (!readOnly) {
         $('#add').click(materialExponentSpreadObj.add);
         $('#del').click(materialExponentSpreadObj.del);
         materialExponentSpread.bind(spreadNS.Events.SelectionChanged, materialExponentSpreadObj.selectionChanged);
-        materialExponentSpread.bind(spreadNS.Events.EditEnded, materialExponentSpreadObj.editEnded);
         materialExponentSpread.bind(spreadNS.Events.ButtonClicked, materialExponentSpreadObj.buttonClicked);
-        materialExponentSpread.bind(spreadNS.Events.ClipboardPasted, materialExponentSpreadObj.clipboardPasted);
         if (!editForAudit) {
-            SpreadJsObj.addDeleteBind(materialExponentSpread, materialExponentSpreadObj.deletePress);
             // 右键菜单
             $.contextMenu({
                 selector: '#material-exponent-spread',

+ 10 - 4
app/public/js/path_tree.js

@@ -423,20 +423,29 @@ const createNewPathTree = function (type, setting) {
             }
         }
 
+        refreshNodeVisible() {
+            for (const node of this.nodes) {
+                const parent = this.getParent(node);
+                node.visible = parent ? (parent.expanded && parent.visible && !node.filter) : !node.filter;
+            }
+        }
         loadFilter(select, filterType = 'access') {
             const defaultValue = filterType === 'access' ? true : false;
             this.select = select ? select.split(',') : [];
             for (const d of this.datas) {
                 d.filter = defaultValue;
+                d._fixed = defaultValue;
             }
             const parents = [];
             for (const s of this.select) {
                 const node = this.getItems(s);
                 if (!node) continue;
                 node.filter = !defaultValue;
+                node._fixed = !defaultValue;
                 const posterity = this.getPosterity(node);
                 for (const p of posterity) {
                     p.filter = !defaultValue;
+                    p._fixed = !defaultValue;
                 }
                 parents.push(...this.getAllParents(node));
             }
@@ -446,10 +455,7 @@ const createNewPathTree = function (type, setting) {
                 parent.filter = !parent.children.find(x => { return !x.filter });
 
             }
-            for (const node of this.nodes) {
-                const parent = this.getParent(node);
-                node.visible = parent ? (parent.expanded && parent.visible && !node.filter) : !node.filter;
-            }
+            this.refreshNodeVisible();
         }
         loadAssValid(select) {
             const keyField = this.setting.id;

+ 189 - 0
app/public/js/stage.js

@@ -5166,6 +5166,7 @@ $(document).ready(() => {
             opinion: $(`${'#sp-back'}`).find('[name=opinion]').val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '),
             coop_checked: confirm && confirm.length > 0 ? confirm.map(x => { return x.id; }) : [],
             checkType,
+            audit_locked_lid: $('[name=audit_locked_lid]').val(),
         };
         if ($('#warning-text').length) $('#warning-text').remove();
         if (!checkType && !$('#warning-text').length) {
@@ -5473,6 +5474,194 @@ $(document).ready(() => {
             stageAssistRela.refreshAssistTable();
         });
     });
+
+    // 审批退回锁定
+    const spBackLock = (function(){
+        let lockSpread, lockSheet, lockTree, auditLockedHis;
+        let lockType;
+        const getLockedLid = function() {
+            if (!lockTree) return '';
+            const lockedLid = lockTree.nodes.filter(x => { return x.locked; }).map(y => { return y.ledger_id; });
+            if (lockedLid.length > 200) throw '锁定的节点过多,请尽量在父项锁定数据';
+            return lockedLid.join(',');
+        };
+        const getAuditLocked = function() {
+            if (!lockTree) return [];
+            const lockedLid = lockTree.nodes.filter(x => { return x.locked; }).map(y => { return y.ledger_id});
+            auditLockedHis.forEach(x => {
+                x.org_locked_lid = x.audit_locked_lid.split(',').map(y => { return parseInt(y); });
+                x.new_locked_lid = [];
+                for (const lid in x.org_locked_lid) {
+                    if (lockedLid.indexOf(lid) >= 0) x.new_locked_lid.push(lid);
+                }
+            });
+            const result = auditLockedHis.filter(x => { return x.new_locked_lid.length < x.org_locked_lid.length; })
+                .map(y => { return { id: y.id, audit_locked_lid: y.new_locked_lid.join(',') }; });
+            return result;
+        };
+        const init = function(setting) {
+            if (lockSpread) return;
+            lockSpread = SpreadJsObj.createNewSpread($('#sp-lock-spread')[0]);
+            lockSheet = lockSpread.getActiveSheet();
+            const spreadSetting = {
+                cols: [
+                    {title: '锁定', colSpan: '1', rowSpan: '1', field: 'locked', hAlign: 1, width: 60, cellType: 'checkbox'},
+                    {title: '项目节编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 145, formatter: '@', cellType: 'tree'},
+                    {title: '清单编号', colSpan: '1', rowSpan: '1', field: 'b_code', hAlign: 0, width: 70, formatter: '@'},
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', },
+                    {title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+                    {title: '单价', colSpan: '1', rowSpan: '1', field: 'unit_price', hAlign: 2, width: 60, type: 'Number',},
+                    {title: '本期合同金额', colSpan: '1', rowSpan: '1', field: 'contract_tp', hAlign: 2, width: 100, type: 'Number'},
+                    {title: '本期变更金额', colSpan: '1', rowSpan: '1', field: 'qc_tp', hAlign: 2, width: 100, type: 'Number'},
+                    {title: '本期完成金额', colSpan: '1', rowSpan: '1', field: 'gather_tp', hAlign: 2, width: 100, type: 'Number'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                frozenLineColor: '#93b5e4',
+                readOnly: true,
+            };
+            sjsSettingObj.setFxTreeStyle(spreadSetting, sjsSettingObj.FxTreeStyle.jz);
+            SpreadJsObj.initSheet(lockSheet, spreadSetting);
+            lockTree = createNewPathTree('stage', {
+                id: 'ledger_id', pid: 'ledger_pid', order: 'order',
+                level: 'level', rootId: -1,
+                keys: ['id', 'tender_id', 'ledger_id'],
+                stageId: 'id',
+                calcFields: [],
+            });
+            const lockDatas = setting.tree.datas.map(y => {
+                return {
+                    id: y.id, tender_id: y.tender_id,
+                    ledger_id: y.ledger_id, ledger_pid: y.ledger_pid, order: y.order,
+                    level: y.level, is_leaf: y.is_leaf, full_path: y.full_path,
+                    code: y.code, b_code: y.b_code, name: y.name, unit: y.unit,
+                    unit_price: y.unit_price, quantity: y.quantity, total_price: y.total_price,
+                    contract_tp: y.contract_tp, qc_tp: y.qc_tp, gather_tp: y.gather_tp,
+                    filter: y.filter, _fixed: y._fixed, locked: 0,
+                }
+            });
+            lockTree.loadDatas(lockDatas);
+            lockTree.refreshNodeVisible();
+            SpreadJsObj.loadSheetData(lockSheet, SpreadJsObj.DataType.Tree, lockTree);
+
+            if (lockType === 'lock') {
+                lockSpread.bind(spreadNS.Events.ButtonClicked, function(e, info) {
+                    if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+                    const col = info.sheet.zh_setting.cols[info.col];
+                    if (col.field !== 'locked') return;
+
+                    const node = SpreadJsObj.getSelectObject(info.sheet);
+                    if (node._fixed) {
+                        toastr.warning('无权锁定该节点,如需锁定,请在子项尝试');
+                        return;
+                    }
+                    if (!node.locked) {
+                        const parents = info.sheet.zh_tree.getAllParents(node);
+                        if (parents.find(x => { return x.locked; })) {
+                            toastr.warning('父项已锁定,请勿在子项重复锁定');
+                            return;
+                        }
+                    }
+                    node.locked = !node.locked;
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+                });
+            } else {
+                lockSpread.bind(spreadNS.Events.ButtonClicked, function(e, info) {
+                    if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+                    const col = info.sheet.zh_setting.cols[info.col];
+                    if (col.field !== 'locked') return;
+
+                    const node = SpreadJsObj.getSelectObject(info.sheet);
+                    if (!node.locked && !node.org_locked) {
+                        toastr.warning('仅可解锁节点,不可锁定新节点');
+                        return;
+                    }
+
+                    node.locked = !node.locked;
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+                });
+            }
+
+            $('#sp-lock-ok').click(function() {
+                if (lockType === 'lock') {
+                    setting.callback(getLockedLid());
+                } else if (lockType === 'unlock') {
+                    setting.callback(getAuditLocked());
+                }
+                $('#sp-lock').modal('hide');
+            });
+            $('#sp-lock').on('shown.bs.modal', function() {
+                lockSpread.refresh();
+            });
+        };
+        const loadLockedLid = function(lid) {
+            if (!lid) return;
+            const lidList = lid.split(',');
+            lockTree.nodes.forEach(x => {
+                if (lidList.indexOf(x.ledger_id + '') >= 0) {
+                    x.locked = 1;
+                }
+            });
+        };
+        const loadAuditLocked = function(auditLocked) {
+            auditLockedHis = auditLocked;
+            const lidList = auditLockedHis.map(x => { return x.audit_locked_lid; }).join(',').split(',');
+            lockTree.nodes.forEach(x => {
+                if (lidList.indexOf(x.ledger_id + '') >= 0) {
+                    x.locked = 1;
+                    x.org_locked = 1;
+                }
+            });
+            SpreadJsObj.reloadColData(lockSheet, 0);
+        };
+        const showLocked = function(setting) {
+            lockType = 'lock';
+            init(setting);
+            loadLockedLid(setting.locked_lid);
+            $('#sp-back').modal('hide');
+            $('#sp-lock').modal('show');
+        };
+        const showUnlocked = function(setting) {
+            lockType = 'unlock';
+            init(setting);
+            loadAuditLocked(setting.auditLocked);
+            $('#sp-lock').modal('show');
+        };
+        return { showLocked, showUnlocked }
+    })();
+    $('#sp-back-lock').click(function(){
+        spBackLock.showLocked({
+            tree: stageTree,
+            locked_lid: $('[name=audit_locked_lid]').val(),
+            callback: function(lid) {
+                $('[name=audit_locked_lid]').val(lid);
+                $('#sp-back').modal('show');
+            },
+        })
+    });
+    $('#sp-unlock').click(function(){
+        postData(window.location.pathname + '/load', { filter: 'auditLocked'}, function(result){
+            if (!result.auditLocked || result.auditLocked.length === 0) {
+                toastr.warning('暂无需要解锁的数据');
+                return;
+            }
+
+            spBackLock.showUnlocked({
+                tree: stageTree,
+                auditLocked: result.auditLocked,
+                callback: async function(auditLocked) {
+                    const newAuditLocked = await postDataAsync(window.location.pathname + '/unlock', { updateType: 'audit_locked_lid', updateData: auditLocked});
+                    if (newAuditLocked.length === 0) $('#sp-unlock').hide();
+                }
+            })
+        });
+    });
 });
 function makeOneShouFang(sf) {
     const lData = _.find(ledgerData, { id: sf.lid });

+ 2 - 0
app/router.js

@@ -676,6 +676,7 @@ module.exports = app => {
     app.post('/tender/:id/advance/:order/audit/delete', sessionAuth, tenderCheck, subProjectCheck, advanceCheck, 'advanceController.deleteAudit');
     app.post('/tender/:id/advance/:order/audit/start', sessionAuth, tenderCheck, subProjectCheck, tenderBuildCheck, advanceCheck, 'advanceController.start');
     app.post('/tender/:id/advance/:order/audit/check', sessionAuth, tenderCheck, subProjectCheck, tenderBuildCheck, advanceCheck, 'advanceController.checkAudit');
+    app.post('/tender/:id/advance/:order/audit/save', sessionAuth, tenderCheck, subProjectCheck, tenderBuildCheck, advanceCheck, 'advanceController.saveAudit');
     app.post('/tender/:id/advance/:order/update', sessionAuth, tenderCheck, subProjectCheck, advanceCheck, 'advanceController.update');
     app.post('/tender/:id/advance/:order/file/upload', sessionAuth, tenderCheck, subProjectCheck, advanceCheck, 'advanceController.upload');
     app.get('/tender/:id/advance/:order/file/:fid/download', sessionAuth, tenderCheck, subProjectCheck, 'advanceController.downloadFile');
@@ -814,6 +815,7 @@ module.exports = app => {
     app.get('/tender/:id/measure/stage/:order', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.index');
     app.get('/tender/:id/stage/:order', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.index');// 针对旧数据wap端跳转web问题
     app.post('/tender/:id/measure/stage/:order/load', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.getStageData');
+    app.post('/tender/:id/measure/stage/:order/unlock', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.unlock');
     app.post('/tender/:id/measure/stage/:order/pos', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.getStagePosData');
     app.post('/tender/:id/measure/stage/:order/update', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.updateStageData');
     app.post('/tender/:id/measure/stage/:order/valid-change', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.searchValidChange');

+ 90 - 0
app/service/advance_audit.js

@@ -7,6 +7,7 @@ const pushOperate = require('../const/spec_3f').pushOperate;
 const smsTypeConst = require('../const/sms_type');
 const wxConst = require('../const/wechat_template.js');
 const advanceConst = require('../const/advance');
+const auditType = require('../const/audit').auditType;
 module.exports = app => {
     class AdvanceAudit extends app.BaseService {
         constructor(ctx) {
@@ -786,6 +787,95 @@ module.exports = app => {
         }
 
         /**
+         * 管理员修改审批流程使用的分组数据(当前预付款为单人逐级审批)。
+         */
+        async getAdminAuditGroups(vid, times) {
+            const ownerAndAuditors = await this.getAuditorsWithOwner(vid, times);
+            const owner = ownerAndAuditors[0];
+            const auditors = [owner].concat(await this.getAuditors(vid, times));
+            return auditors.map((item, index) => [{
+                ...item,
+                audit_type: auditType.key.common,
+                audit_order: index,
+            }]);
+        }
+
+        /**
+         * 管理员修改审批流程。
+         */
+        async saveAudit(advance, data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const audits = await transaction.select(this.tableName, {
+                    where: { vid: advance.id, times: advance.times },
+                    orders: [['order', 'asc'], ['id', 'asc']],
+                });
+                const current = audits.find(item => item.audit_id === Number(data.old_aid));
+                if (!current) throw '审批人不存在';
+
+                if (data.operate !== 'del') {
+                    const newId = Number(data.new_aid);
+                    if (!newId) throw '请选择审批人';
+                    if (audits.some(item => item.audit_id === newId && item.id !== current.id)) {
+                        throw '该审核人已存在,请勿重复添加';
+                    }
+                    const account = await this.ctx.service.projectAccount.getDataById(newId);
+                    if (!account || !account.enable || Number(account.project_id) !== Number(this.ctx.session.sessionProject.id)) {
+                        throw '审批人不存在或已停用';
+                    }
+
+                    if (data.operate === 'add') {
+                        if ([auditConst.status.uncheck, auditConst.status.checking].indexOf(current.status) < 0) throw '当前节点后无法新增';
+                        await this._syncOrderByDelete(transaction, advance.id, current.order + 1, advance.times, '+');
+                        await transaction.insert(this.tableName, {
+                            tid: advance.tid,
+                            vid: advance.id,
+                            type: advance.type,
+                            audit_id: newId,
+                            times: advance.times,
+                            order: current.order + 1,
+                            status: auditConst.status.uncheck,
+                        });
+                    } else if (data.operate === 'change') {
+                        if (current.status !== auditConst.status.uncheck) throw '当前审批人无法更换';
+                        await transaction.update(this.tableName, { id: current.id, audit_id: newId });
+                    } else {
+                        throw '不支持的操作';
+                    }
+                } else {
+                    if (current.status !== auditConst.status.uncheck) throw '当前审批人无法移除';
+                    await transaction.delete(this.tableName, { id: current.id });
+                    await this._syncOrderByDelete(transaction, advance.id, current.order + 1, advance.times);
+                }
+
+                // 同步固定审批配置,避免中间件再次用旧配置覆盖本次调整。
+                const shenpiStatus = this.ctx.tender.info.shenpi.advance;
+                const newAudits = await transaction.select(this.tableName, {
+                    where: { vid: advance.id, times: advance.times },
+                    orders: [['order', 'asc'], ['id', 'asc']],
+                });
+                if (shenpiStatus === shenpiConst.sp_status.gdspl) {
+                    const groups = newAudits.map((item, index) => [{
+                        ...item,
+                        audit_type: auditType.key.common,
+                        audit_order: index + 1,
+                    }]);
+                    await this.ctx.service.shenpiAudit.updateAuditListWithAuditType(
+                        transaction, this.ctx.tender.id, shenpiStatus, shenpiConst.sp_type.advance, groups, advance.sp_group || 0
+                    );
+                } else if (shenpiStatus === shenpiConst.sp_status.gdzs) {
+                    await this.ctx.service.shenpiAudit.updateAuditList(
+                        transaction, this.ctx.tender.id, shenpiStatus, shenpiConst.sp_type.advance, this._.map(newAudits, 'audit_id')
+                    );
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        /**
          * 取待审批期列表(wap用)
          *
          * @param auditorId

+ 2 - 2
app/service/change.js

@@ -2282,12 +2282,12 @@ module.exports = app => {
             return change;
         }
 
-        async checkRevising(tenderId, cid) {
+        async checkRevising(tenderId, cid, order_by) {
             let flag = false;
             // 判断修订及变更清单里是否有新增部位清单
             const lastRevise = await this.ctx.service.ledgerRevise.getLastestRevise(tenderId);
             if (lastRevise && lastRevise.status !== audit.revise.status.checked) {
-                const changeAuditList = await this.ctx.service.changeAuditList.getList(cid);
+                const changeAuditList = await this.ctx.service.changeAuditList.getList(cid, order_by);
                 const changeLedgerList = await this.ctx.service.changeLedger.getAllDataByCondition({ where: { tender_id: tenderId } });
                 const changePosList = await this.ctx.service.changePos.getAllDataByCondition({ where: { tid: tenderId } });
                 for (const cl of changeAuditList) {

+ 6 - 2
app/service/contract_col_set.js

@@ -100,8 +100,12 @@ module.exports = app => {
               const dcs = defaultColSet.find(x => x.field === field); // 取默认配置(兜底)
 
               if (csd) {
-                  // 合并:colSetDefine(name、fixed)> colSet(show、alias、顺序)> defaultColSet(兜底)
-                  result.push({ ...dcs, ...cs, ...csd });
+                  // 列定义提供名称、类型等元数据;显示状态和别名必须以已保存配置为准。
+                  // 附加属性定义本身也带有 show/alias,不能让它覆盖用户的列设置。
+                  const item = { ...dcs, ...cs, ...csd };
+                  if (Object.prototype.hasOwnProperty.call(cs, 'show')) item.show = cs.show;
+                  if (Object.prototype.hasOwnProperty.call(cs, 'alias')) item.alias = cs.alias;
+                  result.push(item);
               }
               colSetFieldSet.add(field); // 标记该字段已处理
             }

Plik diff jest za duży
+ 19 - 31
app/service/contract_sp_audit.js


+ 3 - 1
app/service/material.js

@@ -372,7 +372,9 @@ module.exports = app => {
                     const sql2 = 'UPDATE ' + this.ctx.service.materialExponent.tableName + ' as me, ' +
                         this.ctx.service.materialExponentHistory.tableName + ' as meh ' +
                         'SET me.`weight_num` = meh.`weight_num`, me.`basic_price` = meh.`basic_price`, ' +
-                        'me.`basic_times` = meh.`basic_times`, me.`m_price` = meh.`m_price`, ' +
+                        'me.`basic_times` = meh.`basic_times`, me.`m_up_risk` = meh.`m_up_risk`, ' +
+                        'me.`m_down_risk` = meh.`m_down_risk`, me.`m_price` = meh.`m_price`, ' +
+                        'me.`effective_price` = meh.`effective_price`, ' +
                         'me.`calc_num` = meh.`calc_num`, me.`is_summary` = meh.`is_summary` ' +
                         'WHERE meh.`tid` = ? AND meh.`order` = ? AND meh.`me_id` = me.`id`';
                     const sqlParam2 = [this.ctx.tender.id, materialInfo.order - 1];

+ 3 - 0
app/service/material_audit.js

@@ -512,7 +512,10 @@ module.exports = app => {
                                 weight_num: me.weight_num,
                                 basic_price: me.basic_price,
                                 basic_times: me.basic_times,
+                                m_up_risk: me.m_up_risk,
+                                m_down_risk: me.m_down_risk,
                                 m_price: me.m_price,
+                                effective_price: me.effective_price !== null ? me.effective_price : me.m_price,
                                 calc_num: me.calc_num,
                                 is_summary: me.is_summary,
                             };

+ 224 - 40
app/service/material_exponent.js

@@ -11,6 +11,7 @@
 const auditConst = require('../const/audit').material;
 const materialConst = require('../const/material');
 const MaterialCalculator = require('../lib/material_calc');
+const Decimal = require('decimal.js');
 
 module.exports = app => {
     class MaterialExponent extends app.BaseService {
@@ -25,6 +26,69 @@ module.exports = app => {
             this.tableName = 'material_exponent';
         }
 
+        _normalizeRisk(value) {
+            if (value === null || value === undefined || value === '') return 0;
+            const risk = Number(value);
+            if (!Number.isInteger(risk) || risk < 0 || risk > 100) {
+                throw '上涨、下跌幅度必须为0~100的整数';
+            }
+            return risk;
+        }
+
+        getEffectivePrice(data, decimal = (this.ctx.material.exponent_decimal ? this.ctx.material.exponent_decimal.up : materialConst.exponent_decimal.up)) {
+            if (data.m_price === null || data.m_price === undefined || data.m_price === '') return null;
+
+            const currentPrice = new Decimal(data.m_price);
+            const upRisk = this._normalizeRisk(data.m_up_risk);
+            const downRisk = this._normalizeRisk(data.m_down_risk);
+            let effectivePrice = currentPrice;
+
+            if ((upRisk !== 0 || downRisk !== 0) && data.basic_price !== null && data.basic_price !== undefined && data.basic_price !== '' && new Decimal(data.basic_price).gt(0)) {
+                const basicPrice = new Decimal(data.basic_price);
+                const upperLimit = basicPrice.mul(new Decimal(1).add(new Decimal(upRisk).div(100)));
+                const lowerLimit = basicPrice.mul(new Decimal(1).sub(new Decimal(downRisk).div(100)));
+
+                if (currentPrice.gt(upperLimit)) {
+                    effectivePrice = currentPrice.sub(basicPrice.mul(upRisk).div(100));
+                } else if (currentPrice.lt(lowerLimit)) {
+                    effectivePrice = currentPrice.add(basicPrice.mul(downRisk).div(100));
+                } else {
+                    effectivePrice = basicPrice;
+                }
+            }
+
+            return Number(effectivePrice.toDecimalPlaces(Number(decimal), Decimal.ROUND_HALF_UP).toString());
+        }
+
+        _normalizeUpdateData(data) {
+            const editableCols = [
+                'id', 'symbol', 'symbol_desc', 'code', 'basic_price', 'basic_times',
+                'm_up_risk', 'm_down_risk', 'm_price', 'weight_num', 'remark', 'is_summary',
+            ];
+            const updateData = this._.pick(data, editableCols);
+            if (!updateData.id) throw '指数工料参数有误';
+            if (updateData.m_up_risk !== undefined) updateData.m_up_risk = this._normalizeRisk(updateData.m_up_risk);
+            if (updateData.m_down_risk !== undefined) updateData.m_down_risk = this._normalizeRisk(updateData.m_down_risk);
+            return updateData;
+        }
+
+        _setEffectivePrice(currentData, updateData) {
+            const priceCols = ['basic_price', 'm_price', 'm_up_risk', 'm_down_risk'];
+            const needUpdatePrice = priceCols.some(col => updateData[col] !== undefined);
+            const needUpdateCalc = needUpdatePrice || updateData.weight_num !== undefined;
+            if (!needUpdateCalc) return updateData;
+            const calcData = Object.assign({}, currentData, updateData);
+            if (needUpdatePrice) {
+                updateData.effective_price = this.getEffectivePrice(calcData);
+                calcData.effective_price = updateData.effective_price;
+            }
+            const effectivePrice = calcData.effective_price !== null && calcData.effective_price !== undefined ? calcData.effective_price : calcData.m_price;
+            const calcNum = calcData.type === materialConst.ex_type[1].value && calcData.basic_price > 0 ?
+                this.ctx.helper.mul(calcData.weight_num, this.ctx.helper.div(effectivePrice, calcData.basic_price)) : null;
+            updateData.calc_num = calcNum ? this.ctx.helper.round(calcNum, this.ctx.material.exponent_decimal.qty) : calcNum;
+            return updateData;
+        }
+
         /**
          * 添加指数清单
          * @return {void}
@@ -156,7 +220,7 @@ module.exports = app => {
             if (!this.ctx.tender || !this.ctx.material) {
                 throw '数据错误';
             }
-            delete data.in_time;
+            data = this._normalizeUpdateData(data);
             // delete data.m_tp;
             // 判断是否可修改
             // 判断t_type是否为费用
@@ -165,10 +229,15 @@ module.exports = app => {
                 const result = {};
                 const info = {};
                 switch (this.ctx.material.exponentStatus) {
-                    case materialConst.exponent_status.shared_noNode:
+                    case materialConst.exponent_status.shared_noNode: {
+                        const exponentInfo = await transaction.get(this.tableName, { id: data.id, tid: this.ctx.tender.id });
+                        if (!exponentInfo) throw '指数工料数据不存在';
+                        this._setEffectivePrice(exponentInfo, data);
                         await transaction.update(this.tableName, data);
                         [result.ex_tp, result.ex_tax_tp, result.ex_expr] = await this.calcMaterialExTp(transaction);
+                        result.exponentData = await transaction.select(this.tableName, { where: { id: data.id } });
                         break;
+                    }
                     case materialConst.exponent_status.shared_node:
                         if (!mn_id) {
                             throw '分项参数有误';
@@ -215,6 +284,9 @@ module.exports = app => {
                         break;
                     default: break;
                 }
+                if (this.ctx.material.order < this.ctx.material.highOrder && this.ctx.session.sessionUser.is_admin) {
+                    await this._syncHistoricalTotals(transaction);
+                }
                 await transaction.commit();
                 return result;
             } catch (err) {
@@ -248,6 +320,9 @@ module.exports = app => {
                 updateData.id = data.id;
                 await transaction.update(this.tableName, updateData);
             }
+            if (updateData.basic_price !== undefined) {
+                await this.ctx.service.materialExponentShard.resetEffectivePrice(transaction, [data.id]);
+            }
             if (needCalc) {
                 if (mn_id) {
                     await this.ctx.service.materialExponentShard.calcMaterialAllExTpByNode(transaction, this.ctx.material.id, mn_id);
@@ -271,17 +346,25 @@ module.exports = app => {
             // 判断t_type是否为费用
             const transaction = await this.db.beginTransaction();
             try {
+                datas = datas.map(data => this._normalizeUpdateData(data));
                 const result = { ex_tp: this.ctx.material.ex_tp };
                 const info = {};
                 if (this.ctx.material.exponentStatus !== materialConst.exponent_status.shared_noNode) {
                     info.meList = await transaction.select(this.tableName, { where: { id: this._.map(datas, 'id') } });
                 }
                 switch (this.ctx.material.exponentStatus) {
-                    case materialConst.exponent_status.shared_noNode:
+                    case materialConst.exponent_status.shared_noNode: {
+                        const exponentList = await transaction.select(this.tableName, { where: { id: this._.map(datas, 'id'), tid: this.ctx.tender.id } });
+                        for (const data of datas) {
+                            const exponentInfo = this._.find(exponentList, { id: data.id });
+                            if (!exponentInfo) throw '指数工料数据不存在';
+                            this._setEffectivePrice(exponentInfo, data);
+                        }
                         await transaction.updateRows(this.tableName, datas);
                         [result.ex_tp, result.ex_tax_tp, result.ex_expr] = await this.calcMaterialExTp(transaction);
                         result.exponentData = await transaction.select(this.tableName, { where: { id: this._.map(datas, 'id') } });
                         break;
+                    }
                     case materialConst.exponent_status.shared_node:
                         if (!mn_id) {
                             throw '参数有误';
@@ -316,6 +399,9 @@ module.exports = app => {
                         break;
                     default: break;
                 }
+                if (this.ctx.material.order < this.ctx.material.highOrder && this.ctx.session.sessionUser.is_admin) {
+                    await this._syncHistoricalTotals(transaction);
+                }
                 await transaction.commit();
                 return result;
             } catch (err) {
@@ -324,6 +410,105 @@ module.exports = app => {
             }
         }
 
+        async saveHistoryRisks(datas) {
+            if (!this.ctx.session.sessionUser.is_admin || this.ctx.material.order >= this.ctx.material.highOrder) {
+                throw '无权修改往期指数幅度';
+            }
+            const updateList = datas instanceof Array ? datas : [datas];
+            const transaction = await this.db.beginTransaction();
+            try {
+                const historyIds = this._.map(updateList, 'id');
+                const historyList = await transaction.select(this.ctx.service.materialExponentHistory.tableName, {
+                    where: { id: historyIds, mid: this.ctx.material.id, order: this.ctx.material.order },
+                });
+                const updates = [];
+                for (const data of updateList) {
+                    const historyInfo = this._.find(historyList, { id: data.id });
+                    if (!historyInfo || historyInfo.type !== materialConst.ex_type[1].value) throw '往期指数工料数据不存在';
+                    const updateData = { id: historyInfo.id };
+                    if (data.m_up_risk !== undefined) updateData.m_up_risk = this._normalizeRisk(data.m_up_risk);
+                    if (data.m_down_risk !== undefined) updateData.m_down_risk = this._normalizeRisk(data.m_down_risk);
+                    if (Object.keys(updateData).length === 1) continue;
+                    updateData.effective_price = this.getEffectivePrice(Object.assign({}, historyInfo, updateData));
+                    const calcNum = historyInfo.basic_price > 0 ? this.ctx.helper.mul(historyInfo.weight_num, this.ctx.helper.div(updateData.effective_price, historyInfo.basic_price)) : 0;
+                    updateData.calc_num = calcNum ? this.ctx.helper.round(calcNum, this.ctx.material.exponent_decimal.qty) : calcNum;
+                    updates.push(updateData);
+                }
+                if (updates.length === 0) throw '上涨、下跌幅度未发生变化';
+                await transaction.updateRows(this.ctx.service.materialExponentHistory.tableName, updates);
+                const [ex_tp, ex_tax_tp, ex_expr] = await this._calcHistoryExTp(transaction);
+                await this._syncHistoricalTotals(transaction);
+                const historyData = await transaction.select(this.ctx.service.materialExponentHistory.tableName, { where: { id: this._.map(updates, 'id') } });
+                await transaction.commit();
+                return { ex_tp, ex_tax_tp, ex_expr, historyData };
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async _calcHistoryExTp(transaction) {
+            let basicCalc = 0;
+            const exCalc = this.ctx.material.ex_calc ? JSON.parse(this.ctx.material.ex_calc) : null;
+            if (exCalc) {
+                for (const calc of exCalc) {
+                    if (!calc.select) continue;
+                    basicCalc = this.ctx.helper.add(basicCalc, calc.code === 'zdy' && calc.result ? calc.result : calc.value);
+                }
+            }
+            const exponentList = await transaction.select(this.ctx.service.materialExponentHistory.tableName, {
+                where: { mid: this.ctx.material.id, order: this.ctx.material.order, is_summary: materialConst.is_summary.yes },
+            });
+            let expr = basicCalc + '*[';
+            let sumByChange = 0;
+            let constant = 0;
+            for (const ex of exponentList) {
+                if (ex.type === materialConst.ex_type[0].value) {
+                    constant = ex.weight_num || 0;
+                    expr += constant.toString();
+                } else if (ex.type === materialConst.ex_type[1].value) {
+                    const effectivePrice = ex.effective_price !== null && ex.effective_price !== undefined ? ex.effective_price : ex.m_price;
+                    const calcNum = ex.basic_price > 0 ? this.ctx.helper.mul(ex.weight_num, this.ctx.helper.div(effectivePrice, ex.basic_price)) : 0;
+                    const change = calcNum ? this.ctx.helper.round(calcNum, this.ctx.material.exponent_decimal.qty) : 0;
+                    expr += change !== 0 ? '+' + ex.weight_num.toString() + '*(' + effectivePrice.toString() + '/' + ex.basic_price.toString() + ')' : '';
+                    sumByChange = this.ctx.helper.add(sumByChange, change);
+                }
+            }
+            expr += '-1]';
+            expr = constant !== 0 ? expr : null;
+            const exTp = constant !== 0 ? this.ctx.helper.round(this.ctx.helper.mul(basicCalc, this.ctx.helper.sub(this.ctx.helper.add(constant, sumByChange), 1)), this.ctx.material.exponent_decimal.tp) : null;
+            const exTaxTp = exTp !== null ? this.ctx.helper.round(this.ctx.helper.mul(exTp, 1 + this.ctx.material.exponent_rate / 100), this.ctx.material.exponent_decimal.tp) : null;
+            await transaction.update(this.ctx.service.material.tableName, {
+                id: this.ctx.material.id,
+                ex_tp: exTp,
+                ex_tax_tp: exTaxTp,
+                ex_expr: expr,
+            });
+            return [exTp, exTaxTp, expr];
+        }
+
+        async _syncHistoricalTotals(transaction) {
+            const materialList = await transaction.select(this.ctx.service.material.tableName, {
+                where: { tid: this.ctx.tender.id },
+                orders: [['order', 'asc']],
+            });
+            let exponentPreTp = 0;
+            let exponentTaxPreTp = 0;
+            const updates = [];
+            for (const material of materialList) {
+                updates.push({
+                    id: material.id,
+                    ex_pre_tp: exponentPreTp,
+                    ex_tax_pre_tp: exponentTaxPreTp,
+                });
+                exponentPreTp = this.ctx.helper.add(exponentPreTp, material.ex_tp || 0);
+                exponentTaxPreTp = this.ctx.helper.add(exponentTaxPreTp, material.ex_tax_tp || 0);
+            }
+            if (updates.length > 0) await transaction.updateRows(this.ctx.service.material.tableName, updates);
+            const tpData = await this.ctx.service.materialAudit.getTpData(transaction, this.ctx.material.id, this.ctx.material.decimal, this.ctx.material.exponent_decimal);
+            await transaction.update(this.ctx.service.material.tableName, { id: this.ctx.material.id, tp_data: JSON.stringify(tpData) });
+        }
+
         async _updateMoreExponentData(transaction, datas, result, meList, ms_id = null, mn_id = null) {
             // 判断data值是否需要更新materialExponent和materialExponentShard
             const mesCondition = { me_id: this._.map(datas, 'id') };
@@ -331,8 +516,8 @@ module.exports = app => {
             if (mn_id !== null) mesCondition.mn_id = mn_id;
             const mesList = await transaction.select(this.ctx.service.materialExponentShard.tableName, { where: mesCondition });
             const materialExponentColArray = ['symbol', 'symbol_desc', 'code', 'basic_price', 'is_summary', 'basic_times'];
-            const materialExponentShardColArray = ['weight_num', 'm_price', 'remark'];
-            const updateCalcArray = ['weight_num', 'basic_price', 'm_price', 'is_summary'];
+            const materialExponentShardColArray = ['weight_num', 'm_up_risk', 'm_down_risk', 'm_price', 'remark'];
+            const updateCalcArray = ['weight_num', 'basic_price', 'm_up_risk', 'm_down_risk', 'm_price', 'is_summary'];
             const updateMeArray = [];
             const udpateMseArray = [];
             let needCalc = false;
@@ -377,9 +562,15 @@ module.exports = app => {
             }
             if (udpateMseArray.length > 0) {
                 await transaction.updateRows(this.ctx.service.materialExponentShard.tableName, udpateMseArray);
+                await this.ctx.service.materialExponentShard.resetEffectivePrice(transaction, this._.uniq(this._.map(udpateMseArray, 'id')), true);
                 // const condition = mn_id ? { mid: this.ctx.material.id, mn_id } : { mid: this.ctx.material.id, ms_id };
                 result.exponentShardData = await transaction.select(this.ctx.service.materialExponentShard.tableName, { where: { id: this._.map(udpateMseArray, 'id') } });
             }
+            const basicPriceMeIds = this._.map(this._.filter(updateMeArray, data => data.basic_price !== undefined), 'id');
+            if (basicPriceMeIds.length > 0) {
+                await this.ctx.service.materialExponentShard.resetEffectivePrice(transaction, basicPriceMeIds);
+                result.exponentShardData = await transaction.select(this.ctx.service.materialExponentShard.tableName, { where: { mid: this.ctx.material.id, me_id: basicPriceMeIds } });
+            }
             return needCalc;
         }
 
@@ -495,10 +686,11 @@ module.exports = app => {
                     constant = ex.weight_num ? ex.weight_num : 0;
                     expr += constant.toString();
                 } else if (ex.type === materialConst.ex_type[1].value) {
-                    const calc_num = ex.basic_price > 0 ? this.ctx.helper.mul(ex.weight_num, this.ctx.helper.div(ex.m_price, ex.basic_price)) : 0;
+                    const effectivePrice = ex.effective_price !== null && ex.effective_price !== undefined ? ex.effective_price : ex.m_price;
+                    const calc_num = ex.basic_price > 0 ? this.ctx.helper.mul(ex.weight_num, this.ctx.helper.div(effectivePrice, ex.basic_price)) : 0;
                     const change = calc_num ? this.ctx.helper.round(calc_num, decimal.qty) : 0;
                     // const change = ex.calc_num ? ex.calc_num : 0;
-                    expr = expr + (change !== 0 ? '+' + ex.weight_num.toString() + '*(' + ex.m_price.toString() + '/' + ex.basic_price.toString() + ')' : '');
+                    expr = expr + (change !== 0 ? '+' + ex.weight_num.toString() + '*(' + effectivePrice.toString() + '/' + ex.basic_price.toString() + ')' : '');
                     sumByChange = this.ctx.helper.add(sumByChange, change);
                 }
             }
@@ -596,45 +788,37 @@ module.exports = app => {
             const updateArray = [];
             const updateStageArray = [];
             const upColArray = ['basic_price', 'm_price'];
-            // const qtyColArray = ['calc_num'];
             for (const ex of exponentList) {
+                let updateEx = null;
                 for (const col of upColArray) {
-                    if (ex[col] && ex[col] !== this.ctx.helper.round(ex[col], decimal.up)) {
-                        const upex = this._.find(updateArray, { id: ex.id });
-                        if (upex) {
-                            upex[col] = this.ctx.helper.round(ex[col], decimal.up);
-                        } else {
-                            const insertEx = {
-                                id: ex.id,
-                            };
-                            insertEx[col] = this.ctx.helper.round(ex[col], decimal.up);
-                            updateArray.push(insertEx);
-                        }
+                    if (ex[col] !== null && ex[col] !== undefined && ex[col] !== this.ctx.helper.round(ex[col], decimal.up)) {
+                        updateEx = updateEx || { id: ex.id };
+                        updateEx[col] = this.ctx.helper.round(ex[col], decimal.up);
                     }
                 }
-                // for (const col of qtyColArray) {
-                //     if (ex[col] && ex[col] !== this.ctx.helper.round(ex[col], decimal.qty)) {
-                //         const upex = this._.find(updateArray, { id: ex.id });
-                //         if (upex) {
-                //             upex[col] = this.ctx.helper.round(ex[col], decimal.qty);
-                //         } else {
-                //             const insertEx = {
-                //                 id: ex.id,
-                //             };
-                //             insertEx[col] = this.ctx.helper.round(ex[col], decimal.qty);
-                //             updateArray.push(insertEx);
-                //         }
-                //     }
-                // }
+                const effectivePrice = this.getEffectivePrice(Object.assign({}, ex, updateEx || {}), decimal.up);
+                if (effectivePrice !== ex.effective_price) {
+                    updateEx = updateEx || { id: ex.id };
+                    updateEx.effective_price = effectivePrice;
+                }
+                if (ex.type === materialConst.ex_type[1].value) {
+                    const calcData = Object.assign({}, ex, updateEx || {});
+                    const calcNum = calcData.basic_price > 0 ? this.ctx.helper.mul(calcData.weight_num, this.ctx.helper.div(effectivePrice, calcData.basic_price)) : 0;
+                    const roundedCalcNum = calcNum ? this.ctx.helper.round(calcNum, decimal.qty) : calcNum;
+                    if (roundedCalcNum !== ex.calc_num) {
+                        updateEx = updateEx || { id: ex.id };
+                        updateEx.calc_num = roundedCalcNum;
+                    }
+                }
+                if (updateEx) updateArray.push(updateEx);
             }
             for (const ex of exponentShardList) {
-                if (ex.m_price && ex.m_price !== this.ctx.helper.round(ex.m_price, decimal.up)) {
-                    const insertEx = {
-                        id: ex.id,
-                    };
-                    insertEx.m_price = this.ctx.helper.round(ex.m_price, decimal.up);
-                    updateStageArray.push(insertEx);
-                }
+                const materialExponent = this._.find(exponentList, { id: ex.me_id });
+                if (!materialExponent) continue;
+                const updateEx = { id: ex.id };
+                if (ex.m_price !== null && ex.m_price !== undefined) updateEx.m_price = this.ctx.helper.round(ex.m_price, decimal.up);
+                updateEx.effective_price = this.getEffectivePrice(Object.assign({}, materialExponent, ex, updateEx), decimal.up);
+                if (updateEx.m_price !== ex.m_price || updateEx.effective_price !== ex.effective_price) updateStageArray.push(updateEx);
             }
             if (updateArray.length > 0) await transaction.updateRows(this.tableName, updateArray);
             if (updateStageArray.length > 0) await transaction.updateRows(this.ctx.service.materialExponentShard.tableName, updateStageArray);

+ 14 - 3
app/service/material_exponent_node.js

@@ -86,7 +86,8 @@ module.exports = app => {
                             for (const node of newNodeDatas) {
                                 const ms = this._.find(msList, { id: node.ms_id });
                                 for (const ex of exponentList) {
-                                    const exponentShardOlds = this._.filter(oldMsShardList, { ms_id: ms.id, me_id: ex.id });
+                                    const exponentShardOld = this._.find(oldMsShardList, { ms_id: ms.id, me_id: ex.id });
+                                    const sourceData = !this.ctx.material.exponent_node && exponentShardOld ? exponentShardOld : {};
                                     insertExponentShardDatas.push({
                                         tid: this.ctx.tender.id,
                                         mid: this.ctx.material.id,
@@ -94,7 +95,10 @@ module.exports = app => {
                                         mn_id: node.id,
                                         me_id: ex.id,
                                         type: ex.type,
-                                        m_price: !this.ctx.material.exponent_node ? exponentShardOlds.m_price : 0,
+                                        m_up_risk: sourceData.m_up_risk || 0,
+                                        m_down_risk: sourceData.m_down_risk || 0,
+                                        m_price: sourceData.m_price || 0,
+                                        effective_price: sourceData.effective_price !== null && sourceData.effective_price !== undefined ? sourceData.effective_price : (sourceData.m_price || 0),
                                         remark: ex.remark,
                                     });
                                 }
@@ -102,6 +106,7 @@ module.exports = app => {
                         } else {
                             for (const node of newNodeDatas) {
                                 for (const ex of exponentList) {
+                                    const sourceData = !this.ctx.material.exponent_node ? ex : {};
                                     insertExponentShardDatas.push({
                                         tid: this.ctx.tender.id,
                                         mid: this.ctx.material.id,
@@ -109,7 +114,10 @@ module.exports = app => {
                                         mn_id: node.id,
                                         me_id: ex.id,
                                         type: ex.type,
-                                        m_price: !this.ctx.material.exponent_node ? ex.m_price : 0,
+                                        m_up_risk: sourceData.m_up_risk || 0,
+                                        m_down_risk: sourceData.m_down_risk || 0,
+                                        m_price: sourceData.m_price || 0,
+                                        effective_price: sourceData.effective_price !== null && sourceData.effective_price !== undefined ? sourceData.effective_price : (sourceData.m_price || 0),
                                         remark: ex.remark,
                                     });
                                 }
@@ -149,7 +157,10 @@ module.exports = app => {
                                     mn_id: null,
                                     me_id: ex.id,
                                     type: ex.type,
+                                    m_up_risk: 0,
+                                    m_down_risk: 0,
                                     m_price: 0,
+                                    effective_price: 0,
                                     remark: ex.remark,
                                 });
                             }

+ 65 - 9
app/service/material_exponent_shard.js

@@ -12,7 +12,7 @@ const auditConst = require('../const/audit').material;
 const materialConst = require('../const/material');
 const MaterialCalculator = require('../lib/material_calc');
 
-const needUpdateArray = ['weight_num', 'm_price', 'remark'];
+const needUpdateArray = ['weight_num', 'm_up_risk', 'm_down_risk', 'm_price', 'remark'];
 
 module.exports = app => {
     class MaterialExponentShard extends app.BaseService {
@@ -70,15 +70,21 @@ module.exports = app => {
         // 单个修改工料时同步修改
         async update(transaction, data, ms_id = null, mn_id = null, needCalc = false) {
             const mesInfo = await transaction.get(this.tableName, { tid: this.ctx.tender.id, ms_id, mn_id, me_id: data.id });
+            if (!mesInfo) throw '指数分表数据不存在';
             const updateData = {
                 id: mesInfo.id,
             };
             for (const nu of needUpdateArray) {
                 if (data[nu] !== undefined && mesInfo[nu] !== data[nu]) updateData[nu] = data[nu];
             }
+            if (updateData.m_price !== undefined || updateData.m_up_risk !== undefined || updateData.m_down_risk !== undefined || needCalc) {
+                const exponentInfo = await this._getExponentInfo(transaction, data.id);
+                if (!exponentInfo) throw '指数工料数据不存在';
+                updateData.effective_price = this.ctx.service.materialExponent.getEffectivePrice(Object.assign({}, exponentInfo, mesInfo, updateData));
+            }
             // 如果只有id,则不更新
             if (Object.keys(updateData).length > 1) await transaction.update(this.tableName, updateData);
-            if (updateData.m_price !== undefined || updateData.weight_num !== undefined || needCalc) {
+            if (updateData.m_price !== undefined || updateData.m_up_risk !== undefined || updateData.m_down_risk !== undefined || updateData.weight_num !== undefined || needCalc) {
                 const mnInfo = mn_id ? await transaction.get(this.ctx.service.materialExponentNode.tableName, { mid: this.ctx.material.id, id: mn_id }) : null;
                 const msInfo = ms_id && !mn_id ? await transaction.get(this.ctx.service.materialStage.tableName, { mid: this.ctx.material.id, id: ms_id }) : null;
                 const ex_calc = mnInfo ? JSON.parse(mnInfo.ex_calc) : msInfo ? JSON.parse(msInfo.ex_calc) : materialConst.ex_calc;
@@ -87,9 +93,50 @@ module.exports = app => {
             return true;
         }
 
+        async _getExponentInfo(transaction, meId) {
+            if (this.ctx.material.order < this.ctx.material.highOrder) {
+                const historyInfo = await transaction.get(this.ctx.service.materialExponentHistory.tableName, {
+                    mid: this.ctx.material.id,
+                    order: this.ctx.material.order,
+                    me_id: meId,
+                });
+                if (historyInfo) return Object.assign({}, historyInfo, { id: historyInfo.me_id });
+            }
+            return await transaction.get(this.ctx.service.materialExponent.tableName, { id: meId, tid: this.ctx.tender.id });
+        }
+
+        async _getExponentList(transaction, mid, summaryOnly = true) {
+            if (mid === this.ctx.material.id && this.ctx.material.order < this.ctx.material.highOrder) {
+                const historyCondition = { mid, order: this.ctx.material.order };
+                if (summaryOnly) historyCondition.is_summary = materialConst.is_summary.yes;
+                const historyList = await transaction.select(this.ctx.service.materialExponentHistory.tableName, { where: historyCondition });
+                if (historyList.length > 0) return historyList.map(data => Object.assign({}, data, { id: data.me_id }));
+            }
+            const exponentCondition = { tid: this.ctx.tender.id };
+            if (summaryOnly) exponentCondition.is_summary = materialConst.is_summary.yes;
+            return await transaction.select(this.ctx.service.materialExponent.tableName, { where: exponentCondition });
+        }
+
+        async resetEffectivePrice(transaction, ids, byShardId = false) {
+            if (!ids || ids.length === 0) return [];
+            const condition = { mid: this.ctx.material.id };
+            condition[byShardId ? 'id' : 'me_id'] = ids;
+            const shardList = await transaction.select(this.tableName, { where: condition });
+            if (shardList.length === 0) return [];
+            const updates = [];
+            for (const shard of shardList) {
+                const exponentInfo = await this._getExponentInfo(transaction, shard.me_id);
+                if (!exponentInfo) continue;
+                const effectivePrice = this.ctx.service.materialExponent.getEffectivePrice(Object.assign({}, exponentInfo, shard));
+                if (effectivePrice !== shard.effective_price) updates.push({ id: shard.id, effective_price: effectivePrice });
+            }
+            if (updates.length > 0) await transaction.updateRows(this.tableName, updates);
+            return updates;
+        }
+
         async calcMaterialAllExTpByStage(transaction, mid, extra_ms_id = null, decimal = this.ctx.material.exponent_decimal ? this.ctx.material.exponent_decimal : materialConst.exponent_decimal, rate = this.ctx.material.exponent_rate) {
             const msList = await this.ctx.service.materialStage.getAllDataByCondition({ where: { mid } });
-            const exponentList = await transaction.select(this.ctx.service.materialExponent.tableName, { where: { tid: this.ctx.tender.id, is_summary: materialConst.is_summary.yes } });
+            const exponentList = await this._getExponentList(transaction, mid);
             const allStageExponentList = await transaction.select(this.tableName, { where: { mid } });
             const updateStages = [];
             for (const ms of msList) {
@@ -107,7 +154,7 @@ module.exports = app => {
 
         async calcMaterialAllExTpByNode(transaction, mid, extra_mn_id = null, needCalcStage = true, decimal = this.ctx.material.exponent_decimal ? this.ctx.material.exponent_decimal : materialConst.exponent_decimal, rate = this.ctx.material.exponent_rate) {
             const mnList = await transaction.select(this.ctx.service.materialExponentNode.tableName, { where: { mid } });
-            const exponentList = await transaction.select(this.ctx.service.materialExponent.tableName, { where: { tid: this.ctx.tender.id, is_summary: materialConst.is_summary.yes } });
+            const exponentList = await this._getExponentList(transaction, mid);
             const allExponentShardList = await transaction.select(this.tableName, { where: { mid } });
             const updateNodes = [];
             for (const mn of mnList) {
@@ -165,17 +212,20 @@ module.exports = app => {
             if (msid !== null) condition.ms_id = msid;
             if (mnid !== null) condition.mn_id = mnid;
             const stageExponentList = this._.filter(allStageExponentList, condition);
+            const inheritWeight = !this._.some(exponentList, ex => ex.type === materialConst.ex_type[0].value && this._.some(stageExponentList, { me_id: ex.id }));
             let sumByChange = 0;
             let constant = 0;
             for (const ex of exponentList) {
-                const stageEx = this._.find(stageExponentList, { me_id: ex.id });
+                const stageEx = this._.find(stageExponentList, { me_id: ex.id }) || ex;
+                const weightNum = stageEx.weight_num !== null && stageEx.weight_num !== undefined ? stageEx.weight_num : (inheritWeight ? (ex.weight_num || 0) : 0);
                 if (ex.type === materialConst.ex_type[0].value) {
-                    constant = stageEx.weight_num ? stageEx.weight_num : 0;
+                    constant = weightNum;
                     expr += constant.toString();
                 } else if (ex.type === materialConst.ex_type[1].value) {
-                    const calc_num = ex.basic_price > 0 ? this.ctx.helper.mul(stageEx.weight_num, this.ctx.helper.div(stageEx.m_price, ex.basic_price)) : 0;
+                    const effectivePrice = stageEx.effective_price !== null && stageEx.effective_price !== undefined ? stageEx.effective_price : stageEx.m_price;
+                    const calc_num = ex.basic_price > 0 ? this.ctx.helper.mul(weightNum, this.ctx.helper.div(effectivePrice, ex.basic_price)) : 0;
                     const change = calc_num ? this.ctx.helper.round(calc_num, decimal.qty) : 0;
-                    expr = expr + (change !== 0 ? '+' + stageEx.weight_num.toString() + '*(' + stageEx.m_price.toString() + '/' + ex.basic_price.toString() + ')' : '');
+                    expr = expr + (change !== 0 ? '+' + weightNum.toString() + '*(' + effectivePrice.toString() + '/' + ex.basic_price.toString() + ')' : '');
                     sumByChange = this.ctx.helper.add(sumByChange, change);
                 }
             }
@@ -189,7 +239,7 @@ module.exports = app => {
         // 更改计算总指数金额并返回值和公式
         async calcMaterialExTp(transaction, ex_calc, mid, msid = null, mnid = null, decimal = this.ctx.material.exponent_decimal, rate = this.ctx.material.exponent_rate, needCalcStage = true) {
             if (!msid && !mnid) throw '参数有误';
-            const exponentList = await transaction.select(this.ctx.service.materialExponent.tableName, { where: { tid: this.ctx.tender.id, is_summary: materialConst.is_summary.yes } });
+            const exponentList = await this._getExponentList(transaction, mid);
             const condition = {};
             if (msid !== null) condition.ms_id = msid;
             if (mnid !== null) condition.mn_id = mnid;
@@ -272,7 +322,10 @@ module.exports = app => {
                         me_id: me.id,
                         type: me.type,
                         weight_num: me.weight_num,
+                        m_up_risk: me.m_up_risk,
+                        m_down_risk: me.m_down_risk,
                         m_price: me.m_price,
+                        effective_price: me.effective_price !== null ? me.effective_price : me.m_price,
                         remark: me.remark,
                     };
                     newExponentList.push(newExponent);
@@ -301,7 +354,10 @@ module.exports = app => {
                         ms_id: ms.id,
                         type: e.type,
                         weight_num: e.weight_num,
+                        m_up_risk: e.m_up_risk,
+                        m_down_risk: e.m_down_risk,
                         m_price: e.m_price,
+                        effective_price: e.effective_price !== null ? e.effective_price : e.m_price,
                     };
                     insertStageExponentData.push(oneStageExponentData);
                 }

+ 2 - 2
app/service/project_col_set.js

@@ -54,8 +54,8 @@ module.exports = app => {
         async getProjectColSet(pid, spid) {
             const curSet = await this.loadProjectColSet(pid, spid);
             if (curSet) return curSet;
-            await this.initProjectColSet(pid, spid, colSetType);
-            return await this.loadProjectColSet(pid, spid, colSetType);
+            await this.initProjectColSet(pid, spid);
+            return await this.loadProjectColSet(pid, spid);
         }
 
         async setProjectColSet(pid, spid, colSetType, colSet) {

+ 25 - 5
app/service/stage_audit.js

@@ -471,18 +471,19 @@ module.exports = app => {
                     status: checkData.checkType,
                     opinion: checkData.opinion,
                     end_time: time,
+                    audit_locked_lid: '',
                 }];
                 if (selfAudit.audit_type === auditType.key.multi || (selfAudit.audit_type === auditType.key.and && selfAudit.audit_group_order)) {
                     const skipAudit = flowAudits.filter(x => {
                         if (!selfAudit.audit_group_order || x.id === selfAudit.id) return false;
                         return x.status === auditConst.status.checking && x.audit_group === selfAudit.audit_group && x.audit_group_order <= selfAudit.audit_group_order;
                     });
-                    if (skipAudit.length > 0) skipAudit.forEach(x => { auditUpdateData.push({ id: x.id, status: auditConst.status.checkSkip, opinion: '', end_time: time }); });
+                    if (skipAudit.length > 0) skipAudit.forEach(x => { auditUpdateData.push({ id: x.id, status: auditConst.status.checkSkip, opinion: '', end_time: time, audit_locked_lid: '' }); });
                 } else if (selfAudit.audit_type === auditType.key.or) {
                     if (flowAudits.length - audits.length + 1 >= selfAudit.audit_group_need) {
                         for (const audit of audits) {
                             if (audit.aid === selfAudit.aid) continue;
-                            auditUpdateData.push({ id: audit.id, status: auditConst.status.checkSkip, opinion: '', end_time: time });
+                            auditUpdateData.push({ id: audit.id, status: auditConst.status.checkSkip, opinion: '', end_time: time, audit_locked_lid: '' });
                             await this.ctx.service.noticeAgain.stopNoticeAgain(transaction, this.tableName, audit.id);
                         }
                     }
@@ -515,7 +516,7 @@ module.exports = app => {
                         // 流程至下一审批人
                         const updateData = nextAudits.map(x => {
                             return {
-                                id: x.id, status: x.audit_locked ? auditConst.status.checking : x.status,
+                                id: x.id, status: x.audit_locked ? x.status : auditConst.status.checking,
                                 begin_time: x.audit_locked ? x.begin_time : time, audit_locked: 0,
                             };
                         });
@@ -674,6 +675,7 @@ module.exports = app => {
                     audit_group_need: x.audit_group_need,
                     audit_ledger_id: x.audit_ledger_id,
                     audit_locked: locked,
+                    audit_locked_lid: x.aid === selfAudit.aid ? checkData.audit_locked_lid || '' : '',
                 }
             });
 
@@ -910,6 +912,7 @@ module.exports = app => {
                         audit_ledger_id: x.audit_ledger_id,
                         audit_locked: locked,
                         begin_time: locked ? x.begin_time : null, end_time: locked ? x.end_time : null,
+                        audit_locked_lid: x.aid === selfAudit.aid ? checkData.audit_locked_lid || '' : '',
                     });
                 });
                 await transaction.insert(this.tableName, newFlowAuditors);
@@ -2369,13 +2372,14 @@ module.exports = app => {
         }
 
         async getUniqAuditor(stageId, times) {
-            const auditors = await this.ctx.service.stageAudit.getAuditors(stageId, times); // 全部参与的审批人
+            const auditors = await this.ctx.service.stageAudit.getAuditors(stageId, times, 'DESC'); // 全部参与的审批人
             const result = [];
             auditors.forEach(x => {
                 if (result.findIndex(r => { return x.aid === r.aid && x.audit_order === r.audit_order; }) < 0) {
                     result.push(x);
                 }
             });
+            result.reverse();
             return result;
         }
 
@@ -2384,7 +2388,23 @@ module.exports = app => {
         }
 
         async getAuditLockedId(stage) {
-            return await this.db.query(`SELECT * FROM ${this.tableName} where sid = ? and times = ? and audit_locked_lid <> ''`, [stage.id, stage.times]);
+            const sql = `SELECT sa.*, pa.name, pa.company, pa.role FROM ${this.tableName} sa LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON sa.aid = pa.id` +
+                " WHERE sid = ? AND times = ? AND audit_locked_lid <> '' AND status <> ?";
+            return await this.db.query(sql, [stage.id, stage.times, auditConst.status.checking]);
+        }
+
+        async updateUnlockLid(stage, data) {
+            const updateData = [];
+            for (const d of data) {
+                if (!stage.auditLocked.find(x => {return x.id === d.id; })) continue;
+                updateData.push({ id: d.id, audit_locked_lid: d.audit_locked_lid });
+            }
+            if (updateData.length > 0) {
+                await this.db.updateRows(this.tableName, updateData);
+                return this.getAuditLockedId(stage);
+            } else {
+                return stage.auditLocked;
+            }
         }
 
         /**

+ 19 - 11
app/view/advance/detail.ejs

@@ -93,9 +93,12 @@
                             </tr>
                         </thead>
                         <tbody>
-                            <tr>
-                                <td width="30%">
-                                    <div class="card">
+                            <tr>
+                                <td width="30%">
+                                    <% if (ctx.session.sessionUser.is_admin && advance.status !== auditConst.status.uncheck && advance.status !== auditConst.status.checked) { %>
+                                    <a class="sp-list-item hideSp" href="#sub-sp2" data-toggle="modal" data-target="#sub-sp2">修改审批流程</a>
+                                    <% } %>
+                                    <div class="card">
                                         <ul class="list-group list-group-flush" id="auditors2">
                                             <% auditors.forEach((item, idx) => { %>
                                             <% if (idx === 0) { %>
@@ -186,9 +189,12 @@
                             </tr>
                         </thead>
                         <tbody>
-                            <tr>
-                                <td width="30%" style="vertical-align: top">
-                                    <div class="card">
+                            <tr>
+                                <td width="30%" style="vertical-align: top">
+                                    <% if (ctx.session.sessionUser.is_admin && advance.status !== auditConst.status.uncheck && advance.status !== auditConst.status.checked) { %>
+                                    <a class="sp-list-item hideSp" href="#sub-sp2" data-toggle="modal" data-target="#sub-sp2">修改审批流程</a>
+                                    <% } %>
+                                    <div class="card mt-2">
                                         <ul class="list-group list-group-flush">
                                             <% auditors.forEach((item, idx) => { %>
                                             <% if (idx === 0) { %>
@@ -378,7 +384,8 @@
     const preUrl = '<%- preUrl %>';
     const fileList = JSON.parse(unescape('<%- escape(JSON.stringify(fileList)) %>')) || [];
     const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
-    const curAuditor = JSON.parse(unescape('<%- escape(JSON.stringify(ctx.advance.curAuditor)) %>'));
+    const curAuditor = JSON.parse(unescape('<%- escape(JSON.stringify(ctx.advance.curAuditor)) %>'));
+    const auditType = JSON.parse('<%- JSON.stringify(auditType) %>');
 
     // 展开历史审核记录
     $('td #fold-btn').click(function () {
@@ -399,10 +406,11 @@
     // 处理换行
     advance.remark && $('#ad-remark').html(advance.remark.replace(/<br\/>/g, '\n').replace(/' '/, '\s'));
 </script>
-<% if(isEdited && ctx.session.sessionUser.accountId === ctx.advance.uid) { %>
-<script>
-    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
-    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+<% if((isEdited && ctx.session.sessionUser.accountId === ctx.advance.uid) || (ctx.session.sessionUser.is_admin && advance.status !== auditConst.status.uncheck && advance.status !== auditConst.status.checked)) { %>
+<script>
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const advanceAdminAuditors = JSON.parse(unescape('<%- escape(JSON.stringify(adminAuditors || [])) %>'));
     const shenpi_status = <%- ctx.tender.info.shenpi.advance %>;
     const shenpiConst =  JSON.parse('<%- JSON.stringify(shenpiConst) %>');
 </script>

+ 61 - 10
app/view/advance/modal_audit.ejs

@@ -1,4 +1,50 @@
 <!--上报审批-->
+<% if (ctx.session.sessionUser.is_admin && ctx.advance.status !== auditConst.status.uncheck && ctx.advance.status !== auditConst.status.checked) { %>
+<div class="modal fade" id="sub-sp2" data-backdrop="static">
+    <div class="modal-dialog" style="max-width: 650px" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">修改审批流程</h5>
+            </div>
+            <div class="modal-body">
+                <div class="card mt-1">
+                    <div class="modal-height-500" style="overflow: auto">
+                      <style>
+                                    #admin-edit-shenpi thead th {
+                                        border-bottom: 0;
+                                    }
+
+                                    #admin-edit-shenpi td, #admin-edit-shenpi th {
+                                        padding: 0.75rem;
+                                    }
+
+                                    #admin-edit-shenpi th {
+                                        background: none;
+                                        color: #212529;
+                                        border-top: 0;
+                                    }
+                                </style>
+                        <table class="table table-hover" id="admin-edit-shenpi">
+                            <thead>
+                            <tr class="card-header text-center">
+                                <th width="100px">审批流程</th>
+                                <th>审批人员</th>
+                                <th width="80" style="text-align: center">审批状态</th>
+                                <th width="200" style="text-align: center">操作</th>
+                            </tr>
+                            </thead>
+                            <tbody id="admin-edit-shenpi-list"></tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+            <form class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
 <div class="modal fade" id="sub-sp" data-backdrop="static">
     <div class="modal-dialog" role="document">
         <div class="modal-content">
@@ -197,7 +243,7 @@
                                                             <%- auditor.status === auditConst.status.checkNoPre ? history.find(item => item.sort === auditor.sort-1).name : '' %>
                                                         </span>
                                                     </div>
-                                                    
+
                                                 </div>
                                             </div>
                                             <!--审批意见-->
@@ -294,10 +340,10 @@
                                                             <div class="col">
                                                                 <span
                                                                     class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
-                                                            
+
                                                             </div>
 
-                                                            
+
                                                         </div>
                                                     </div>
                                                 </div>
@@ -501,9 +547,9 @@
                                                             <div class="col">
                                                                 <span
                                                                     class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
-                                                            
+
                                                             </div>
-                                                            
+
                                                         </div>
                                                     </div>
                                                 </div>
@@ -543,7 +589,7 @@
                                                             <div class="col">
                                                                 <span
                                                                     class="pull-right <%- auditConst.statusClass[auditor.status] %>"><%- auditConst.statusString[auditor.status] %></span>
-                                                            
+
                                                             </div>
 
                                                         </div>
@@ -736,6 +782,11 @@
             </div>
         </div>
     <% } %>
+<style>
+    #admin-edit-shenpi thead th { border-bottom: 0; }
+    #admin-edit-shenpi td, #admin-edit-shenpi th { padding: 0.75rem; }
+    #admin-edit-shenpi th { background: none; color: #212529; border-top: 0; }
+</style>
 <script>
     const csrf = '<%= ctx.csrf %>';
     const authMobile = '<%= authMobile %>';
@@ -833,11 +884,11 @@
     $('.sp-list-btn').click(function () {
         const type = $(this).data('type')
         if (type === 'hide') {
-            $('.sp-list-item').hide()
-            $('.modal-title').text('审批流程')
+            $('#sp-list .sp-list-item').hide()
+            $('#sp-list .modal-title').text('审批流程')
         } else {
-            $('.sp-list-item').show()
-            $('.modal-title').text('重新上报')
+            $('#sp-list .sp-list-item').show()
+            $('#sp-list .modal-title').text('重新上报')
         }
     });
 

Plik diff jest za duży
+ 2 - 2
app/view/contract/detail.ejs


+ 1 - 1
app/view/dashboard/workspace.ejs

@@ -536,7 +536,7 @@
                                                         <% const cSpId = c.spid || c.tender_spid; %>
                                                         <% const cDetailUrl = c.is_project_contract ? '/sp/' + c.spid + '/contract/detail' + (c.contract_type === 2 ? '/income' : '') + (c.shenpi_type !== 'contract' ? '?tab=pay' : '') : '/sp/' + c.tender_spid + '/contract/tender/' + c.tid + '/detail' + (c.contract_type === 2 ? '/income' : '') + (c.shenpi_type !== 'contract' ? '?tab=pay' : ''); %>
                                                         <% const cDisplayName = c.is_project_contract ? '项目合同-' + (c.contract_name || c.name) : '标段合同-' + (c.contract_name || c.name); %>
-                                                        <% const cShowResubmitBtn = c.status === acContract.status.checkNo || (c.status === acContract.status.uncheck && c.times > 1); %>
+                                                        <% const cShowResubmitBtn = c.status === acContract.status.checkNo || (c.status === acContract.status.uncheck && c.times > 1) || (c.contract_status === acContract.status.checkNo && c.uid === uid); %>
                                                         <tr data-type="contract">
                                                             <% if (c.start_audit === 2) { %><td class="text-center text-danger"><i class="fa fa-exclamation-triangle"></i></td><% } else if (c.start_audit === 1) { %><td class="text-center text-warning"><i class="fa fa-bell"></i></td><% } else { %><td></td><% } %>
                                                             <td><span class="badge text-width" style="background-color: #e3f2fd; color: #1976d2;">合同管理</span></td>

+ 2 - 0
app/view/material/exponent.ejs

@@ -197,9 +197,11 @@
 </div>
 <script>
     const readOnly = <%- material.readOnly %>;
+    const isAdmin = <%- ctx.session.sessionUser.is_admin %>;
     const materialID = <%- material.id %>;
     const materialTax = <%- material.material_tax %>;
     const materialOrder = <%- material.order %>;
+    const materialHighOrder = <%- material.highOrder %>;
     const isStageSelf = parseInt('<%- material.is_stage_self %>');
     const showStageExponent = <%- material.showStageExponent %>;
     const exponent_nodes = JSON.parse(unescape('<%- escape(JSON.stringify(ctx.material.exponent_node)) %>'));

+ 5 - 0
app/view/stage/audit_btn.ejs

@@ -54,6 +54,11 @@
     <% if (ctx.stage.user_id === ctx.session.sessionUser.accountId && ctx.stage.order === ctx.stage.highOrder && (ctx.stage.status === auditConst.status.checkNo || ctx.stage.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>
     <% } %>
+    <% if (ctx.stage.auditLocked && ctx.stage.auditLocked.length > 0) { %>
+        <% if (ctx.session.sessionUser.is_admin || ctx.stage.auditLocked.find(x => { return x.aid === ctx.session.sessionUser.accountId; })) { %>
+            <button class="btn btn-outline-danger btn-sm btn-block mt-5" id="sp-unlock">解锁</button>
+        <% } %>
+    <% } %>
 </div>
 <script>
     $('.sp-list-btn').click(function () {

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

@@ -689,6 +689,12 @@
                     </div>
                 </div>
                 <div class="modal-footer">
+                    <% if (ctx.url === '/tender/' + ctx.tender.id + '/measure/stage/' + ctx.stage.order) { %>
+                    <div class="mr-auto">
+                        <button class="btn btn-sm btn-warning" type="button" id="sp-back-lock">锁定数据</button>
+                        <input type="hidden" name="audit_locked_lid" value="" />
+                    </div>
+                    <% } %>
                     <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
                     <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
                     <button type="submit" class="btn btn-warning btn-sm">确认退回</button>

+ 18 - 0
app/view/stage/modal.ejs

@@ -707,6 +707,24 @@
         </div>
     </div>
 </div>
+<!--审批退回锁定-->
+<div class="modal fade" id="sp-lock" data-backdrop="static">
+    <div class="modal-dialog modal-xl" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">锁定计量数据</h5>
+            </div>
+            <div class="modal-body">
+                <div id="sp-lock-spread" class="modal-height-600">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="sp-lock-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
 <% include ./audit_modal.ejs %>
 <% include ../shares/merge_peg_modal.ejs %>
 <% include ../shares/check_modal2.ejs %>

+ 36 - 0
sql/update.sql

@@ -42,6 +42,42 @@ ADD COLUMN `kp_time` datetime NULL DEFAULT NULL COMMENT '开票时间' AFTER `kp
 ALTER TABLE `zh_stage_audit`
 ADD COLUMN `audit_locked_lid` varchar(5000) NOT NULL DEFAULT '' COMMENT '审批退回锁定台账id' AFTER `audit_locked`;
 
+ALTER TABLE `zh_material_exponent`
+ADD COLUMN `m_up_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '上涨幅度(%)' AFTER `basic_times`,
+ADD COLUMN `m_down_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '下跌幅度(%)' AFTER `m_up_risk`,
+ADD COLUMN `effective_price` decimal(30,8) DEFAULT NULL COMMENT '有效价格指数(Ft)' AFTER `m_price`;
+
+ALTER TABLE `zh_material_exponent_history`
+ADD COLUMN `m_up_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '本期上涨幅度(%)' AFTER `basic_times`,
+ADD COLUMN `m_down_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '本期下跌幅度(%)' AFTER `m_up_risk`,
+ADD COLUMN `effective_price` decimal(30,8) DEFAULT NULL COMMENT '本期有效价格指数(Ft)' AFTER `m_price`;
+
+ALTER TABLE `zh_material_exponent_shard`
+ADD COLUMN `m_up_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '上涨幅度(%)' AFTER `weight_num`,
+ADD COLUMN `m_down_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '下跌幅度(%)' AFTER `m_up_risk`,
+ADD COLUMN `effective_price` decimal(30,8) DEFAULT NULL COMMENT '有效价格指数(Ft)' AFTER `m_price`;
+
+ALTER TABLE `zh_material_exponent`
+ADD COLUMN `m_up_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '上涨幅度(%)' AFTER `basic_times`,
+ADD COLUMN `m_down_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '下跌幅度(%)' AFTER `m_up_risk`,
+ADD COLUMN `effective_price` decimal(30,8) DEFAULT NULL COMMENT '有效价格指数(Ft)' AFTER `m_price`;
+
+ALTER TABLE `zh_material_exponent_history`
+ADD COLUMN `m_up_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '本期上涨幅度(%)' AFTER `basic_times`,
+ADD COLUMN `m_down_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '本期下跌幅度(%)' AFTER `m_up_risk`,
+ADD COLUMN `effective_price` decimal(30,8) DEFAULT NULL COMMENT '本期有效价格指数(Ft)' AFTER `m_price`;
+
+ALTER TABLE `zh_material_exponent_shard`
+ADD COLUMN `m_up_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '上涨幅度(%)' AFTER `weight_num`,
+ADD COLUMN `m_down_risk` tinyint(3) NOT NULL DEFAULT 0 COMMENT '下跌幅度(%)' AFTER `m_up_risk`,
+ADD COLUMN `effective_price` decimal(30,8) DEFAULT NULL COMMENT '有效价格指数(Ft)' AFTER `m_price`;
+
+ALTER TABLE `zh_filing`
+ADD COLUMN `upload_tips` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '上传说明' AFTER `tips`;
 ------------------------------------
 -- 表数据
 ------------------------------------
+
+UPDATE `zh_material_exponent` SET `effective_price` = `m_price` WHERE `effective_price` IS NULL;
+UPDATE `zh_material_exponent_history` SET `effective_price` = `m_price` WHERE `effective_price` IS NULL;
+UPDATE `zh_material_exponent_shard` SET `effective_price` = `m_price` WHERE `effective_price` IS NULL;