 /**
 * Created by CSL on 2017-07-19.
 * 计算程序。所有定额、清单、父清单的计算都从此入。
 */

let calcTools = {
    getNodeByFlag: function (flag) {
        let bill = cbTools.findBill(flag);
        if (bill) return this.getNodeByID(bill.ID)
        else return null;
    },
    getNodeByID: function (ID){
        return cbTools.getNodeByID(ID);
    },

    isBill: function(treeNode){
        return treeNode.sourceType === ModuleNames.bills;
    },
    isParentBill: function (treeNode) {
        return this.isBill(treeNode) &&
            treeNode.source.children &&
            treeNode.source.children.length > 0;
    },
    isLeafBill: function(treeNode){
        return this.isBill(treeNode) &&
            treeNode.source.children &&
            treeNode.source.children.length === 0;
    },
    isBill_DXFY: function(treeNode){
        return this.isBill(treeNode) && treeNode.data.type == billType.DXFY;
    },
    isBill_FB: function(treeNode){
        return this.isBill(treeNode) && treeNode.data.type == billType.FB;
    },
    isBill_FX: function(treeNode){
        return this.isBill(treeNode) && treeNode.data.type == billType.FX;
    },
    isBill_BILL: function(treeNode){
        return this.isBill(treeNode) && treeNode.data.type == billType.BILL;
    },
    isBill_BX: function(treeNode){
        return this.isBill(treeNode) && treeNode.data.type == billType.BX;
    },
    isNullBill: function (treeNode) {
        return this.isLeafBill(treeNode) && (treeNode.children.length === 0) && (!treeNode.data.calcBase);
    },
    isCalcBaseBill: function(treeNode){
        return this.isLeafBill(treeNode) && (treeNode.children.length === 0) && (treeNode.data.calcBase);
    },
    isTotalCostBill: function (treeNode) {
        return treeNode.data.flagsIndex && treeNode.data.flagsIndex.fixed && treeNode.data.flagsIndex.fixed.flag &&
            treeNode.data.flagsIndex.fixed.flag == fixedFlag.TOTAL_COST;
    },
    isRationCategory: function(treeNode){
        return treeNode.sourceType === ModuleNames.ration;
    },
    isRationItem: function(treeNode){
        return this.isRationCategory(treeNode) && treeNode.data.type === rationType.ration;
    },
    isVolumePrice: function (treeNode) {
        return this.isRationCategory(treeNode) && treeNode.data.type === rationType.volumePrice;
    },
    isGljRation: function (treeNode) {
        return this.isRationCategory(treeNode) && treeNode.data.type === rationType.gljRation;
    },
    isSameTypeNode: function (node1, node2) {
        if (node1.parent && node2.parent && (node1.parent === node2.parent) && (node1.sourceType === node1.sourceType)
            && (node1.data && node2.data && node1.data.type === node2.data.type)){
            return true;
        }
        return false;
    },
    isInheritFrom: function (treeNode, flagsArr){
        let cur = treeNode;
        while (cur.parent) {
            cur = cur.parent;
        };

        let flag = -1;
        if (cur.data.flagsIndex && cur.data.flagsIndex.fixed && cur.data.flagsIndex.fixed.flag)
            flag = cur.data.flagsIndex.fixed.flag;

        return flagsArr.includes(flag);
    },
    isFBFX:function (treeNode) {
        return projectObj.project.Bills.isFBFX(treeNode);
    },
    isTechMeasure:function(treeNode){
       return projectObj.project.Bills.isTechMeasure(treeNode)
    },
    isInheritFrom:function(treeNode, flag){
        let rst = false;
        let curNode = treeNode;
        while (curNode){
            if (curNode.data && curNode.data.flagsIndex && curNode.data.flagsIndex.fixed &&
                curNode.data.flagsIndex.fixed.flag == flag){
                rst = true;
                break;
            }
            curNode = curNode.parent;
        };
        return rst;
    },
    getChildrenFormulaNodes: function (self, allFormulaNodesArr, parentNodes){       // 获取结点parentNodes下有公式的子结点
        let nodes = [];
        for (let pn of parentNodes){
            for (let node of allFormulaNodesArr){
                let cur = node;
                while (cur) {
                    if (cur == pn && node != self){
                        nodes.push(node);
                        break;
                    }
                    cur = cur.parent;
                };
            };
        };
        return nodes;
    },
    getGLJList: function (treeNode, needOneBill) {
        delete treeNode.data.gljList;
        if (this.isRationCategory(treeNode)) {
            if (treeNode.data.type != rationType.volumePrice) {
                treeNode.data.gljList = projectObj.project.calcProgram.getGljArrByRation(treeNode.data);
            }
        }
        else if (this.isBill(treeNode)){
            let nodeQ = this.uiNodeQty(treeNode);
            let q = nodeQ ? nodeQ : 1;
            let rNodes = projectObj.project.Ration.getRationNodes(treeNode);
            let rations = rNodes.map(function (node) {return node.data});
            treeNode.data.gljList = projectObj.project.ration_glj.getGatherGljArrByRations(rations, needOneBill, q);
        };
    },
    getLeafBills: function (treeNode){
        let leafBills = [];

        function getBill(node){
            if (!node) return;
            if (calcTools.isLeafBill(node))
                leafBills.push(node);

            if (node.firstChild()) getBill(node.firstChild());
            if (node.nextSibling) getBill(node.nextSibling);
        };

        let fc = treeNode.firstChild();
        if (fc) getBill(fc);

        return leafBills;
    },
    forceSelect: function (treeNode, rowsCount = 1, colsCount = 2){
        projectObj.mainController.tree.selected = treeNode;
        let idx = projectObj.project.mainTree.items.indexOf(treeNode);
        let sheet = projectObj.mainSpread.getActiveSheet();
        sheet.setSelection(idx, 0, rowsCount, colsCount);
        sheet.showRow(idx, GC.Spread.Sheets.VerticalPosition.center);
    },
    clearFees: function (treeNode) {
        if (!treeNode || !treeNode.data.feesIndex) {
            return;
        }
        let clearFeeField = ['unitFee', 'totalFee', 'tenderUnitFee', 'tenderTotalFee'];
        for (let feeField in treeNode.data.feesIndex) {
            for (let subFee of clearFeeField) {
                treeNode.data.feesIndex[feeField][subFee] = 0;
            }
        }
    },
    initFees: function (treeNode){
        if (!treeNode.data.fees) {
            treeNode.data.fees = [];
            treeNode.data.feesIndex = {};
             treeNode.updateData.fees = [];   // 在某种未知情况下，treeNode.data.fees有值，导致这句无法执行。下同
             treeNode.updateData.feesIndex = {};
            treeNode.changed = true;
        }
        else if (!treeNode.data.feesIndex){
            treeNode.data.feesIndex = {};
             treeNode.updateData.feesIndex = {};
            for (let fee of treeNode.data.fees){
                treeNode.data.feesIndex[fee.fieldName] = fee;
                treeNode.updateData.feesIndex[fee.fieldName] = fee;
            };
            treeNode.changed = true;
        };
    },
    initFeeField: function (treeNode, fieldName){
        this.initFees(treeNode);
        if (!treeNode.data.feesIndex[fieldName]) {
            let fee = {
                'fieldName': fieldName,
                'unitFee': 0,
                'totalFee': 0,
                'tenderUnitFee': 0,
                'tenderTotalFee': 0
            };

            let newArr = treeNode.data.fees.filter(function (ele) {return ele.fieldName == fieldName});
            if (newArr.length > 0)
                fee = newArr[0]
            else
                treeNode.data.fees.push(fee);

            treeNode.data.feesIndex[fieldName] = fee;
            treeNode.updateData.fees = treeNode.data.fees;
            treeNode.changed = true;
        };
    },
    checkFeeField: function (treeNode, feeObj){
        if (!feeObj) return;
        if (feeObj.fieldName == '') return;

        // 初始化前，先拦截属性末定义、又要给该属性赋0的情况
        if (!treeNode.data.feesIndex || !treeNode.data.feesIndex[feeObj.fieldName]){
            if (feeObj.unitFee == 0 && feeObj.totalFee == 0) return;
        }

        this.initFeeField(treeNode, feeObj.fieldName);

        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;
        };

        // 不知在何种情况下，tenderUnitFee、tenderTotalFee的值会变成NaN,这里提前处理一下
        if (isNaN(treeNode.data.feesIndex[feeObj.fieldName].tenderUnitFee))
            treeNode.data.feesIndex[feeObj.fieldName].tenderUnitFee = undefined;
        if (isNaN(treeNode.data.feesIndex[feeObj.fieldName].tenderTotalFee))
            treeNode.data.feesIndex[feeObj.fieldName].tenderTotalFee = undefined;

        if (treeNode.data.feesIndex[feeObj.fieldName].tenderUnitFee != feeObj.tenderUnitFee){
            treeNode.data.feesIndex[feeObj.fieldName].tenderUnitFee = feeObj.tenderUnitFee;
            treeNode.changed = true;
        };

        if (treeNode.data.feesIndex[feeObj.fieldName].tenderTotalFee != feeObj.tenderTotalFee){
            treeNode.data.feesIndex[feeObj.fieldName].tenderTotalFee = feeObj.tenderTotalFee;
            treeNode.changed = true;
        };
    },
    setFieldValue: function (treeNode, fieldName, value){
        if (value == null) value = 0;
        if (fieldName.includes('feesIndex')){
            let arr = fieldName.split('.');
            this.initFeeField(treeNode, arr[1]);
            if (treeNode.data.feesIndex[arr[1]][arr[2]] != value) {
                treeNode.data.feesIndex[arr[1]][arr[2]] = value;
                treeNode.updateData.feesIndex = treeNode.data.feesIndex;
                if(fieldName == 'feesIndex.common.unitFee'){
                    let fee = _.find(treeNode.updateData.fees,{'fieldName':arr[1]});
                    if(fee) fee[arr[2]] = value;
                }
                treeNode.changed = true;
            }
        }
        else{
            if (treeNode.data[fieldName] != value){
                treeNode.updateData[fieldName] = value;
                treeNode.changed = true;
            }
        };
    },
    initSummaryFee: function (treeNode){
        if (!treeNode.data.summaryFees){
            treeNode.data.summaryFees = {
                totalFee: 0,
                estimateFee: 0,
                safetyFee: 0,
                chargeFee: 0
            };
            treeNode.updateData.summaryFees = {
                totalFee: 0,
                estimateFee: 0,
                safetyFee: 0,
                chargeFee: 0
            };
            treeNode.changed = true;
        };
    },
    hasAdjustPrice: function(){      // 编办是否有材料的调整价发文
        if (projectObj.project.projSetting.glj_col && projectObj.project.projSetting.glj_col.showAdjustPrice == true)
            return true
        else
            return false;
    },
    // 参数fieldName值: 'common.totalFee'、'equipment.unitFee'
    getFee: function (treeNode, fieldName) {
        if (!treeNode) return 0;
        let ns = fieldName.split(".");
        if (ns.length != 2)
            return 0
        else if (treeNode.data.feesIndex && treeNode.data.feesIndex[ns[0]] && treeNode.data.feesIndex[ns[0]][ns[1]])
            return parseFloat(treeNode.data.feesIndex[ns[0]][ns[1]])
        else
            return 0;
    },

    // 该方法已精简，不支持价差基数的计算。
    rationBaseFee: function (treeNode, gljTypes, priceType, isTender){
        if (!treeNode.data.gljList) return 0;
        let me = this, result = 0;
        let price = 0, temp = 0, temp2 = 0;
        for (let glj of treeNode.data.gljList) {
            if (gljTypes.indexOf(glj.type) >= 0) {
                if (isTender){
                    calcTools.calcGLJTenderPrice(glj);
                    calcTools.calcGLJTenderQty(treeNode, glj);
                };

                let gljQ = isTender ? me.uiGLJQty(glj["tenderQuantity"]) : me.uiGLJQty(glj["quantity"]);
                let mprice = isTender ? me.uiGLJPrice(glj["tenderPrice"], glj) : me.uiGLJPrice(glj["marketPrice"], glj);
                let aprice = calcTools.hasAdjustPrice() ? me.uiGLJPrice(glj["adjustPrice"], glj) : me.uiGLJPrice(glj["basePrice"], glj);

                if (priceType == priceTypes.ptBasePrice){ price = me.uiGLJPrice(glj["basePrice"], glj);}
                else if (priceType == priceTypes.ptAdjustPrice){price = aprice;}
                else if (priceType == priceTypes.ptMarketPrice){price = mprice;}

                let nodeQ = calcTools.uiNodeQty(treeNode);
                temp = (price * gljQ * nodeQ).toDecimal(decimalObj.process);
                result = (result + temp).toDecimal(decimalObj.process);
            };
        };
        result = result.toDecimal(decimalObj.ration.totalPrice);
        return result;
    },
    // masterTypeFilter 过滤机械机型：[]全部， [1，2]特大机械  [3，4]中小机械。  detailType 如机上人工费、机械折旧费等
    machineDetailFee: function (treeNode, gljArr, masterTypeFilter, detailType, isTender) {
        if (!gljArr) return 0;
        let result = 0;
        let nodeQ = calcTools.uiNodeQty(treeNode) ? calcTools.uiNodeQty(treeNode) : 0;
        for (let glj of gljArr) {
            if (baseMachineMasterTypes.includes(glj.type)){
                // 机型不符
                if ((masterTypeFilter.length > 0) && (glj.model && !masterTypeFilter.includes(glj.model))) continue;

                let gljQ;
                if (isTender){
                    calcTools.calcGLJTenderQty(treeNode, glj);
                    gljQ = glj.tenderQuantity;
                }
                else
                    gljQ = glj.quantity;
                // 获取机械组成物(调价不深入到组成物)
                let mds = projectObj.project.composition.getCompositionByGLJ(glj);
                if (!mds) mds = [];
                let mdSum = 0;
                for (let md of mds) {
                    if (md.type == detailType) {
                        let q = md["consumption"] ? md["consumption"] : 0;
                        let p = md["marketPrice"] ? md["marketPrice"] : 0;
                        mdSum = mdSum + (q * p).toDecimal(decimalObj.glj.unitPriceHasMix);
                        mdSum = (mdSum).toDecimal(decimalObj.glj.unitPriceHasMix);
                    }
                }
                result = (result + (nodeQ * gljQ * mdSum).toDecimal(decimalObj.process)).toDecimal(decimalObj.process)
            }
        }
        result = (result).toDecimal(decimalObj.ration.totalPrice);
        return result;
    },
    // 总造价清单、叶子清单、定额的暂估费。父清单是汇总子清单的暂估费，走计算程序逻辑，不在这里。
    estimateFee: function (treeNode, isBase, isTender){
        let me = this, sumU = 0, sumT = 0;
        let nodeQ = me.uiNodeQty(treeNode, isTender);
        let isGather = (projectObj.project.property.zanguCalcMode == zanguCalcType.gatherMaterial);

        // 先汇总数量，再乘市场价。如果是叶子清单，进入这里的gljList中的材料，已经是同类材料跨定额汇总过的了。
        function eTFee(){
            if (!treeNode.data.gljList) return 0;
            let GLJObjs = [];
            for (let glj of treeNode.data.gljList) {
                if (!allMaterialTypes.includes(glj.type)) continue;
                if (glj.isEstimate){
                    GLJObjs.push({code: glj.code, name: glj.name, specs: glj.specs, unit: glj.unit, type: glj.type,
                        // quantity: (nodeQ * glj.quantity).toDecimal(decimalObj.process),
                        quantity: me.uiGLJQty((glj.totalQuantity)).toDecimal(decimalObj.process),
                        marketPrice: glj.marketPrice});
                }
                else{   // 组成物
                    if (!compositionTypes.includes(glj.type)) continue;
                    let mds = projectObj.project.composition.getCompositionByGLJ(glj);
                    if (!mds) mds = [];
                    for (let md of mds){
                        if (md.isEstimate){
                            let isExist = false;
                            // let totalQ = (nodeQ * me.uiGLJQty(glj.quantity)).toDecimal(decimalObj.glj.quantity);
                            let totalQ = me.uiGLJQty((glj.totalQuantity)).toDecimal(decimalObj.glj.quantity);
                             let mdQ = (totalQ * me.uiGLJQty(md.consumption)).toDecimal(decimalObj.process);

                            for (let obj of GLJObjs){
                                if (gljOprObj.getIndex(md, gljKeyArray) == gljOprObj.getIndex(obj, gljKeyArray)){
                                    isExist = true;
                                    obj.quantity = (obj.quantity + mdQ).toDecimal(decimalObj.glj.quantity);
                                    break;
                                }
                            };
                            if (!isExist)
                                GLJObjs.push({code: md.code, name: md.name, specs: md.specs, unit: md.unit, type: md.type,
                                    quantity: mdQ, marketPrice: md.marketPrice});
                        }
                    }
                }
            };

            let rst = 0;
            for (let obj of GLJObjs){
                let tp = (me.uiGLJQty(obj.quantity) * me.uiGLJPrice(obj.marketPrice, obj)).toDecimal(decimalObj.bills.totalPrice);
                rst = (rst + tp).toDecimal(decimalObj.bills.totalPrice);
            };
            return rst;
        };
        // 汇总子结点的暂估合价
        function eTFeeByChildren(){
            let rst = 0;
            for (let node of treeNode.children){
                if (node.data.feesIndex && node.data.feesIndex['estimate']) {
                    rst = (rst + parseFloatPlus(node.data.feesIndex['estimate'].totalFee)).toDecimal(decimalObj.process);
                };
            };
            rst = (rst).toDecimal(decimalObj.bills.totalPrice);
            return rst;
        };
        // 先数量乘市场价，再汇总
        function eUFee(){
            if (!treeNode.data.gljList) return 0;
            let rst = 0;
            for (let glj of treeNode.data.gljList) {
                if (!allMaterialTypes.includes(glj.type)) continue;
                if (glj.isEstimate){
                    rst = rst + (me.uiGLJQty(glj.quantity) * me.uiGLJPrice(glj.marketPrice, glj)).toDecimal(decimalObj.process);
                    rst = rst.toDecimal(decimalObj.process);
                }
                else{   // 组成物
                    if (!compositionTypes.includes(glj.type)) continue;
                    let mds = projectObj.project.composition.getCompositionByGLJ(glj);
                    if (!mds) mds = [];
                    for (let md of mds){
                        if (!md.isEstimate) continue;
                        let mdU = (me.uiGLJQty(md.consumption) * me.uiGLJPrice(md.marketPrice)).toDecimal(decimalObj.glj.unitPrice);
                        rst = rst + (mdU * me.uiGLJQty(glj.quantity)).toDecimal(decimalObj.process);
                        rst = rst.toDecimal(decimalObj.process);
                    }
                }
            };
            rst = rst.toDecimal(decimalObj.bills.unitPrice);
            return rst;
        };

        // 总造价暂估费
        if (me.isTotalCostBill(treeNode)){
            let nodes = projectObj.project.mainTree.roots;
            for (let node of nodes){
                if (me.isTotalCostBill(node)) break;
                let eU = 0, eT = 0;
                if (node.data.feesIndex && node.data.feesIndex.estimate){
                    eU = node.data.feesIndex.estimate.unitFee;
                    eT = node.data.feesIndex.estimate.totalFee;
                }
                else {
                    eU = 0, eT = 0;
                };
                sumU = (sumU + parseFloatPlus(eU)).toDecimal(decimalObj.process);
                sumT = (sumT + parseFloatPlus(eT)).toDecimal(decimalObj.process);
            };
            sumU = (sumU).toDecimal(decimalObj.bills.unitPrice);
            sumT = (sumT).toDecimal(decimalObj.bills.totalPrice);
        }
        else if (me.isParentBill(treeNode)){  // 父清单不要汇总单价。
            sumT = eTFeeByChildren();
            sumU = undefined;
        }
        else if (me.isLeafBill(treeNode)){
            if (projectObj.project.Bills.isEngineerEst(treeNode)){
                sumT = treeNode.data.feesIndex['common'].totalFee;
                sumU = treeNode.data.feesIndex['common'].unitFee;
            }
            else{
                if (isGather){
                    me.getGLJList(treeNode, false);
                    sumT = eTFee();
                }
                else
                    sumT = eTFeeByChildren();

                let q = nodeQ ? nodeQ : 1;
                sumU = (sumT / q).toDecimal(decimalObj.bills.totalPrice);
            }
        }
        else if (me.isRationCategory(treeNode)){
            me.getGLJList(treeNode, false);

            sumU = eUFee();
            if (isBase) return sumU;

            if (isGather)
                sumT = eTFee()
            else
                sumT = (nodeQ * sumU).toDecimal(decimalObj.ration.totalPrice);
        };

        me.checkFeeField(treeNode, {'fieldName': 'estimate', 'unitFee': sumU, 'totalFee': sumT});
    },
    marketPriceToBase: function (treeNode, baseName, isTender) {
        if (treeNode.data.type != rationType.volumePrice && treeNode.data.type != rationType.gljRation) return;
        let result = 0, me = this;

        let rcj = (treeNode.data.subType === gljType.LABOUR && calcTools.inBase(baseName, 'RGF')) ||
            (baseMaterialTypes.includes(treeNode.data.subType) && calcTools.inBase(baseName, 'CLF')) ||
            (treeNode.data.subType === gljType.GENERAL_MACHINE && calcTools.inBase(baseName, 'JXF')) ||
            (treeNode.data.subType === gljType.EQUIPMENT && calcTools.inBase(baseName, 'SBF')) ||
            (treeNode.data.subType === gljType.MAIN_MATERIAL && calcTools.inBase(baseName, 'ZCF'));

            // (treeNode.data.subType === gljType.LABOUR && calcTools.inBase(baseName, 'SC_RGF')) ||
            // (baseMaterialTypes.includes(treeNode.data.subType) && calcTools.inBase(baseName, 'SC_CLF')) ||
            // (treeNode.data.subType === gljType.GENERAL_MACHINE && calcTools.inBase(baseName, 'SC_JXF')) ||
            // (treeNode.data.subType === gljType.EQUIPMENT && calcTools.inBase(baseName, 'SC_SBF'));

        if (rcj) {
            let nodeQ = calcTools.uiNodeQty(treeNode) ? calcTools.uiNodeQty(treeNode) : 0;
            if (treeNode.data.type == rationType.volumePrice){
                result = treeNode.data.marketUnitFee ? (parseFloat(treeNode.data.marketUnitFee) * nodeQ).toDecimal(decimalObj.ration.unitPrice) : 0
            }
            else if (treeNode.data.type == rationType.gljRation){
                let prc = 0;
                // if (treeNode.data.subType === gljType.EQUIPMENT && calcTools.inBase(baseName, 'SC_SBF'))
                //     prc = gljOprObj.getMarketPrice(treeNode)
                // else
                //     prc = gljOprObj.getBasePrice(treeNode);

                if (baseName.includes('定额'))
                    prc = gljOprObj.getBasePrice(treeNode)
                else
                    prc = treeNode.data.marketUnitFee ? parseFloat(treeNode.data.marketUnitFee).toDecimal(decimalObj.ration.unitPrice) : gljOprObj.getBasePrice(treeNode);

                result = (prc * nodeQ).toDecimal(decimalObj.ration.unitPrice);
            };

        }
        else if (treeNode.data.subType === gljType.GENERAL_MACHINE && calcTools.inBase(baseName, 'JSRGF')) {
            let glj = {
                'code': treeNode.data.code,
                'name': treeNode.data.name,
                'specs': treeNode.data.specs,
                'unit': treeNode.data.unit,
                'quantity': 1,
                'type': treeNode.data.subType      // 注意：这里要取subType
            };
            result = me.machineDetailFee(treeNode, [glj], [], gljType.MACHINE_LABOUR, isTender);
        }
        else if (
            (treeNode.data.type == rationType.gljRation) &&
            ((treeNode.data.subType === gljType.LABOUR && calcTools.inBase(baseName, 'JC_RGF')) ||
                (baseMaterialTypes.includes(treeNode.data.subType) && calcTools.inBase(baseName, 'JC_CLF')) ||
                (treeNode.data.subType === gljType.GENERAL_MACHINE && calcTools.inBase(baseName, 'JC_JXF')) ||
                (treeNode.data.subType === gljType.MAIN_MATERIAL && calcTools.inBase(baseName, 'JC_ZCF')) ||
                (treeNode.data.subType === gljType.EQUIPMENT && calcTools.inBase(baseName, 'JC_SBF')))
        ) {
            let aprice = me.uiGLJPrice(treeNode.data.basePrice);   // 量价虚拟的工料机不可能有发文，这里直接取定额价。
            let mprice = me.uiGLJPrice(treeNode.data.marketUnitFee);
            result = (mprice - aprice).toDecimal(decimalObj.ration.unitPrice);
        }
        return result;
    },
    partASupplyFee: function (treeNode, baseName, isTender, isRationPirce = true) {
        if (!treeNode.data.gljList) return 0;
        let projectGLJ = projectObj.project.projectGLJ;

        let supplyT = [];
        if (baseName.includes('甲供'))
            supplyT = [supplyType.BFJG, supplyType.WQJG]  // 字段中存储的是汉字、数字混杂！
        else if (baseName.includes('甲定'))
            supplyT = [supplyType.JDYG];

        let gljT = [], compT = [];
        if (calcTools.inBase(baseName, 'JG_RGF') || calcTools.inBase(baseName, 'JD_RGF')){
            gljT = [gljType.LABOUR];
        }
        else if (calcTools.inBase(baseName, 'JG_CLF') || calcTools.inBase(baseName, 'JD_CLF')){             // 甲供、甲定材料
            gljT = baseMaterialTypes;
            compT = compositionTypes;
        }
        else if (calcTools.inBase(baseName, 'JG_JXF') || calcTools.inBase(baseName, 'JD_JXF')) {            // 甲供、甲定机械
            gljT = baseMachineTypes;
            compT = [gljType.GENERAL_MACHINE, gljType.INSTRUMENT, gljType.OTHER_MACHINE_USED];              // 取并集，兼容重庆2018新定额
        }
        else if (calcTools.inBase(baseName, 'JG_ZCF') || calcTools.inBase(baseName, 'JD_ZCF')) {            // 甲供、甲定主材
            gljT = [gljType.MAIN_MATERIAL];
            compT = [gljType.MAIN_MATERIAL];
        }
        else if (calcTools.inBase(baseName, 'JG_SBF') || calcTools.inBase(baseName, 'JD_SBF')) {           // 甲供、甲定设备
            gljT = [gljType.EQUIPMENT];
        };
        // alert(JSON.stringify(projectGLJ.testGLJs()));
        let supplyGLJs = projectGLJ.getGLJsBySupply(supplyT, gljT);
        if (supplyGLJs.length == 0) return 0;
        let supplyGLJsIdx = {};
        for (let sglj of supplyGLJs){supplyGLJsIdx[sglj.id] = sglj};

        function isSupply(composition, supplies) {
            for (let supply of supplies){
                if(supply.code == composition.code && supply.name == composition.name && supply.unit == composition.unit &&
                    supply.specs == composition.specs && supply.type == composition.type ){
                    composition.basePrice = supply.unit_price.base_price;
                    composition.marketPrice = supply.unit_price.market_price;
                    composition.supply = supply.supply;
                    composition.supplyX = 1;
                    if (composition.supply == supplyType.BFJG){
                        let Q = supply.quantity ? supply.quantity : 1;
                        composition.supplyX = supply.supply_quantity / Q;
                    }
                    return true;
                }
            };
            return false;
        };

        let sum = 0;
        for (let glj of treeNode.data.gljList){
            let gljQ, gljP;
            if (isTender){
                calcTools.calcGLJTenderQty(treeNode, glj);
                gljQ = glj.tenderQuantity;
            }
            else
                gljQ = glj.quantity;

            gljP = isRationPirce ? glj.basePrice : glj.marketPrice;

            let X = 1;    // 部分甲供系数(默认1，即完全甲供)
            let tempSGLJ = supplyGLJsIdx[glj.projectGLJID];
            // 当前材料是甲供材料：①普通 ②商品硂等不计组成物的母体材料
            if (tempSGLJ) {
                // 处理部分甲供
                if (baseName.includes('甲供') && (tempSGLJ.supply == supplyType.BFJG)){
                    // let Q =  isTender ? tempSGLJ.tenderQuantity : tempSGLJ.quantity;
                    let Q = tempSGLJ.quantity;
                    Q = Q ? Q : 1;
                    X = tempSGLJ.supply_quantity / Q;
                }
                sum = (sum + gljP * gljQ * X).toDecimal(decimalObj.process);
            }
            else{   // 当前材料不是甲供材料
                if (compT.includes(glj.type)) {   // 混凝土等。组成物的母体，母体如果有组成物，则母体无法作为甲供材料，无法设置，此时要看其组成物是否是甲供材料；母体如果没有组成物，则母体有可能成为甲供材料。
                    let pGLJ = projectGLJ.getDataByID(glj.projectGLJID);
                    let compositions = pGLJ.ratio_data;    // 组成物明细
                    if (compositions.length > 0){
                        for (let c of compositions){
                            if (isSupply(c, supplyGLJs)) {
                                X = 1;
                                if (baseName.includes('甲供') && (c.supply == supplyType.BFJG)){
                                    X = c.supplyX;
                                };
                                let cP = isRationPirce ? c.basePrice : c.marketPrice;
                                sum = (sum + cP * c.consumption * gljQ * X).toDecimal(decimalObj.process);
                            }
                        }
                    };
                }
            };
        };
        sum = sum.toDecimal(decimalObj.ration.unitPrice);
        return sum;
    },
    getCalcType: function (treeNode) {
        if (this.isRationCategory(treeNode)){
            return treeNodeCalcType.ctRationCalcProgram;
        }
        else if (this.isNullBill(treeNode)){
            return treeNodeCalcType.ctNull;
        }
        else if (this.isLeafBill(treeNode)) {
            if (treeNode.children && treeNode.children.length > 0){
                // 清单单价计算模式下的叶子清单：取自己的计算程序ID，找到自己的计算程序计算。(汇总清单所有定额的工料机)
                // if (projectObj.project.property.billsCalcMode === leafBillGetFeeType.billsPrice)
                //     return treeNodeCalcType.ctBillCalcProgram;
                // else                                        // 前三种计算模式下的叶子清单：汇总定额的计算程序的费用类别
                    return treeNodeCalcType.ctGatherRationsFees;
            }
            else{                                          // 公式计算
                return treeNodeCalcType.ctCalcBaseValue;
            };
        }
        else if (this.isBill(treeNode)) {                              // 父清单：汇总子清单的费用类别
            return treeNodeCalcType.ctGatherBillsFees;
        }
        else {
            return treeNodeCalcType.ctRationCalcProgram;
        };
    },
    cutNodeForSave(treeNode){
        let me = this;
        /*  subType、quantity、calcBase、programID、marketUnitFee等等字段较为特殊，它们的改变一定会触发计算并导致计算
            结果的变化，从而引发保存动作。将这些字段放在该位置跟计算结果一起保存，可减少前端跟后端的通讯频率。              */
        let data = treeNode.updateData;
        data.projectID = projectObj.project.ID();
        data.ID = treeNode.data.ID;
        //对比计算结果与原数据的值，有改变的才提交保存
        if(treeNode.temData){
            for(let key in treeNode.data){
                if(key == "feesIndex" || key =="flagsIndex" || key =="gljList" || key =="calcTemplate") continue;// 这两个字段不用保存到后台;
                if(treeNode.data[key]&& typeof treeNode.data[key] == "object"){//对象类型转换成字符串比较
                    if(Array.isArray(treeNode.data[key])) treeNode.data[key].sort();
                    if(Array.isArray(treeNode.temData[key])) treeNode.temData[key].sort();
                    if(JSON.stringify(treeNode.temData[key])!== JSON.stringify(treeNode.data[key]) ){
                        data[key] = treeNode.data[key];
                    }
                }else if(treeNode.temData[key]!==treeNode.data[key]){
                    data[key] = treeNode.data[key];
                }
            }
        }
        // 定额大类
        if (me.isRationCategory(treeNode)) {
            // 这4个位置要分别找到更新的地方按新的来更新
           // data.isSubcontract = treeNode.data.isSubcontract;
           // data.evaluationProject = treeNode.data.evaluationProject;
            //定额类型的工料机做特殊处理
            if(me.isGljRation(treeNode)){
             //   data.code = treeNode.data.code;
              //  data.projectGLJID = treeNode.data.projectGLJID;
                delete data.marketUnitFee;
            };
        };
        // 优化掉 undefined 属性
        data = JSON.parse(JSON.stringify(data));
        delete data.feesIndex;
        return data;
    },

    uiNodeQty: function (treeNode){
        return parseFloatPlus(treeNode.data.quantity).toDecimal(decimalObj.decimal("quantity", treeNode));
    },
    uiNodeTenderQty: function (treeNode){
        return this.calcNodeTenderQty(treeNode);
    },
    calcNodeTenderQty: function (treeNode){
        let qCoe = (treeNode.data.rationQuantityCoe == undefined) ? 1 : treeNode.data.rationQuantityCoe;
        treeNode.data.tenderQuantity = (this.uiNodeQty(treeNode) * qCoe).toDecimal(decimalObj.decimal("quantity", treeNode));
        return treeNode.data.tenderQuantity;
    },
    calcGLJTenderQty: function (treeNode, glj){
        if (treeNode.data.quantityCoe == undefined){
            glj.tenderQuantity = glj.quantity;
            return;
        };

        let qCoe = 1;
        if (gljType.LABOUR == glj.type){
            if (treeNode.data.quantityCoe.labour)
                qCoe = treeNode.data.quantityCoe.labour;
        }
        else if (baseMaterialTypes.indexOf(glj.type)){
            if (treeNode.data.quantityCoe.material)
                qCoe = treeNode.data.quantityCoe.material;
        }
        else if (baseMachineTypes.indexOf(glj.type)){
            if (treeNode.data.quantityCoe.machine)
                qCoe = treeNode.data.quantityCoe.machine;
        }
        else if (gljType.MAIN_MATERIAL == glj.type){
            if (treeNode.data.quantityCoe.main)
                qCoe = treeNode.data.quantityCoe.main;
        }
        else if (gljType.EQUIPMENT == glj.type){
            if (treeNode.data.quantityCoe.equipment)
                qCoe = treeNode.data.quantityCoe.equipment;
        };

        glj.tenderQuantity = (glj.quantity * qCoe).toDecimal(decimalObj.glj.quantity);
    },
    calcGLJTenderPrice: function (glj) {
        if (!glj.projectGLJID){   // 量价定额虚拟出来的工料机，没有projectGLJID，在项目工料机中查不到。
            let pCoe = 1;
            if (projectObj.project.property.tenderSetting && projectObj.project.property.tenderSetting.gljPriceTenderCoe)
                pCoe = projectObj.project.property.tenderSetting.gljPriceTenderCoe;
            glj.tenderPrice = (glj.marketPrice * pCoe).toDecimal(decimalObj.glj.unitPrice);
        }else{
            let projGLJ = calcTools.getProjectGLJ(glj);
            glj.tenderPrice = projectObj.project.projectGLJ.getTenderMarketPrice(projGLJ);
        };
    },
    // 界面显示的工料机价格，包括定额价、市场价等。参数 price 传入一个普通的价格数值即可。
    uiGLJPrice: function (price, glj){
        if (price){
            let projGLJ = glj ? calcTools.getProjectGLJ(glj) : null;
            let d = (projGLJ && projGLJ.ratio_data.length > 0) ? decimalObj.glj.unitPriceHasMix : decimalObj.glj.unitPrice;
            return parseFloat(price).toDecimal(d);
        }
        else return 0;
    },
    // 界面显示的工料机数量。参数 quantity 传入一个普通的数量数值即可。
    uiGLJQty: function (quantity){
        if (quantity)
            return parseFloat(quantity).toDecimal(decimalObj.glj.quantity)
        else return 0;
    },
    
    getRationsByProjectGLJ(PGLJID){
        let rationIDs = [];
        let RGs = projectObj.project.ration_glj.datas;
        let PGLJIDs = [];
        if(Array.isArray(PGLJID)){//为了提高效率，改成支持传入数组
            PGLJIDs = PGLJID
        }else {
            PGLJIDs = [PGLJID];
        }
        for (let rg of RGs){
            if (PGLJIDs.indexOf(rg.projectGLJID) !== -1){
                rationIDs.push(rg.rationID);
            }
        };

        let rationNodes = [];
        let nodes = projectObj.project.mainTree.nodes;
        for (let rID of rationIDs){
            rationNodes.push(nodes['id_' + rID]);
        };

        // 工料机形式的定额
        let items = projectObj.project.mainTree.items;
        for (let item of items){
            if (PGLJIDs.indexOf(item.data.projectGLJID)  !== -1)
                rationNodes.push(item);
        };

        return rationNodes;
    },
    getNodesByProgramID(programID){
        let discreteNodes = [];
        let nodes = projectObj.project.mainTree.items;
        for (let node of nodes){
            if (node.data.programID == programID)
                discreteNodes.push(node);
        };
        return discreteNodes;
    },
    getProjectGLJ(glj){
        if (glj.projectGLJID){
            return projectObj.project.projectGLJ.getDataByID(glj.projectGLJID);
        }
        else return null;
    },
    labourDays(node, isTender){
        if (!node.data.gljList) return 0;
        let rst = 0;
        calcTools.uiNodeQty(node);
        for (let glj of node.data.gljList) {
            if (glj.type == gljType.LABOUR) {
                let gljQ;
                if (isTender){
                    calcTools.calcGLJTenderQty(node, glj);
                    gljQ = glj.tenderQuantity.toDecimal(decimalObj.glj.quantity);
                }
                else
                    gljQ = glj.quantity.toDecimal(decimalObj.glj.quantity);

                // rst = rst + (gljQ * calcTools.uiNodeQty(node)).toDecimal(decimalObj.process);
                // rst = rst.toDecimal(decimalObj.process);
                rst = (rst + gljQ).toDecimal(decimalObj.process);
            }
        };
        return rst.toDecimal(decimalObj.glj.quantity);
    },
    getProjectFeatureProperty(propertyKey){
        for (let o of projectObj.project.property.projectFeature){
              if (o.key == propertyKey){
                  return o.value;
              }
        };
    },
    inBase(baseName, kindName){
        return rationCalcBasesNameKinds[kindName].includes(baseName);
    },
    isBudgetProject(){
        return projectObj.project.property.valuationType == 'bill';
    },
    isBillProject(){
        return projectObj.project.property.valuationType == 'ration';
    }
};

