/** * Created by CSL on 2017-07-19. * 计算程序。所有定额、清单、父清单的计算都从此入。 * dispExpr: F8*(L-1); expression: "@('8') * (L-1)"; * 说明:F后跟行号,L替换人工系数值,@后跟ID。用到L的规则必须有labourCoeID属性(反过来不要求), * 用到费率的规则必须有feeRateID属性,当有该属性时,会自动显示费率值。 */ let defaultBillTemplate = { ID: 15, name: "清单缺省", calcItems: [ { ID: 1, code: "1", name: "定额直接费", dispExpr: "F2+F3+F4", expression: "@('2')+@('3')+@('4')", statement: "人工费+材料费+机械费", feeRate: null, memo: '' }, { ID: 2, code: "1.1", name: "人工费", dispExpr: "HJ", expression: "HJ", statement: "合计", feeRate: 50, fieldName: 'labour', memo: '' }, { ID: 3, code: "1.2", name: "材料费", dispExpr: "HJ", expression: "HJ", statement: "合计", feeRate: 30, fieldName: 'material', memo: '' }, { ID: 4, code: "1.3", name: "机械费", dispExpr: "HJ", expression: "HJ", statement: "合计", feeRate: 20, fieldName: 'machine', memo: '' }, { ID: 5, code: "2", name: "企业管理费", dispExpr: "F1", expression: "@('1')", statement: "定额直接费", feeRate: null, fieldName: 'manage', memo: '' }, { ID: 6, code: "3", name: "利润", dispExpr: "F1", expression: "@('1')", statement: "定额直接费", feeRate: null, fieldName: 'profit', memo: '' }, { ID: 7, code: "4", name: "风险费用", dispExpr: "F1", expression: "@('1')", statement: "定额直接费", feeRate: null, fieldName: 'risk', memo: '' }, { ID: 8, code: "5", name: "综合单价", dispExpr: "F1+F5+F6+F7", expression: "@('1')+@('5')+@('6')+@('7')", statement: "定额直接费+企业管理费+利润+风险费用", feeRate: null, fieldName: 'common', memo: '' } ] }; let analyzer = { calcTemplate: null, success: true, standard: function(expr){ let str = expr; str = str.replace(/\s/g, ""); // 去空格、去中文空格 str = str.replace(/(/g, "("); // 中文括号"("换成英文括号"(" str = str.replace(/)/g, ")"); // 中文括号")"换成英文括号")" str = str.replace(/f/g, "F"); // f换成F return str; }, analyzeCalcBase: function(expr){ // 前提:必须无空格、无特殊符号 function getCalcBase(expr){ let base = '', iPos1 = -1, iPos2 = -1; for (let i = 0; i < expr.length; i++) { if (expr[i] === '['){ iPos1 = i; } else if (iPos1 != -1 && expr[i]===']'){ iPos2 = i; }; if (iPos1 != -1 && iPos2 != -1){ base = expr.slice(iPos1, iPos2 + 1); break; } }; return base; }; function calcBaseToIDExpr(base){ /*// for test. 公路模式,基数到ID let id = -1; if (base == '[人工费]'){ id = 111; } else if (base == '[材料费]'){ id = 222; } else if (base == '[机械费]'){ id = 333; } else id = "错误"; return "@('" + id + "')";*/ let baseValue = base.slice(1, -1); return "base('" + baseValue + "')"; }; while (expr.indexOf('[') > 0) { let base = getCalcBase(expr); let id = calcBaseToIDExpr(base); let baseValue = base.slice(1, -1); // []会给下面的正则带来干扰,这里去掉 var pattBase =new RegExp(baseValue, "g"); expr = expr.replace(pattBase, id); expr = expr.replace(/\[base\('/g, "base('"); // [@(' [base(' expr = expr.replace(/'\)\]/g, "')"); // ')] }; return expr; }, analyzeLineRef: function(expr){ let me = this; function isOperator(char){ var operator = "+-*/()"; return operator.indexOf(char) > -1; }; function lineNumToID(lineNum){ if (lineNum > me.calcTemplate.calcItems.length){ me.success = false; return '越界'; } else{ let id = me.calcTemplate.calcItems[lineNum - 1].ID; return id; } }; // 前提:必须无空格、无特殊符号、标准大写F function getSection(expr){ let section = '', iPos1 = -1, iPos2 = -1; for (let i = 0; i < expr.length; i++) { if (expr[i] === 'F'){ iPos1 = i; } else if (iPos1 != -1 && isOperator(expr[i])){ iPos2 = i; } else if (iPos1 != -1 && i == expr.length - 1){ iPos2 = i + 1; }; if (iPos1 != -1 && iPos2 != -1){ section = expr.slice(iPos1, iPos2); break; } }; return section; }; function sectionToIDExpr(section){ if (section){ let lineNum = section.slice(1); if (isNaN(lineNum)){ me.success = false; return '错误'; // 这里的返回提示不能加上section,因为会无限循环 } else return "@('" + lineNumToID(lineNum) + "')"; } else return ''; }; while (expr.indexOf('F') > 0) { let sec = getSection(expr); let id = sectionToIDExpr(sec); var pattSec =new RegExp(sec, "g"); expr = expr.replace(pattSec, id); }; return expr; }, analyzeUserExpr: function(calcTemplate, calcItem){ let me = this; me.calcTemplate = calcTemplate; let expr = calcItem.dispExpr; // 标准化:处理特殊字符、中文符号、大小写 expr = me.standard(expr); calcItem.dispExpr = expr; // 先换掉计算基数 expr = me.analyzeCalcBase(expr); // 再换掉行引用 expr = me.analyzeLineRef(expr); calcItem.expression = expr; return me.success; } }; let executeObj = { treeNode: null, template: null, calcBase: null, at: function(ID) { let me = executeObj, rst = 0; rst = me.template.compiledCalcItems[ID].unitFee; rst = parseFloat(rst); return rst; }, base: function(calcBaseName) { let me = executeObj, rst = 0, //base = getRationCalcBase(calcBaseName); base = me.calcBase[calcBaseName]; if (base != null) { let price = 0, aprice = 0, mprice = 0, tmpSum = 0, mdSum = 0; function isSubset(sub, arr){ // if(!(sub instanceof Array) || !(arr instanceof Array)) return false; // if(sub.length > arr.length) return false; for(var i = 0, len = sub.length; i < len; i++){ if(arr.indexOf(sub[i]) == -1) return false; } return true; } // 机上人工费:多一层 if (isSubset(base.gljTypes, [gljType.MACHINE_LABOUR])) { if (!me.treeNode.data.gljList) tmpSum = 0 else{ for (let glj of me.treeNode.data.gljList) { if (glj.type == gljType.GENERAL_MACHINE) { // 获取机械组成物 let mds = projectObj.project.composition.getCompositionByCode(glj.code); if (!mds) mds = []; for (let md of mds){ if (base.gljTypes.indexOf(md.glj_type) >= 0) { price = md["base_price"]; if (!price) price = 0; mdSum = mdSum + (md["consumption"] * price).toDecimal(me.digit); mdSum = (mdSum).toDecimal(me.digitDefault); } }; tmpSum = tmpSum + (glj["quantity"] * mdSum).toDecimal(me.digitDefault); tmpSum = (tmpSum).toDecimal(me.digitDefault); } }; } }else{ if (!me.treeNode.data.gljList) tmpSum = 0 else{ for (let glj of me.treeNode.data.gljList) { if (base.gljTypes.indexOf(glj.type) >= 0) { if (base.calcType == baseCalc){ price = glj["basePrice"];} else if (base.calcType == adjustCalc){price = glj["adjustPrice"];} else if (base.calcType == budgetCalc){price = glj["marketPrice"];} else if (base.calcType == diffCalc){ aprice = glj["adjustPrice"]; if (!aprice) aprice = 0; mprice = glj["marketPrice"]; if (!mprice) mprice = 0; price = mprice - aprice; }; if (!price) price = 0; tmpSum = tmpSum + (glj["quantity"] * price).toDecimal(me.digitDefault); tmpSum = (tmpSum).toDecimal(me.digitDefault); }; }; }; }; rst = (tmpSum).toDecimal(me.digitDefault); }; return rst; }, HJ: function () { let me = this; return me.treeNode.calcBaseValue; } }; class CalcProgram { constructor(project){ let me = this; me.project = project; me.datas = []; project.registerModule(ModuleNames.calc_program, me); }; getSourceType () { return ModuleNames.calc_program; }; loadData (datas) { this.datas = datas; }; doAfterUpdate (err, data) { if(!err){ // do } }; // 经测试,全部编译一次耗时0.003~0.004秒。耗时基本忽略不计。 compileAllTemps(){ let me = this; me.digit = 2; me.digitDefault = 6; me.compiledFeeRates = {}; me.compiledLabourCoes = {}; me.compiledTemplates = {}; me.compiledTemplateMaps = {}; me.compiledTemplateNames = []; me.compiledFeeTypeMaps = {}; me.compiledFeeTypeNames = []; me.compiledCalcBases = {}; me.saveForReports = []; me.feeRates = this.project.FeeRate.datas.rates; me.labourCoes = this.project.labourCoe.datas.coes; me.feeTypes = feeType; me.calcBases = rationCalcBase; me.templates = this.project.calcProgram.datas.templates; me.templates.push(defaultBillTemplate); // 先编译公用的基础数据 me.compilePublics(); for (let t of me.templates){ me.compileTemplate(t); }; // 存储费率临时数据,报表用。 if (me.saveForReports.length > 0){ let saveDatas = {}; saveDatas.projectID = projectInfoObj.projectInfo.ID; saveDatas.calcItems = me.saveForReports; CommonAjax.post('/calcProgram/saveCalcItems', saveDatas, function (data) { me.saveForReports = []; }); }; }; compilePublics(){ let me = this; for (let rate of me.feeRates) { me.compiledFeeRates[rate.ID] = rate; } for (let coe of me.labourCoes) { me.compiledLabourCoes[coe.ID] = coe; } for (let ft of me.feeTypes) { me.compiledFeeTypeMaps[ft.type] = ft.name; me.compiledFeeTypeMaps[ft.name] = ft.type; // 中文预编译,可靠性有待验证 me.compiledFeeTypeNames.push(ft.name); } for (let cb of me.calcBases) { me.compiledCalcBases[cb.dispName] = cb; // 中文预编译,可靠性有待验证 } }; compileTemplate(template){ let me = this; me.compiledTemplates[template.ID] = template; me.compiledTemplateMaps[template.ID] = template.name; me.compiledTemplateMaps[template.name] = template.ID; me.compiledTemplateNames.push(template.name); template.hasCompiled = false; template.errs = []; let private_extract_ID = function(str, idx){ let rst = '', lBracket = 0, rBracket = 0, firstIdx = idx, lastIdx = 0; for (let i = idx; i < str.length; i++) { if (str[i] === '(') { lBracket++; if (lBracket == 1) firstIdx = i + 1; } if (str[i] === ')') { rBracket++; if (lBracket == rBracket) { lastIdx = i - 1; if (lastIdx > firstIdx) { if (str[firstIdx] === "'") firstIdx++; if (str[lastIdx] !== "'") lastIdx++; if (lastIdx > firstIdx) { rst = str.slice(firstIdx, lastIdx); } } break; } } } return rst; }; let private_parse_ref = function(item, itemIdx){ let idx = item.expression.indexOf('@(', 0); while (idx >= 0) { let ID = private_extract_ID(item.expression, idx); if (ID.length > 0) { let subItem = template.compiledCalcItems[ID]; if (subItem) { if (subItem.ID !== item.ID) { private_parse_ref(subItem, template.compiledCalcItems[ID + "_idx"]); } else { template.errs.push("There exists the self refer ID: " + ID); } } else { template.errs.push("There exists the invalid ID by which could not find the item: " + ID); console.log('invalid ID: ' + ID); } } idx = item.expression.indexOf('@(', idx + ID.length + 3); } if (template.compiledSeq.indexOf(itemIdx) < 0) { template.compiledSeq.push(itemIdx); } }; let private_setup_seq = function(item, itemIdx){ if (template.compiledSeq.indexOf(itemIdx) < 0) { private_parse_ref(item, itemIdx); } }; let private_compile_items = function() { for (let idx of template.compiledSeq) { let item = template.calcItems[idx]; item.dispExprUser = item.dispExpr; // 用于界面显示。disExpr是公式模板,不允许修改:人工系数占位符被修改后变成数值,第二次无法正确替换。 if (item.expression == 'HJ') item.compiledExpr = '$CE.HJ()' else{ item.compiledExpr = item.expression.split('@(').join('$CE.at('); item.compiledExpr = item.compiledExpr.split('base(').join('$CE.base('); }; if (item.labourCoeID){ let lc = me.compiledLabourCoes[item.labourCoeID].coe; item.dispExprUser = item.dispExpr.replace(/L/gi, lc.toString()); item.compiledExpr = item.compiledExpr.replace(/L/gi, lc.toString()); }; if (item.feeRateID) { let orgFeeRate = item.feeRate; let cmf = me.compiledFeeRates[item.feeRateID]; item.feeRate = cmf?cmf.rate:100; if (!orgFeeRate || (orgFeeRate && orgFeeRate != item.feeRate)){ me.saveForReports.push({templatesID: template.ID, calcItem: item}); } }; // 字段名映射 item.displayFieldName = me.compiledFeeTypeMaps[item.fieldName]; } }; if (template && template.calcItems && template.calcItems.length > 0) { template.compiledSeq = []; template.compiledCalcItems = {}; for (let i = 0; i < template.calcItems.length; i++) { let item = template.calcItems[i]; template.compiledCalcItems[item.ID] = item; template.compiledCalcItems[item.ID + "_idx"] = i; } for (let i = 0; i < template.calcItems.length; i++) { private_setup_seq(template.calcItems[i], i); } if (template.errs.length == 0) { private_compile_items(); template.hasCompiled = true; } else { console.log('errors: ' + template.errs.toString()); } }; }; // 仅内部调用。注意:外部不能直接使用 InnerCalc(treeNode){ let me = this; let project = me.project; function initFees(treeNode){ if (!treeNode.data.fees) { treeNode.data.fees = []; treeNode.data.feesIndex = {}; treeNode.changed = true; }; }; function checkFee(treeNode, feeObj){ if (feeObj.fieldName == '') return; if (!treeNode.data.feesIndex[feeObj.fieldName]){ let fee = { 'fieldName': feeObj.fieldName, 'unitFee': feeObj.unitFee, 'totalFee': feeObj.totalFee, 'tenderUnitFee': 0, 'tenderTotalFee': 0 }; treeNode.data.fees.push(fee); treeNode.data.feesIndex[feeObj.fieldName] = fee; treeNode.changed = true; } else{ if (treeNode.data.feesIndex[feeObj.fieldName].unitFee != feeObj.unitFee){ treeNode.data.feesIndex[feeObj.fieldName].unitFee = feeObj.unitFee; treeNode.changed = true; }; if (treeNode.data.feesIndex[feeObj.fieldName].totalFee != feeObj.totalFee){ treeNode.data.feesIndex[feeObj.fieldName].totalFee = feeObj.totalFee; treeNode.changed = true; }; }; }; // 汇总定额或子清单的费用类别 if (treeNode.calcType == treeNodeCalcType.ctGatherRations || treeNode.calcType == treeNodeCalcType.ctGatherBills){ initFees(treeNode); let objsArr = (treeNode.calcType == treeNodeCalcType.ctGatherRations) ? project.Ration.getRationsByNode(treeNode) : treeNode.children; let rst = []; for (let ft of feeType) { let ftObj = {}; ftObj.fieldName = ft.type; ftObj.name = ft.name; let uf = 0, tf = 0, tuf = 0, ttf = 0; for (let item of objsArr) { let data = (treeNode.calcType == treeNodeCalcType.ctGatherRations) ? item : item.data; if (data.feesIndex && data.feesIndex[ft.type]) { uf = (uf + parseFloat(data.feesIndex[ft.type].unitFee)).toDecimal(me.digitDefault); tf = (tf + parseFloat(data.feesIndex[ft.type].totalFee)).toDecimal(me.digitDefault); tuf = (tuf + parseFloat(data.feesIndex[ft.type].tenderUnitFee)).toDecimal(me.digitDefault); ttf = (ttf + parseFloat(data.feesIndex[ft.type].tenderTotalFee)).toDecimal(me.digitDefault); }; }; ftObj.unitFee = uf.toDecimal(me.digit); ftObj.totalFee = tf.toDecimal(me.digit); ftObj.tenderUnitFee = tuf.toDecimal(me.digit); ftObj.tenderTotalFee = ttf.toDecimal(me.digit); checkFee(treeNode, ftObj); rst.push(ftObj); }; treeNode.data.calcTemplate = {"calcItems": rst}; } else{ // 叶子清单的缺省计算程序需要提供总金额作为计算基数(不需要工料机),然后每条按比例(费率)计算,不需要工料机明细。 if (treeNode.calcType == treeNodeCalcType.ctCalcBaseValue){ delete treeNode.data.gljList; if (treeNode.data.programID == undefined){ treeNode.data.programID = defaultBillTemplate.ID; }; } else { if (treeNode.sourceType === project.Ration.getSourceType()) { treeNode.data.gljList = project.ration_glj.getGljArrByRation(treeNode.data.ID); } else if (treeNode.sourceType === project.Bills.getSourceType()) { let rations = project.Ration.getBillsSortRation(treeNode.source.getID()); treeNode.data.gljList = project.ration_glj.getGatherGljArrByRations(rations); }; if (treeNode.data.programID == undefined){ treeNode.data.programID = 1; }; }; let template = me.compiledTemplates[treeNode.data.programID]; treeNode.data.calcTemplate = template; if (treeNode && template.hasCompiled) { let $CE = executeObj; $CE.treeNode = treeNode; $CE.template = template; $CE.calcBase = me.compiledCalcBases; initFees(treeNode); for (let idx of template.compiledSeq) { let calcItem = template.calcItems[idx]; let feeRate = calcItem.feeRate; if (!feeRate) feeRate = 100; // 100% calcItem.unitFee = (eval(calcItem.compiledExpr) * feeRate * 0.01).toDecimal(me.digit); // 如果eval()对清单树有影响,就换成小麦的Expression对象再试 let quantity = treeNode.data.quantity; if (!quantity) quantity = 0; calcItem.totalFee = (calcItem.unitFee * quantity).toDecimal(me.digit); checkFee(treeNode, calcItem); }; } }; } // 计算本节点(默认同时递归计算所有父节点,可选) calculate(treeNode, calcParents = true){ let me = this; let isRation = treeNode.sourceType === me.project.Ration.getSourceType(); let isBill = treeNode.sourceType === me.project.Bills.getSourceType(); let isLeafBill = isBill && treeNode.source.children && treeNode.source.children.length === 0; let isBillPriceCalc = me.project.projSetting.billsCalcMode === billsPrice; if (isRation) treeNode.calcType = treeNodeCalcType.ctRationCalcProgram else if (isLeafBill) { if (treeNode.children && treeNode.children.length > 0){ me.calcLeafBillChildren(treeNode); if (treeNode.children[0].sourceType == me.project.Ration.getSourceType()){ if (isBillPriceCalc) // 清单单价计算模式下的叶子清单:取自己的计算程序ID,找到自己的计算程序计算 treeNode.calcType = treeNodeCalcType.ctBillCalcProgram; else // 前三种计算模式下的叶子清单:汇总定额的计算程序的费用类别 treeNode.calcType = treeNodeCalcType.ctGatherRations; } else if (treeNode.children[0].sourceType == me.project.VolumePrice.getSourceType()){ let value = 20000; // if (treeNode.data.feesIndex && treeNode.data.feesIndex.common && treeNode.data.feesIndex.common.unitFee != 0) // value = treeNode.data.feesIndex.common.unitFee; treeNode.calcType = treeNodeCalcType.ctCalcBaseValue; treeNode.calcBaseValue = value; }; } else{ // 公式计算 let value = 20000; // if (treeNode.data.feesIndex && treeNode.data.feesIndex.common && treeNode.data.feesIndex.common.unitFee != 0) // value = treeNode.data.feesIndex.common.unitFee; treeNode.calcType = treeNodeCalcType.ctCalcBaseValue; treeNode.calcBaseValue = value; }; } else if (isBill) // 父清单:汇总子清单的费用类别 treeNode.calcType = treeNodeCalcType.ctGatherBills; me.InnerCalc(treeNode); // 计算所有父结点 if (treeNode.changed && calcParents && treeNode.parent) { me.calculate(treeNode.parent); }; }; // 存储、刷新本节点(默认存储刷新所有父节点,可选) saveNode(treeNode, saveParents = true) { if (!treeNode.changed) return; let me = this; let nodesArr = []; let curNode = treeNode; while (curNode) { if (curNode.changed){nodesArr.push(curNode)}; if (saveParents) curNode = curNode.parent else break; }; me.saveNodes(nodesArr); }; // 多个树结点入库存储,刷新界面显示。 saveNodes(treeNodes){ if (treeNodes.length < 1) return; let me = this; me.project.beginUpdate(''); for (let node of treeNodes){ if (node.changed){ let data = { ID: node.data.ID, projectID: me.project.ID(), quantity: node.data.quantity, programID: node.data.programID, fees: node.data.fees }; let newData = {'updateType': 'ut_update', 'updateData': data}; me.project.push(node.sourceType, [newData]); } }; me.project.endUpdate(); for (let node of treeNodes){delete node.changed}; projectObj.mainController.refreshTreeNode(treeNodes); if (activeSubSheetIs(subSheetIndex.ssiCalcProgram)) { calcProgramObj.showData(me.project.mainTree.selected, false); }; }; /* 计算所有树结点(分3种情况),并将发生计算改动的结点入库存储。 参数取值如下: calcAllType.catAll 计算所有树结点 (不指定参数时的默认值) calcAllType.catBills 计算所有清单 (改变项目属性中清单取费算法时会用到) calcAllType.catRations 计算所有定额、工料机形式的定额、量价,因为它们都走自己的计算程序 (改变人工系数、费率值、工料机单价时会用到) */ calcAllNodes(calcType = calcAllType.catAll){ let me = this; let needSaveNodes = []; function calcNodes(nodes) { for (let node of nodes) { if (node.children.length > 0) { calcNodes(node.children); }; if ((calcType == calcAllType.catAll) || (calcType == node.sourceType)) { me.calculate(node, false); if (node.changed) needSaveNodes.push(node); }; } }; calcNodes(me.project.mainTree.roots); me.saveNodes(needSaveNodes); }; // 重新计算叶子清单下的所有子结点:如定额、工料机定额等(calculate算法基于定额、工料机定额的计算结果是正确的,实际上有时它们的计算结果并不是最新的) calcLeafBillChildren(treeNode){ let me = this; if (treeNode.children && treeNode.children.length > 0) { let needSaveNodes = []; for (let child of treeNode.children){ me.calculate(child, false); if (child.changed) needSaveNodes.push(child); }; me.saveNodes(needSaveNodes); }; }; }