let rationCalcBasesNameKinds = {
    RGF:        ['人工费', '定额基价人工费', '定额人工费'],
    CLF:        ['材料费', '定额基价材料费', '定额材料费'],
    QTCLF:      ['定额其他材料费'],
    JXF:        ['机械费', '施工机具使用费', '施工机械使用费', '定额基价机械费', '定额机械费', '定额施工机具使用费', '定额施工机械使用费'],
    JSRGF:      ['定额基价机上人工费', '定额施工机械人工费', '施工机械人工费'],
    ZCF:        ['主材费', '市场价主材费'],
    SBF:        ['设备费', '市场价设备费', '设备购置费','定额设备费'],
    RGGR:       ['人工工日'],
    // ZGCLF:      ['暂估材料费'],

    // 市场价人工费、市场价材料费、市场价机械费。江西用，跟重庆区分开，概念上不太统一，这里要注意区分。
    // SC_RGF:     ['人工费'],
    // SC_CLF:     ['材料费'],
    // SC_JXF:     ['机械费', '施工机具使用费', '施工机械使用费'],
    // SC_JSRGF:   ['施工机械人工费'],
    SC_SBF:     ['设备费', '市场价设备费', '设备购置费'],

    JC_RGF:     ['人工费价差'],
    JC_CLF:     ['材料费价差'],
    JC_JXF:     ['机械费价差'],
    JC_ZCF:     ['主材费价差'],
    JC_SBF:     ['设备费价差'],

    JG_RGF:     ['甲供定额基价人工费', '甲供定额人工费', '甲供人工费'],
    JG_CLF:     ['甲供定额基价材料费', '甲供定额材料费', '甲供材料费'],
    JG_JXF:     ['甲供定额基价机械费', '甲供定额机械费', '甲供机械费', '甲供定额施工机具费'],
    JG_ZCF:     ['甲供主材费'],
    JG_SBF:     ['甲供设备费'],

    JD_RGF:     ['甲定定额基价人工费', '甲定定额人工费', '甲定人工费'],
    JD_CLF:     ['甲定定额基价材料费', '甲定定额材料费', '甲定材料费'],
    JD_JXF:     ['甲定定额基价机械费', '甲定定额机械费', '甲定机械费', '甲定定额施工机具费'],
    JD_ZCF:     ['甲定主材费'],
    JD_SBF:     ['甲定设备费'],

    FB_RGF:     ['分包定额基价人工费', '分包人工费'],
    FB_CLF:     ['分包定额基价材料费', '分包材料费'],
    FB_JXF:     ['分包定额基价机械费', '分包机械费'],
    FB_ZCF:     ['分包主材费'],
    FB_SBF:     ['分包设备费'],
    FB_RGGR:    ['分包人工工日']
};

let rationCalcBases = {
    '人工费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.LABOUR], priceTypes.ptMarketPrice, isTender);
    },
    '材料费': function (node, isTender) {
        return calcTools.rationBaseFee(node, baseMaterialTypes, priceTypes.ptMarketPrice, isTender);
    },
    '施工机械使用费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.GENERAL_MACHINE], priceTypes.ptMarketPrice, isTender);
    },
    '施工机械人工费': function (node, isTender) {
        return calcTools.machineDetailFee(node, node.data.gljList, [], gljType.MACHINE_LABOUR, isTender);
    },
    '定额商品砼费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.COMMERCIAL_CONCRETE, gljType.COMMERCIAL_MORTAR], priceTypes.ptBasePrice, isTender);
    },
    '定额外购砼构件费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.PURCHASE_COMPONENT], priceTypes.ptBasePrice, isTender);
    },
    '定额绿化苗木费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.GREEN_SEEDLING], priceTypes.ptBasePrice, isTender);
    },
    '设备购置费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.EQUIPMENT], priceTypes.ptMarketPrice, isTender);
    },
    '定额人工费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.LABOUR], priceTypes.ptBasePrice, isTender);
    },
    '定额材料费': function (node, isTender) {
        return calcTools.rationBaseFee(node, baseMaterialTypes, priceTypes.ptBasePrice, isTender);
    },
    '定额施工机械使用费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.GENERAL_MACHINE], priceTypes.ptBasePrice, isTender);
    },
    '定额设备费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.EQUIPMENT], priceTypes.ptBasePrice, isTender);
    }

    /*
    '人工费价差': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.LABOUR], priceTypes.ptDiffPrice, isTender);
    },
    '材料费价差': function (node, isTender) {
        return calcTools.rationBaseFee(node, baseMaterialTypes, priceTypes.ptDiffPrice, isTender);
    },
    '机械费价差': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.GENERAL_MACHINE], priceTypes.ptDiffPrice, isTender);
    },
    '主材费价差': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.MAIN_MATERIAL], priceTypes.ptDiffPrice, isTender);
    },
    '设备费价差': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.EQUIPMENT], priceTypes.ptDiffPrice, isTender);
    },
    '主材费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.MAIN_MATERIAL], priceTypes.ptBasePrice, isTender);
    },
    '设备费': function (node, isTender) {
        return calcTools.rationBaseFee(node, [gljType.EQUIPMENT], priceTypes.ptBasePrice, isTender);
    },
    '人工工日': function (node, isTender) {
        return calcTools.labourDays(node, isTender);
    },
    '甲供定额基价人工费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲供定额基价人工费', isTender);
    },
    '甲供定额基价材料费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲供定额基价材料费', isTender);
    },
    '甲供定额基价机械费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲供定额基价机械费', isTender);
    },
    '甲供主材费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲供主材费', isTender);
    },
    '甲供设备费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲供设备费', isTender);
    },
    '甲定定额基价人工费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲定定额基价人工费', isTender);
    },
    '甲定定额基价材料费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲定定额基价材料费', isTender);
    },
    '甲定定额基价机械费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲定定额基价机械费', isTender);
    },
    '甲定主材费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲定主材费', isTender);
    },
    '甲定设备费': function (node, isTender) {
        return calcTools.partASupplyFee(node, '甲定设备费', isTender);
    },
    '暂估材料费': function (node, isTender) {
        return calcTools.estimateFee(node, true, isTender);
    },
    '分包定额基价人工费': function (node, isTender) {
        if (node.data.isSubcontract)
            return calcTools.rationBaseFee(node, [gljType.LABOUR], priceTypes.ptBasePrice, isTender)
        else
            return 0;
    },
    '分包定额基价材料费': function (node, isTender) {
        if (node.data.isSubcontract)
            return calcTools.rationBaseFee(node, baseMaterialTypes, priceTypes.ptBasePrice, isTender)
        else
            return 0;
    },
    '分包定额基价机械费': function (node, isTender) {
        if (node.data.isSubcontract)
            return calcTools.rationBaseFee(node, [gljType.GENERAL_MACHINE], priceTypes.ptBasePrice, isTender)
        else
            return 0;
    },
    '分包主材费': function (node, isTender) {
        if (node.data.isSubcontract)
            return calcTools.rationBaseFee(node, [gljType.MAIN_MATERIAL], priceTypes.ptBasePrice, isTender)
        else
            return 0;
    },
    '分包设备费': function (node, isTender) {
        if (node.data.isSubcontract)
            return calcTools.rationBaseFee(node, [gljType.EQUIPMENT], priceTypes.ptBasePrice, isTender)
        else
            return 0;
    },
    '分包人工工日': function (node, isTender) {
        if (node.data.isSubcontract)
            return calcTools.labourDays(node, isTender)
        else
            return 0;
    }*/
};

let analyzer = {
    error: '',
    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
        str = str.replace(/l/g, "L");               // l换成L
        return str;
    },
    getFArr: function (expr) {
        let pattF = new RegExp(/F\d+/gi);
        let arrF = expr.match(pattF);
        return arrF ? arrF : [];
    },
    getAtIDArr: function (expr) {     // ['@1','@2']
        let patt = new RegExp(/@\d+/gi);
        let arr = expr.match(patt);
        return arr ? arr : [];
    },
    getBaseArr: function (expr) {
        let pattBase = new RegExp(/\[[\u4E00-\u9FA5]+\]/gi);
        let arrBase = expr.match(pattBase);
        return arrBase ? arrBase : [];
    },

    getID: function (FName, template) {          // F13 → 4
        let idx = FName.slice(1) - 1;
        let id = template.calcItems[idx] ? template.calcItems[idx].ID : null;
        return id;
    },
    getFName: function (atID, template) {          // @3 → F7
        for (var i = 0; i < template.calcItems.length; i++) {
            if (template.calcItems[i].ID == atID.slice(1)) {
                return 'F'+ (i + 1);
            }
        }
        return null;
    },
    getItem: function (FName, template){      // F3 → calcItems[2] → {Object}
        let idx = FName.slice(1) - 1;
        return template.calcItems[idx];
    },
    isCycleCalc: function (expression, ID, template) {     // 这里判断expression，是ID引用: @5+@6
        let atID = `@${ID}`;
        // 避免部分匹配，如：@10匹配@1
        let idx = expression.indexOf(atID);
        if (idx >= 0){
            let nextPos = idx + atID.length;
            if (nextPos >= expression.length)
                return true;

            let char = expression.charAt(nextPos);
            if (!char.isNumberStr())
                return true;
        };

        let atIDs = analyzer.getAtIDArr(expression);
        for (let atID of atIDs) {
            let item = template.compiledCalcItems[atID.slice(1)];
            if (analyzer.isCycleCalc(item.expression, ID, template))
                return true;
        }

        return false;
    },
    isLegal: function (dispExpr, itemID, template) {  // 检测包括：无效字符、基数是否中括号、基数是否定义、行引用、循环计算

        function testValue(expr){
            try {
                expr = expr.replace(/\[[\u4E00-\u9FA5]+\]/gi, '0');
                expr = expr.replace(/@\d+/gi, '0');
                expr = expr.replace(/L/gi, '0');
                expr = expr.replace(/%/gi, '*0.01');
                if (expr.includes('00'))
                    return false;

                eval(expr);
                return true;
            }
            catch (err) {
                return false;
            }
        };

        let me = analyzer;
        let expr = me.standard(dispExpr);
        let invalidChars = /[^0-9\u4e00-\u9fa5\+\-\*\/\(\)\.\[\]FL%]/g;
        if (invalidChars.test(expr)){
            analyzer.error = `表达式中含有${hintBox.font('无效字符')}！`;
            // hintBox.infoBox('错误提示',`表达式中含有${hintBox.font('无效字符')}！`,1);
            return false;
        };

        let i = expr.search(/\df/ig);   // 4F3
        let j = expr.search(/\d\[/ig);  // 4[定额基价人工费]
        if (i > -1 || j > -1){
            analyzer.error = `表达式中${hintBox.font('缺少运算符')}！`;
            // hintBox.infoBox('错误提示', `表达式中${hintBox.font('缺少运算符')}！`, 1);
            return false;
        }

        let pattCn = new RegExp(/[\u4E00-\u9FA5]+/gi);
        let arrCn = expr.match(pattCn);
        let pattBase = new RegExp(/\[[\u4E00-\u9FA5]+\]/gi);
        let arrBase = expr.match(pattBase);
        if (arrCn && !arrBase){
            analyzer.error = `定额基数必须用中括号${hintBox.font('[]')}括起来！`;
            // hintBox.infoBox('错误提示', `定额基数必须用中括号${hintBox.font('[]')}括起来！`, 1);
            return false;
        };
        if (arrCn && arrBase && (arrCn.length != arrBase.length)){
            for (let cn of arrCn){
                let tempBase = `[${cn}]`;
                  if (!arrBase.includes(tempBase)){
                      analyzer.error = `定额基数“${hintBox.font(cn)}”必须用中括号${hintBox.font('[]')}括起来！`;
                      // hintBox.infoBox('错误提示', `定额基数“${hintBox.font(cn)}”必须用中括号${hintBox.font('[]')}括起来！`, 1);
                      return false;
                  }
            };
            // 这里要加一个保险。因为上面的 for 循环在“主材费 + [主材费]” 情况下有Bug
            analyzer.error =`定额基数必须用中括号${hintBox.font('[]')}括起来！`;
            // hintBox.infoBox('错误提示', `定额基数必须用中括号${hintBox.font('[]')}括起来！`, 1);
            return false;
        };
        if (arrBase){
            for (let base of arrBase){
                let baseName = base.slice(1, -1);
                if (!rationCalcBases[baseName]){
                    analyzer.error = `定额基数${hintBox.font('[' +baseName + ']')}未定义！`;
                    // hintBox.infoBox('错误提示', `定额基数${hintBox.font('[' +baseName + ']')}未定义！`, 1);
                    return false;
                }
            };
        };

        let arrF = me.getFArr(expr);
        for (let F of arrF){
            let num = F.slice(1);
            if (num > template.calcItems.length){
                let s = hintBox.font('F'+num);
                analyzer.error = `表达式中 “${hintBox.font(s)}” 行号引用错误！`;
                // hintBox.infoBox('错误提示', `表达式中 “${hintBox.font(s)}” 行号引用错误！`, 1);
                return false;
            };
        };

        let expression = me.getExpression(expr, template);
        if (me.isCycleCalc(expression, itemID, template)){
            analyzer.error = `表达式中有${hintBox.font('循环计算')}！`;
            // hintBox.infoBox('错误提示', `表达式中有${hintBox.font('循环计算')}！`, 1);
            return false;
        };
        if (!testValue(expression)){
            analyzer.error = `表达式中有${hintBox.font('语法错误')}！`;
            // hintBox.infoBox('错误提示', `表达式中有${hintBox.font('语法错误')}！`, 1);
            return false;
        };

        return true;  // 表达式合法
    },
    refIDToLines: function (expression, template) {    // @2、@3 → F7、F8
        let rst = expression;
        let atIDArr = analyzer.getAtIDArr(rst);
        let fArr = [];
        for (let atID of atIDArr) {
            let FN = analyzer.getFName(atID, template);
            fArr.push(FN);
        };
        for (let i = 0; i < atIDArr.length; i++) {
            let patt = new RegExp(atIDArr[i]);
            let val = fArr[i];
            rst = rst.replace(patt, val);
        };
        return rst;
    },
    refLineToIDs: function (dispExpr, template) {   // F7、F8 → @2、@3
        let rst = analyzer.standard(dispExpr);
        let fArr = analyzer.getFArr(rst);
        let IDArr = [];
        for (let F of fArr) {
            let ID = analyzer.getID(F, template);
            IDArr.push(ID);
        };
        for (let i = 0; i < fArr.length; i++) {
            let patt = new RegExp(fArr[i]);
            let val = `@${IDArr[i]}`;
            rst = rst.replace(patt, val);
        };
        return rst;
    },
    getDispExpr: function (expression, template) {
        return analyzer.refIDToLines(expression, template);
    },
    getExpression: function (dispExpr, template) {
        return analyzer.refLineToIDs(dispExpr, template);
    },
    getDispExprUser: function (dispExpr, labourCoe) {   // labourCoe 是 str 类型
        let rst = analyzer.standard(dispExpr);
        rst = rst.replace(/L/g, labourCoe);
        return rst;
    },
    getCompiledExpr: function (expression, labourCoe) {   // labourCoe 是 str 类型
        let rst = expression;
        let atIDArr = analyzer.getAtIDArr(rst);
        let IDArr = atIDArr.map(function (atID) {
            return atID.slice(1);
        });
        for (var i = 0; i < atIDArr.length; i++) {
            let patt = new RegExp(atIDArr[i]);
            let val = `$CE.at(${IDArr[i]},false)`;
            rst = rst.replace(patt, val);
        };
        rst = rst.replace(/%/g, "*0.01");
        rst = rst.replace(/\[/g, "$CE.base('");
        rst = rst.replace(/\]/g, "',false)");
        rst = rst.replace(/L/g, labourCoe);
        return rst;
    },
    getCompiledTenderExpr: function (compiledExpr) {
        let rst = compiledExpr.replace(/false/g, "true");
        return rst;
    },
    getStatement: function (expression, template) {
        let rst = expression;
        let atIDArr = analyzer.getAtIDArr(rst);
        let IDArr = atIDArr.map(function (atID) {
            return atID.slice(1);
        });
        for (var i = 0; i < atIDArr.length; i++) {
            let patt = new RegExp(atIDArr[i]);
            let val = projectObj.project.calcProgram.compiledTemplates[template.ID].compiledCalcItems[IDArr[i]].name;
            rst = rst.replace(patt, val);
        };
        rst = rst.replace(/\[/g, "");
        rst = rst.replace(/\]/g, "");
        rst = rst.replace(/L/g, '人工系数');
        return rst;
    },

    calcItemMaxID: function(template){
        let MaxID = 0;
        for (let item of template.calcItems){
            if (item.ID > MaxID)
                MaxID = item.ID;
        };
        return MaxID;
    },
    calcItemIsUsed: function(template, calcItem){
        let atID = '@' + calcItem.ID;
        for (var i = 0; i < template.calcItems.length; i++) {
            let item = template.calcItems[i];
            let atIDArr = analyzer.getAtIDArr(item.expression);
            if (atIDArr.indexOf(atID) >= 0){
                calcItem.tempUsed = i;
                return true;
            }
        }
        return false;
    },
    refreshUsedCalcItemsStatement: function(template, calcItem){
        let atID = '@' + calcItem.ID;
        for (var i = 0; i < template.calcItems.length; i++) {
            let item = template.calcItems[i];
            let atIDArr = analyzer.getAtIDArr(item.expression);
            if (atIDArr.indexOf(atID) >= 0){
                item.statement = analyzer.getStatement(item.expression, template);
            };
        }
    },

    calcItemLabourCoe: function(calcItem){
        let lc = 0;
        if (calcItem.labourCoeID)
            lc = projectObj.project.calcProgram.compiledLabourCoes[calcItem.labourCoeID].coe.toString();
        return lc;
    },
    templateRefresh: function(template){
        for (let item of template){
            item.dispExpr = analyzer.getDispExpr(item.expression, template);
            item.dispExprUser = analyzer.getDispExprUser(item.dispExpr, me.calcItemLabourCoe(item));
        };
    },
    templateMaxID: function(){
        let ts = projectObj.project.calcProgram.templates;
        let MaxID = 0;
        for (let t of ts){
            if (t.ID > MaxID)
                MaxID = t.ID;
        };
        return MaxID;
    },
    templateNewName: function (name) {
        let i = 1;
        while (projectObj.project.calcProgram.compiledTemplateMaps[name + i]) {
            i++;
        };
        return name + i;
    },
    templateNameIsExist: function (name) {
        if (projectObj.project.calcProgram.compiledTemplateMaps[name])
            return true
        else return false;
    },
    templateIsUsed: function (ID){
        let nodes = projectObj.project.mainTree.items;
        for (let node of nodes){
              if (node.data && node.data.programID && node.data.programID == ID) {
                  return true;
              }
        };
        return false;
    },
    fieldNameIsUsed: function(template, fieldName){
        let fieldNameEn = projectObj.project.calcProgram.compiledFeeTypeMaps[fieldName];
        for (var i = 0; i < template.calcItems.length; i++) {
            if (template.calcItems[i].fieldName == fieldNameEn){
                template.fieldNameTempUsed = i;
                return true;
            }
        }
        return false;
    }
};

let executeObj = {
    treeNode: null,
    template: null,
    tempCalcItem: null,

    at: function(ID, isTender) {
        let item = executeObj.template.compiledCalcItems[ID];
        let rst = isTender ? item.tenderTotalFee : item.totalFee;
        rst = parseFloat(rst);
        return rst;
    },
    base: function(baseName, isTender) {
        let me = executeObj;

        // 量价、工料机形式的定额, 要把自己的市场单价用于计算程序中的基数。
        if (calcTools.isVolumePrice(me.treeNode) || calcTools.isGljRation(me.treeNode))
            return calcTools.marketPriceToBase(me.treeNode, baseName, isTender)
        else{
            if (!rationCalcBases[baseName]){
                hintBox.infoBox('系统提示', '定额基数“' + baseName + '”未定义，计算错误。 (模板 ' + me.template.ID + '，规则 ' + me.tempCalcItem.ID +')', 1);
                return 0;
            }
            else
                return rationCalcBases[baseName](me.treeNode, isTender);
        }
    },
    HJ: function () {
        let me = this;
        let p = me.treeNode.data.calcBaseValue ? me.treeNode.data.calcBaseValue : 0;
        let q = calcTools.uiNodeQty(me.treeNode) ? calcTools.uiNodeQty(me.treeNode) : 1;
        let u = (p / q).toDecimal(decimalObj.decimal('unitPrice', me.treeNode));
        return u;
    }
};

class CalcProgram {
    constructor(project){
        let me = this;
        me.project = project;
        me.datas = [];
        me.rationMap = null;//定额 - 工料机映射临时变量
        me.pgljMap = null;
        project.registerModule(ModuleNames.calc_program, me);
    };

    // 兼容Project框架方法
    getSourceType () {
        return ModuleNames.calc_program;
    };
    // 兼容Project框架方法
    loadData (datas) {
        this.datas = datas;
        this.compileAllTemps();
    };
    // 兼容Project框架方法
    doAfterUpdate (err, data) {
        if(!err){
            $.bootstrapLoading.end();
        }
    };

    // 经测试，全部编译一次耗时0.003~0.004秒。耗时基本忽略不计。
    compileAllTemps(){
        let me = this;
        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 = cpFeeTypes;
        // me.calcBases = rationCalcBase;
        me.templates = this.project.calcProgram.datas.templates;

        // me.templates.push(defaultBillTemplate);
        // 先编译公用的基础数据
        me.compilePublics();
        me.compileTemplateMaps();
        for (let t of me.templates){
            me.compileTemplate(t);
        };


        // 存储费率临时数据，报表用。
        // if (me.saveForReports.length > 0){
        //     let saveDatas = {};
        //     saveDatas.projectID = projectObj.project.projectInfo.ID;
        //     saveDatas.calcItems = me.saveForReports;
        //     CommonAjax.post('/calcProgram/saveCalcItems', saveDatas, function (result) {
        //         if (result){
        //             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;         // 中文预编译，可靠性有待验证
        }*/
    };
    compileTemplateMaps(){
        let me = this;

        function clearObj(obj){
            for (let key in obj) {
                delete obj[key];
            }
        };

        clearObj(me.compiledTemplates);
        clearObj(me.compiledTemplateMaps);
        me.compiledTemplateNames.splice(0, me.compiledTemplateNames.length);

        for (let t of me.templates){
            me.compiledTemplates[t.ID] = t;
            me.compiledTemplateMaps[t.ID] = t.name;
            me.compiledTemplateMaps[t.name] = t.ID;
            me.compiledTemplateNames.push(t.name);
        };
    };
    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 subStr = str.slice(idx);
            let patt = new RegExp(/@\d+/);
            let atID = subStr.match(patt);
            let ID = atID ? atID[0].slice(1) : null;
            return ID;
        };
        let private_parse_ref = function(item, itemIdx){
            let idx = item.expression.indexOf('@', 0);
            while (idx >= 0) {
                let ID = private_extract_ID(item.expression, idx);
                let len = ID ? ID.toString().length : 0;
                if (len) {
                    let subItem = template.compiledCalcItems[ID];
                    if (subItem) {
                        if (subItem.ID !== item.ID) {
                            private_parse_ref(subItem, template.compiledCalcItems[ID + "_idx"]);
                        } else {
                            template.errs.push("循环引用ID: " + ID);
                        }
                    } else {
                        template.errs.push("找不到ID: " + ID);
                        console.log('找不到ID： ' + ID);
                    }
                }
                idx = item.expression.indexOf('@', idx + len + 1);
            }
            if (template.compiledSeq.indexOf(itemIdx) < 0) {
                template.compiledSeq.push(itemIdx);
            }
        };
        let private_compile_items = function() {
            for (let idx of template.compiledSeq) {
                let item = template.calcItems[idx];

                let lc = analyzer.calcItemLabourCoe(item);
                // 用于界面显示。disExpr是公式模板，不允许修改：人工系数占位符被修改后变成数值，第二次无法正确替换。
                item.dispExprUser = analyzer.getDispExprUser(item.dispExpr, lc);
                if (item.expression == 'HJ')
                    item.compiledExpr = '$CE.HJ()'
                else
                    item.compiledExpr = analyzer.getCompiledExpr(item.expression, lc);

                if (item.feeRateID) {
                    let orgFeeRate = item.feeRate;
                    let cfr = me.compiledFeeRates[item.feeRateID];
                    item.feeRate = cfr ? cfr.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];
                item.dispExpr = analyzer.getDispExpr(item.expression, template);
                template.compiledCalcItems[item.ID] = item;
                template.compiledCalcItems[item.ID + "_idx"] = i;
            }

            for (let i = 0; i < template.calcItems.length; i++) {
                let item = template.calcItems[i];
                if (template.compiledSeq.indexOf(i) < 0) {
                    private_parse_ref(item, i);
                }
            }

            if (template.errs.length == 0) {
                private_compile_items();
                template.hasCompiled = true;
            } else {
                console.log('errors: ' + template.errs.toString());
            }
        };
    };

    // 只计算treeNode自身。changedArr: 外部传来的一个数组，专门存储发生变动的节点。
    innerCalc(treeNode, changedArr, tender){
        if (treeNode.sourceType === ModuleNames.ration_glj) return;             // 仅用作树节点显示的工料机不能参与计算。

        // 不能直接删除该属性，否则无法冲掉库中已存储的值。下同。
        function deleteProperties (treeNode, propNamesArr){
            for (let pn of propNamesArr){
                if (treeNode.data[pn]){
                    treeNode.data[pn] = null;
                    treeNode.changed = true;
                }
            };
        };
        /*删掉多余的费用。例如：从其它计算方式(有很多费)切换到公式计算方式(只需要common费)，多出来的费要删除。
        reserveArr 值取自：情况①遍历treeNode的计算规则，取有绑定的字段名。这些字段名以外的fee是因旧计算多出来的，需要删除。没有绑定的
        费用类别，不要同步到清单，也需要删除。*/
        function deleteUselessFees(treeNode, reserveArr){
            treeNode.data.fees = _.filter(treeNode.data.fees, function (item) {
                if(!reserveArr.includes(item.fieldName)){
                    delete treeNode.data.feesIndex[item.fieldName];
                    treeNode.changed = true;
                    return false;
                }
                return true;
            });
        };

        let me = this;
        //设置定额工料机映射表
        me.setRationMap();
        me.setUpdateDataToNode(treeNode);                                       // 先更新要计算的节点信息
        if(!treeNode.temData) treeNode.temData = _.cloneDeep(treeNode.data);    // 有可能会有多次计算，只有第一次才保存原始数据，用来对于更新。

        let nQ = calcTools.uiNodeQty(treeNode);
        let nTQ = calcTools.uiNodeTenderQty(treeNode);
        treeNode.calcType = calcTools.getCalcType(treeNode);

        // 父清单汇总子清单的费用类别、叶子清单汇总定额的费用类别
        if (treeNode.calcType == treeNodeCalcType.ctGatherBillsFees || treeNode.calcType == treeNodeCalcType.ctGatherRationsFees){
            treeNode.data.programID = null;
            calcTools.initFees(treeNode);

            let nodes = [];
            if (treeNode.calcType == treeNodeCalcType.ctGatherRationsFees){
                calcTools.getGLJList(treeNode, true);
                nodes = me.project.Ration.getRationNodes(treeNode);
            }
            else nodes = treeNode.children;

            let rst = [];
            for (let ft of cpFeeTypes) {
                let ftObj = {};
                ftObj.fieldName = ft.type;
                ftObj.name = ft.name;
                let buf = 0, btf = 0, btuf = 0, bttf = 0;
                let bq = nQ ? nQ : 1;
                let btq = nTQ ? nTQ : 1;

                if (treeNode.calcType == treeNodeCalcType.ctGatherBillsFees){
                    for (let node of nodes) {
                        if (node.data.feesIndex && node.data.feesIndex[ft.type]) {
                            btf = (btf + parseFloatPlus(node.data.feesIndex[ft.type].totalFee)).toDecimal(decimalObj.process);
                            bttf = (bttf + parseFloatPlus(node.data.feesIndex[ft.type].tenderTotalFee)).toDecimal(decimalObj.process);

                            if (calcTools.isBudgetProject()){
                                buf = (btf / bq).toDecimal(decimalObj.process);
                                btuf = (bttf / bq).toDecimal(decimalObj.process);
                            }
                        };
                    };
                }
                else if (treeNode.calcType == treeNodeCalcType.ctGatherRationsFees){
                    let sum_rtf = 0, sum_rttf = 0;
                    for (let node of nodes) {
                        let ruf = 0, rtuf = 0, rtf = 0, rttf = 0;
                        if (node.data.feesIndex && node.data.feesIndex[ft.type]) {
                            ruf = parseFloatPlus(node.data.feesIndex[ft.type].unitFee).toDecimal(decimalObj.bills.unitPrice);
                            rtuf = parseFloatPlus(node.data.feesIndex[ft.type].tenderUnitFee).toDecimal(decimalObj.bills.unitPrice);
                            rtf = parseFloatPlus(node.data.feesIndex[ft.type].totalFee).toDecimal(decimalObj.bills.totalPrice);
                            rttf = parseFloatPlus(node.data.feesIndex[ft.type].tenderTotalFee).toDecimal(decimalObj.bills.totalPrice);
                        };

                        sum_rtf = (sum_rtf + rtf).toDecimal(decimalObj.process);
                        sum_rttf = (sum_rttf + rttf).toDecimal(decimalObj.process);
                    };

                    buf = (sum_rtf / bq).toDecimal(decimalObj.process);
                    btuf = (sum_rttf / btq).toDecimal(decimalObj.process);
                    btf = sum_rtf;
                    bttf = sum_rttf;

                    if (calcTools.isBillProject() && (ft.type == 'common' || ft.type == 'rationCommon')) {  // 招投标项目, 还要反算
                        buf = buf.toDecimal(decimalObj.bills.unitPrice);
                        btuf = btuf.toDecimal(decimalObj.bills.unitPrice);
                        btf = (buf * nQ).toDecimal(decimalObj.bills.totalPrice);
                        bttf = (btuf * nQ).toDecimal(decimalObj.bills.totalPrice);
                    };
                };

                ftObj.unitFee = buf.toDecimal(decimalObj.bills.unitPrice);
                ftObj.tenderUnitFee = btuf.toDecimal(decimalObj.bills.unitPrice);
                ftObj.totalFee = btf.toDecimal(decimalObj.bills.totalPrice);
                ftObj.tenderTotalFee = bttf.toDecimal(decimalObj.bills.totalPrice);


                calcTools.checkFeeField(treeNode, ftObj);

                rst.push(ftObj);
            };
            treeNode.data.calcTemplate = {"calcItems": rst};
        }
        // 叶子清单：公式计算
        else if (treeNode.calcType == treeNodeCalcType.ctCalcBaseValue){
            delete treeNode.data.gljList;
            deleteProperties(treeNode, ['programID']);
            deleteUselessFees(treeNode, ['common', 'rationCommon']);
            let tf = treeNode.data.calcBaseValue ? treeNode.data.calcBaseValue : 0;
            let ttf = treeNode.data.tenderCalcBaseValue ? treeNode.data.tenderCalcBaseValue : 0;
            let q = nQ ? nQ : 1;
            let tq = nTQ ? nTQ : 1;
            let uf = (tf / q).toDecimal(decimalObj.bills.unitPrice);
            let tuf = (ttf / tq).toDecimal(decimalObj.bills.unitPrice);
            if (calcTools.isBillProject()){
                tf = (uf * q).toDecimal(decimalObj.bills.totalPrice);
                ttf = (tuf * q).toDecimal(decimalObj.bills.totalPrice);
            };
            calcTools.checkFeeField(treeNode, {'fieldName': 'common', 'unitFee': uf, 'totalFee': tf, 'tenderUnitFee': tuf, 'tenderTotalFee': ttf});
            //第一部分建安费下，使用基数计算的清单，定额建安费应=金额
            if (cbTools.getBelongFlagList(treeNode).includes(fixedFlag.CONSTRUCTION_INSTALL_FEE))
                calcTools.checkFeeField(treeNode, {'fieldName': 'rationCommon', 'unitFee': uf, 'totalFee': tf, 'tenderUnitFee': tuf, 'tenderTotalFee': ttf});

            // 总造价清单还要做单项工程、建设项目的四大项金额汇总
            // if (calcTools.isTotalCostBill(treeNode)){
            //     // 公式叶子清单没有暂估费，但总造价清单除外。
            //     calcTools.estimateFee(treeNode);
            //     calcTools.initSummaryFee(treeNode);
            //     treeNode.data.summaryFees.totalFee = tf;
            //     treeNode.data.summaryFees.estimateFee = calcTools.getFee(treeNode, 'estimate.totalFee');
            // }
            treeNode.data.calcTemplate = {"calcItems": []};
        }
        // 叶子清单：无定额、无公式计算（什么都没有时），手工修改单价或金额。
        else if (treeNode.calcType == treeNodeCalcType.ctNull){
            delete treeNode.data.gljList;
            deleteProperties(treeNode, ['calcBase', 'calcBaseValue', 'tenderCalcBaseValue', 'programID']);
            deleteUselessFees(treeNode, ['common', 'rationCommon']);

            if (treeNode.data.feesIndex && treeNode.data.feesIndex.common){
                let uf, tf;
                let ftObj = {fieldName: 'common'};

                if (treeNode.data.calcFlag == treeNodeCalcFlag.customUnitPrice){    // 修改了清单单价：以单价为准，算金额
                    uf = parseFloatPlus(treeNode.data.feesIndex.common.unitFee);
                    tf = (uf * nQ).toDecimal(decimalObj.bills.totalPrice);
                }
                else if (treeNode.data.calcFlag == treeNodeCalcFlag.customTotalPrice){  // 修改了清单金额：以金额为准，算单价。修改了清单数量也一样
                    tf = parseFloatPlus(treeNode.data.feesIndex.common.totalFee);
                    uf = nQ ? (tf / nQ) : tf;        // 如果工程量为0或空，综合单价直接等于综合合价
                    uf = uf.toDecimal(decimalObj.bills.unitPrice);
                    if (calcTools.isBillProject()) {  // 招投标项目, 还要反算
                        tf = (uf * nQ).toDecimal(decimalObj.bills.totalPrice);
                    };
                };

                ftObj.unitFee = uf;
                ftObj.totalFee = tf;
                calcTools.checkFeeField(treeNode, ftObj);

                // 数量、金额时，同步定额建安费
                if (calcTools.isInheritFrom(treeNode, 1)){
                    let ftObj2 = {
                        fieldName: 'rationCommon',
                        unitFee: uf,
                        totalFee: tf
                    };
                    calcTools.checkFeeField(treeNode, ftObj2);
                };
            }

            treeNode.data.calcTemplate = {"calcItems": []};
        }
        // 定额：计算程序
        else{
            let fnArr = [];
            calcTools.getGLJList(treeNode, true);

            if (treeNode.calcType == treeNodeCalcType.ctRationCalcProgram) {
                // 量价、工料机类型的定额要求市场合价
                if (treeNode.data.type == rationType.volumePrice || treeNode.data.type == rationType.gljRation){
                    let muf = treeNode.data.marketUnitFee ? treeNode.data.marketUnitFee : 0;
                    let mtf = (muf * nQ).toDecimal(decimalObj.ration.totalPrice);
                    if (treeNode.data.marketTotalFee != mtf){
                        treeNode.data.marketTotalFee = mtf;
                        treeNode.changed = true;
                    } ;
                };
            };
            // 2018-08-27   zhang  插入空定额的时候，取费专业也为空
           // if (treeNode.data.programID == undefined) treeNode.data.programID = projectObj.project.projectInfo.property.engineering;
            let template = me.compiledTemplates[treeNode.data.programID];
            treeNode.data.calcTemplate = template;

            if (treeNode && template && template.hasCompiled) {//2018-08-27 空行的时候，取费专业为空，template也为空，加入template非空判断
                let $CE = executeObj;
                $CE.treeNode = treeNode;
                $CE.template = template;

                calcTools.initFees(treeNode);

                for (let idx of template.compiledSeq) {
                    let calcItem = template.calcItems[idx];
                    $CE.tempCalcItem = calcItem;
                    let feeRate = 100;  // 100%
                    if (calcItem.feeRate != undefined)
                        feeRate = parseFloat(calcItem.feeRate).toDecimal(decimalObj.feeRate);
                    // console.log(`[${calcItem.ID}]: ${calcItem.compiledExpr}`);   // for test.
                    let tf = (eval(calcItem.compiledExpr) * feeRate * 0.01).toDecimal(decimalObj.process);
                    calcItem.totalFee = tf.toDecimal(decimalObj.decimal('totalPrice', treeNode));
                    let nodeQ = calcTools.uiNodeQty(treeNode) ? calcTools.uiNodeQty(treeNode) : 1;
                    calcItem.unitFee = (tf / nodeQ).toDecimal(decimalObj.decimal('unitPrice', treeNode));

                    if (tender == tenderTypes.ttCalc) {
                        let tExpr = analyzer.getCompiledTenderExpr(calcItem.compiledExpr);
                        calcItem.tenderUnitFee = (eval(tExpr) * feeRate * 0.01).toDecimal(decimalObj.decimal('unitPrice', treeNode));
                        calcItem.tenderTotalFee = (calcItem.tenderUnitFee * treeNode.data.tenderQuantity).toDecimal(decimalObj.decimal('totalPrice', treeNode));
                    };

                    if (calcItem.fieldName) {
                        fnArr.push(calcItem.fieldName);
                        calcTools.checkFeeField(treeNode, calcItem);
                    };
                };

                if (tender == tenderTypes.ttReverseRation || tender == tenderTypes.ttReverseGLJ)
                    this.calcTenderReverse(treeNode, tender);
                deleteUselessFees(treeNode, fnArr);
            };
        };

        // if (!calcTools.isTotalCostBill(treeNode))  // 已在上面的分支中计算过
        //     calcTools.estimateFee(treeNode);
        if (treeNode.changed && !changedArr.includes(treeNode)) changedArr.push(treeNode);
    };

    // 存储、刷新零散的多个结点。
    saveNodes(treeNodes, callback){
        if (treeNodes.length < 1) {
            $.bootstrapLoading.end();
            this.rationMap = null;
            this.pgljMap = null;
            return;
        }

        let me = this;
        /*        me.project.beginUpdate('');
                for (let node of treeNodes){
                    if (node.changed){
                        let data = calcTools.cutNodeForSave(node);
                        let newData = {'updateType': 'ut_update', 'updateData': data};
                        me.project.push(node.sourceType, [newData]);
                    }
                };
                me.project.endUpdate();*/

        let dataArr = [];
        let updateMap = {};
        for (let node of treeNodes){
            if (node.changed){
                let data = calcTools.cutNodeForSave(node);
                let newData = {'type': node.sourceType, 'data': data};
                dataArr.push(newData);
                updateMap[node.data.ID] = data;
            }
        };
        if (dataArr.length < 1) {
            $.bootstrapLoading.end();
            return;
        };
        $.bootstrapLoading.start();
        me.project.updateNodes(dataArr, function (data) {
            for (let node of treeNodes){
                delete node.changed;
                delete node.temData;
                node.updateData = {};
            };
            me.rationMap = null;
            me.pgljMap = null;
            if(callback){
                callback(data);
            };
            projectObj.mainController.refreshTreeNode(treeNodes,false,false);
            // 批量树结点计算后，计算程序早已物是人非，所以这里要重新计算一下。警告：第二个参数千万不能改成3，否则死循环！
            if (activeSubSheetIsCalcProgram())
                calcProgramObj.refreshCalcProgram(projectObj.project.mainTree.selected, 2);
            projectInfoObj.refreshTotalPriceSpan();
            $.bootstrapLoading.end();
        });
    };

    // 计算本节点、所有父节点(默认,可选)、公式引用节点(默认,可选)。
    calculate(treeNode, calcParents = true, calcFormulas = true, tender){
        let me = this;
        let changedNodes = [];
        me.innerCalc(treeNode, changedNodes, tender);
        if (treeNode.changed) {
            // 计算父结点
            if (calcParents){
                let curNode = treeNode.parent;
                while (curNode){
                    me.innerCalc(curNode, changedNodes, tender);
                    curNode = curNode.parent;
                };
            };
            // 父结点算完，再计算所有的公式结点(必须先算完父结点，再算公式结点)
            if (calcFormulas) {
                me.calcFormulaNodes(changedNodes, tender);
            };
        };

        return changedNodes;
    };
    // 计算并保存一个树节点。(修改一个树节点，实际上要计算和保存的是一批树结点：层层父结点、被其它结点(的公式)引用的公式结点)
    calcAndSave(treeNode, callback, tender){
        this.calcNodesAndSave([treeNode],callback,tender)
      /*  let changedNodes = this.calculate(treeNode, true, true, tender); 统一调用相同的方法
        this.saveNodes(changedNodes, callback);*/
    };

    /* 计算所有树结点(分3种情况)，并返回发生变动的零散的多个树结点。参数取值如下：
        calcAllType.catAll       计算所有树结点 (默认值)
        calcAllType.catBills     计算所有清单 (改变项目属性中清单取费算法时会用到)
        calcAllType.catRations   计算所有定额、工料机形式的定额、量价，因为它们都走自己的计算程序 (改变人工系数、费率值、工料机单价时会用到)
                                 (calcAllType.catRations时程序中做了特殊处理，实际上是计算所有树结点！)
    */
    calcAllNodes(calcType = calcAllType.catAll, tender){
        let me = this;
        let changedNodes = [];
        function calcNodes(nodes) {
            for (let node of nodes) {
                me.setUpdateDataToNode(node);
                if (node.children.length > 0) {
                    calcNodes(node.children);
                };

                if (calcType == calcAllType.catAll || calcType == node.sourceType) {
                    node.calcType = calcTools.getCalcType(node);
                    if (node.calcType != treeNodeCalcType.ctCalcBaseValue)
                        me.innerCalc(node, changedNodes, tender);
                };
            }
        };

        // calcAllType.catRations 参数情况不会计算父结点（因为父结点是清单），所以这里进行特殊处理。
        if (calcType == calcAllType.catRations)
            calcType == calcAllType.catAll;

        calcNodes(me.project.mainTree.roots);
        me.calcFormulaNodes(changedNodes, tender);
        return changedNodes;
    };
    // tender: null：不调价(普通计算)。 1: 正向调价   2：反向调价－调子目    3: 反向调价－调工料机
    calcAllNodesAndSave(calcType = calcAllType.catAll, callback, tender){
        let changedNodes = this.calcAllNodes(calcType, tender);
        this.saveNodes(changedNodes, callback);
    };
    setUpdateDataToNode(node){//将updateData里面的值设到node.data上，计算结点时才能拿到最新的值
        if(!_.isEmpty(node.updateData)){
            node.changed = true;
            for(let key in node.updateData){
                node.data[key] = node.updateData[key];
            }
        }
    };

    // 计算零散的、混杂的树节点：清单、定额混合等(如：用到某一计算程序的定额和清单)。
    // 计算多条零散的定额，并计算他们所属的清单、父清单、引用清单。如：批量替换工料机后受影响的定额。
    // 计算多条零散的清单，并计算他们的父清单、引用清单。如：花选删除树结点(如花选清单、定额等，不区分树结点类型)。
    calcNodesAndSave(nodes, callback, tender){
        let me = this, rationNodes = [], billNodes = [], leafBills = [], allChangedNodes = [];
        for (let node of nodes) {
          me.setUpdateDataToNode(node);//多节点更新的情况，要先更新值，因为计算过程中有可能要引用新的值
            if (node.sourceType == ModuleNames.ration)
                rationNodes.push(node)
            else
                billNodes.push(node);
        }

        // 多条定额同属一条叶子清单时，避免叶子清单重复计算
        for (let ration of rationNodes) {
            me.innerCalc(ration, allChangedNodes, tender);
            let leafBill = ration.parent;
            if (leafBill && leafBills.indexOf(leafBill) < 0)
                leafBills.push(leafBill);
        }

        billNodes.merge(leafBills);

        for (let bill of billNodes){
            let changeBills = me.calculate(bill, true, false, tender);
            allChangedNodes.merge(changeBills);
        }

        me.calcFormulaNodes(allChangedNodes, tender);
        me.saveNodes(allChangedNodes, callback);
    };

    // 计算全部公式项。 (changedArr：将通过本方法后发生改变的节点存入changedArr中)
    calcFormulaNodes(changedArr, tender){
        let me = this;
        let formulaNodes = cbTools.getFormulaNodes(true);
        if (formulaNodes.length == 0) return;
        for (let formulaNode of formulaNodes){
            formulaNode.data.userCalcBase = formulaNode.data.calcBase;    // 这句不该出现，projectObj.project.calcBase中要改进。
            projectObj.project.calcBase.calculate(formulaNode, true);
            if (projectObj.project.calcBase.success){
                // 计算公式结点
                me.innerCalc(formulaNode, changedArr, tender);

                // 计算父结点
                if (formulaNode.changed){
                    let curNode = formulaNode.parent;
                    while (curNode){
                        me.innerCalc(curNode, changedArr, tender);
                        curNode = curNode.parent;
                    }
                }
            }
        }
    };

    // 计算叶子清单下的所有子结点、自身、所有父结点、公式引用结点(即跟该叶子清单相关的所有结点)。最后打包存储。
    calcLeafAndSave(treeNode, tender){
        let me = this;
        if(!calcTools.isLeafBill(treeNode)) return;
        if (treeNode.children && treeNode.children.length > 0) {
            let changedNodes = [];
            for (let child of treeNode.children){
                me.innerCalc(child, changedNodes);
            };

            let curChangeds = me.calculate(treeNode, true, true, tender);
            changedNodes.merge(curChangeds);
            me.saveNodes(changedNodes);
        };
    };

    // 排除指定项的综合合价计算(用于带循环计算的情况。这里的汇总只到清单级别即可:清单单价取费时，汇总到清单和汇总到定额两个值不一样)
    getTotalFee(baseNodes, excludeNodes, feeField){
        let rst = 0;
        function calcNodes(nodes) {
            for (let node of nodes) {
                if(!node){
                    continue;
                }
                if (!excludeNodes.includes(node)){
                    if (node.source && node.source.children && node.source.children.length > 0) {
                        calcNodes(node.children);
                    }
                    else{
                        if (node.sourceType == ModuleNames.bills) {
                            //这里不四舍五入，保证中间过程不四舍五入
                            rst += calcTools.getFee(node, feeField);
                        };
                    }
                }
            }
        };
        calcNodes(baseNodes);
        return rst;
    };

    // 税前工程造价 ,adj调价
    getBeforeTaxTotalFee(excludeNodes, tender){
        let baseNodes = [], me = this;
        baseNodes.push(calcTools.getNodeByFlag(fixedFlag.SUB_ENGINERRING));
        baseNodes.push(calcTools.getNodeByFlag(fixedFlag.MEASURE));
        // baseNodes.push(calcTools.getNodeByFlag(fixedFlag.OTHER));
        // baseNodes.push(calcTools.getNodeByFlag(fixedFlag.CHARGE));
        return me.getTotalFee(baseNodes, excludeNodes, tender);
    };

    // 反向调价
    calcTenderReverse(treeNode, tender){
        if (tender == tenderTypes.ttReverseRation) {
            if (treeNode.data.feesIndex.common.tenderUnitFee != treeNode.data.feesIndex.common.unitFee) {
                treeNode.data.feesIndex.common.tenderUnitFee = treeNode.data.feesIndex.common.unitFee;
                treeNode.changed = true;
            }
        };

        if (!treeNode.data.targetTotalFee){
            if (treeNode.data.targetUnitFee){
                treeNode.data.targetTotalFee = (treeNode.data.targetUnitFee * treeNode.data.quantity).toDecimal(decimalObj.decimal('totalPrice', treeNode));
                treeNode.changed = true;
            }
            else{
                return;
            }
        };

        if (!treeNode.data.targetUnitFee)
            treeNode.data.targetUnitFee = (treeNode.data.targetTotalFee / treeNode.data.quantity).toDecimal(decimalObj.decimal('unitPrice', treeNode));
        let coe = 1;
        if (treeNode.data.feesIndex.common.totalFee != 0)
            // coe = (treeNode.data.targetTotalFee / treeNode.data.feesIndex.common.totalFee).toDecimal(decimalObj.process);
            coe = (treeNode.data.targetUnitFee / treeNode.data.feesIndex.common.unitFee).toDecimal(decimalObj.process);

        if (tender == tenderTypes.ttReverseRation){
            treeNode.data.tenderQuantity = (treeNode.data.quantity * coe).toDecimal(decimalObj.decimal("quantity", treeNode));
            if (treeNode.data.rationQuantityCoe != coe){
                treeNode.data.rationQuantityCoe = coe;
                treeNode.changed = true;
            };

            let ttf = (treeNode.data.tenderQuantity * treeNode.data.feesIndex.common.tenderUnitFee).toDecimal(decimalObj.decimal('totalPrice', treeNode));
            if (treeNode.data.feesIndex.common.tenderTotalFee != ttf){
                treeNode.data.feesIndex.common.tenderTotalFee = ttf;
                treeNode.changed = true;
            };
        }else if (tender == tenderTypes.ttReverseGLJ){
            treeNode.data.tenderQuantity = treeNode.data.quantity;
            let qcObj = treeNode.data.quantityCoe;
            if (!qcObj){
                treeNode.data.quantityCoe = {labour: coe, material: coe, machine: coe, main: coe, equipment: coe};
                treeNode.changed = true;
            }else{
                for (let pn in qcObj){
                    if (qcObj[pn] != coe){
                        qcObj[pn] = coe;
                        treeNode.changed = true;
                    }
                };
            };
            projectObj.project.calcProgram.calculate(treeNode, false, false, tenderTypes.ttCalc);  // 再正向算
        };


    };

    setRationMap(){
        if(this.rationMap == null){
            this.rationMap = {};
            for(let glj of projectObj.project.ration_glj.datas){
                if(this.rationMap[glj.rationID]){
                    this.rationMap[glj.rationID].push(glj)
                }else {
                    this.rationMap[glj.rationID] = [glj];
                }
            }
        }
        if(this.pgljMap == null ){
            this.pgljMap = _.indexBy(projectObj.project.projectGLJ.datas.gljList, 'id');
        }
    };
    getGljArrByRation(ration){
        if (ration.type == rationType.gljRation){
            let glj = JSON.parse(JSON.stringify(ration));
            glj.type = glj.subType;
            glj.quantity = 1;
            glj.totalQuantity = parseFloatPlus(ration.quantity);
            return [glj];
        } else{
            if(!this.rationMap) return [];
            let result = this.rationMap[ration.ID];
            if(!result) return [];
            result = gljOprObj.combineWithProjectGlj(result,false,ration,this.pgljMap);
            return result;
        }
    }
};

// export default analyzer;