Forráskód Böngészése

Merge branch 'master' of http://192.168.1.41:3000/SmartCost/YangHuCost

zhangweicheng 4 éve
szülő
commit
ba84d6566e

+ 228 - 87
web/building_saas/main/js/models/calc_program.js

@@ -2122,7 +2122,7 @@
        };
 
        if (tenderType == tenderTypes.ttReverseRation || tenderType == tenderTypes.ttReverseGLJ)
-         this.calcTenderReverse(treeNode, tenderType);
+         this.reverseTenderCalc(treeNode, tenderType);
 
        me.deleteUselessFees(treeNode, fnArr);
      };
@@ -2253,7 +2253,7 @@
      calcNodes(me.project.mainTree.roots);
      me.calcFormulaNodes(changedNodes, tender);
 
-     if (tender) { // 普通计算不执行,只有3种调价计算才清。
+     if (tender) { // 清理调价缓存
        for (let node of projectObj.project.mainTree.items) {
          this.clearTenderCache(node);
        };
@@ -2387,6 +2387,22 @@
      return me.getTotalFee(baseNodes, excludeNodes, tender);
    };
 
+   // 清理调价缓存数据
+   clearTenderCache(treeNode) {
+     // 这些属性值为什么不定义在一个对象里?因为每次要判断对象是否存在,十分麻烦。不如直接写简单。分散书写,统一处理也很好用。
+     if (treeNode.data.tender_activeTotal) delete treeNode.data.tender_activeTotal;
+     if (treeNode.data.tender_activeTarget) delete treeNode.data.tender_activeTarget;
+     if (treeNode.data.tender_fullLoad) delete treeNode.data.tender_fullLoad;
+     if (treeNode.data.tender_distribute) {
+       delete treeNode.data.targetTotalFee;
+       delete treeNode.data.targetUnitFee;
+       delete treeNode.data.tender_distribute;
+     };
+     if (treeNode.data.tender_diffValuePerCoe) delete treeNode.data.tender_diffValuePerCoe;
+     if (treeNode.data.tender_rowNo) delete treeNode.data.tender_rowNo;
+     if (treeNode.data.tender_previousCoe) delete treeNode.data.tender_previousCoe;
+   };
+
    initGljPriceTenderCoe() {
      if (projectObj.project.property.tenderSetting && projectObj.project.property.tenderSetting.gljPriceTenderCoe &&
        (projectObj.project.property.tenderSetting.gljPriceTenderCoe != 1)) {
@@ -2395,8 +2411,8 @@
      }
    };
 
-   // 反向调价需初始化调价树、缓存数据等
-   initReverseTenderDatas() {
+   // 反向调价-初始化调价树、清理缓存数据等
+   reverseTenderInitDatas() {
      for (let node of tender_obj.tenderTree.items) {
        if (node.data.rationQuantityCoe) node.data.rationQuantityCoe = null;
        let qcObj = node.data.quantityCoe;
@@ -2411,20 +2427,23 @@
      this.initGljPriceTenderCoe();
    };
 
-   // 反向调价
-   calcTenderReverse(treeNode, tender) {
-     if (tender == tenderTypes.ttReverseRation) {
+   // 反向调价简单计算:只计算当前单一结点,由目标金额计算定额量系数或工料机量系数coe,再根据此coe计算定额的调后量、调后合价。
+   // ⑴只计算参数指定的一个单一的结点,连父结点、引用结点都不计算。此结点只会是定额、量价、工料机款定额,不会是清单。
+   // ⑵执行此方法的前提:已在其它地方把总的目标金额分摊好了,此结点已获取目标金额。
+   reverseTenderCalc(treeNode, tender) {
+     if (tender == tenderTypes.ttReverseRation) {    // 先初始化,使调后单价=调前单价
        if (treeNode.data.feesIndex.common && 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.targetTotalFee) {   // 没有目标金额、但有输入目标单价的:根据目标单价算出目标金额。
        if (treeNode.data.targetUnitFee) {
          treeNode.data.targetTotalFee = (treeNode.data.targetUnitFee * treeNode.data.quantity).toDecimal(decimalObj.decimal('totalPrice', treeNode));
          treeNode.changed = true;
-       } else { // 既没有目标金额也没有目标单价,此时要初始化使调价合价=原始综合合价,调价单价=原始综合单价。
+       }
+       else { // 既没有目标金额也没有目标单价,此时要初始化使调价合价=原始综合合价,调价单价=原始综合单价。
          if (treeNode.data.feesIndex.common && treeNode.data.feesIndex.common.tenderUnitFee != treeNode.data.feesIndex.common.unitFee) {
            treeNode.data.feesIndex.common.tenderUnitFee = treeNode.data.feesIndex.common.unitFee;
            treeNode.changed = true;
@@ -2437,16 +2456,18 @@
        }
      };
 
+     // 经过前面的一通折腾还是没有目标单价的话,那么就要通过目标金额算出来。
      if (!treeNode.data.targetUnitFee || (parseFloat(treeNode.data.targetUnitFee) == 0)) {
        if (calcTools.hasQuantity(treeNode))
          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.targetUnitFee / treeNode.data.feesIndex.common.unitFee).toDecimal(decimalObj.process);
 
-     // 调价情况之————量价反调工料机(量价无工料机可调,还是按定额来调,即直接调树结点的消耗量)
+     // 量价、工料机款式的定额:它们无下挂工料机可调,都按定额来调,即直接调树结点的消耗量。
      let isVP_RevGLJ = (tender == tenderTypes.ttReverseGLJ) && calcTools.isVP_or_GLJR(treeNode);
 
      if ((tender == tenderTypes.ttReverseRation) || isVP_RevGLJ) {
@@ -2457,13 +2478,7 @@
        };
 
        if (isVP_RevGLJ) {
-         treeNode.data.quantityCoe = {
-           labour: 0,
-           material: 0,
-           machine: 0,
-           main: 0,
-           equipment: 0
-         };
+         treeNode.data.quantityCoe = {labour: 0, material: 0, machine: 0, main: 0, equipment: 0};
          treeNode.changed = true;
        };
 
@@ -2472,64 +2487,29 @@
          treeNode.data.feesIndex.common.tenderTotalFee = ttf;
          treeNode.changed = true;
        };
-     } else if (tender == tenderTypes.ttReverseGLJ) {
+     }
+     else if (tender == tenderTypes.ttReverseGLJ) {
        let qcObj = treeNode.data.quantityCoe;
        if (!qcObj || calcTools.isEmptyObject(qcObj)) {
-         treeNode.data.quantityCoe = {
-           labour: coe,
-           material: coe,
-           machine: coe,
-           main: coe,
-           equipment: coe
-         };
+         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;
-                             }
-                         };*/
-         if (qcObj.labour != coe) {
-           qcObj.labour = coe;
-           treeNode.changed = true;
-         };
-         if (qcObj.material != coe) {
-           qcObj.material = coe;
-           treeNode.changed = true;
-         };
-         if (qcObj.machine != coe) {
-           qcObj.machine = coe;
-           treeNode.changed = true;
-         };
-         if (qcObj.main != coe) {
-           qcObj.main = coe;
-           treeNode.changed = true;
-         };
-         if (qcObj.equipment != coe) {
-           qcObj.equipment = coe;
-           treeNode.changed = true;
-         };
+         /* for (let pn in qcObj){ if (qcObj[pn] != coe){ qcObj[pn] = coe; treeNode.changed = true; } };*/
+         if (qcObj.labour != coe) {qcObj.labour = coe;treeNode.changed = true;};
+         if (qcObj.material != coe) {qcObj.material = coe;treeNode.changed = true;};
+         if (qcObj.machine != coe) {qcObj.machine = coe;treeNode.changed = true;};
+         if (qcObj.main != coe) {qcObj.main = coe;treeNode.changed = true;};
+         if (qcObj.equipment != coe) {qcObj.equipment = coe;treeNode.changed = true;};
        };
        projectObj.project.calcProgram.calculate(treeNode, false, false, tenderTypes.ttCalc); // 再正向算
      };
    };
 
-   // 清理调价缓存数据
-   clearTenderCache(treeNode) {
-     // 这些属性值为什么不定义在一个对象里?因为每次要判断对象是否存在,十分麻烦。不如直接写简单。分散书写,统一处理也很好用。
-     if (treeNode.data.tender_activeTotal) delete treeNode.data.tender_activeTotal;
-     if (treeNode.data.tender_activeTarget) delete treeNode.data.tender_activeTarget;
-     if (treeNode.data.tender_fullLoad) delete treeNode.data.tender_fullLoad;
-     if (treeNode.data.tender_distribute) {
-       delete treeNode.data.targetTotalFee;
-       delete treeNode.data.targetUnitFee;
-       delete treeNode.data.tender_distribute;
-     };
-   };
-
-   // 调价:分摊前的准备工作。主要标记满载的结点。
+   // 反向调价-分摊前的准备工作。包括:
+   // ⑴结点是否满载
+   // ⑵若是满载,汇总孩子的金额、目标金额给它
+   // ⑶标记此结点的目标金额来自孩子,未来它不往下分摊(汇上来再摊下去无用功)
    prepareForDistribute(treeNode) {
      if (!treeNode) return;
 
@@ -2537,10 +2517,7 @@
        this.prepareForDistribute(treeNode.firstChild());
 
      if (treeNode.children.length == 0) {
-       // if (!treeNode.data.feesIndex['common'])
-       //     treeNode.data.tender_fullLoad = true
-       // else
-       //     treeNode.data.tender_fullLoad = false;
+       //
      } else {
        let full = true;
        for (let i = 0; i < treeNode.children.length; i++) {
@@ -2551,8 +2528,7 @@
          };
        }
        if (full) {
-         let total = 0,
-           target = 0;
+         let total = 0, target = 0;
          for (let i = 0; i < treeNode.children.length; i++) {
            let child = treeNode.children[i];
            total = total + parseFloat(child.data.feesIndex['common'].totalFee);
@@ -2567,20 +2543,10 @@
        treeNode.data.tender_fullLoad = full;
      };
 
-     /*if (calcTools.isBill(treeNode) && (treeNode.data.tender_fullLoad != undefined)){
-         console.log(treeNode.data.name + ',tender_fullLoad: ' + treeNode.data.tender_fullLoad + ', target: ' + calcTools.hasTargetTotalFee(treeNode));
-     };*/
-
-     /*        if (calcTools.isRationItem(treeNode)){
-                 console.log(treeNode.data);
-             }*/
-
-     if (treeNode.nextSibling)
-       this.prepareForDistribute(treeNode.nextSibling);
+     if (treeNode.nextSibling) this.prepareForDistribute(treeNode.nextSibling);
    };
 
-
-   // 调价之分摊目标合价:从父到子往下分摊。
+   // 反向调价-分摊目标合价:从父到子往下分摊。
    distributeTargetTotalFee(treeNode) {
      if (!treeNode) return;
 
@@ -2588,7 +2554,7 @@
        // 默认能够执行到这里时每个节点已经被初始化,缓存已删除
        treeNode.data.tender_activeTotal = treeNode.data.feesIndex['common'].totalFee;
 
-       // 开始分摊:只分摊自有目标金额、从父结点分摊到的金额(不分摊子结点上来的金额)
+       // 开始分摊:只分摊自有目标金额、从父结点分摊到的金额(不分摊子结点汇总上来的金额)
        if (calcTools.hasTargetTotalFee(treeNode) && !(treeNode.data.tender_distribute && treeNode.data.tender_distribute == 2)) {
          treeNode.data.tender_activeTarget = treeNode.data.targetTotalFee;
 
@@ -2632,6 +2598,176 @@
        this.distributeTargetTotalFee(treeNode.nextSibling);
    };
 
+   // 反向调价逼近
+   reverseTenderApproach(callback){
+     let me = this;
+
+     let G_DIGIT = 0.01;      // 系数调整步距(0.1最终结果误差大。0.001目标金额与逼前金额差距大时无法有效逼近)
+     let times = 200;         // 逼近计算的极限次数。正常情况下“单位系数金额”列表中的结点耗尽即退出,这里指定轮数是最后保险阀,防止无限死循环。
+     let calcModel = 1;       // 计算模式:1 精度优先(差值不接近0不停,直到结点用完或极限次数用完。时间长精度高)2 速度优先(达到指定差值范围即熔断逼近。时间短差值大)
+     let diffProp = 0.0001;   // 速度优先模式下的:差值比例
+     let isTest = true;       // 测试
+
+     // 取树结点的调价系数。
+     function getCoe(node){
+       return (calcTools.isVP_or_GLJR(node)) ? node.data.rationQuantityCoe : node.data.quantityCoe.labour;
+     }
+
+     // 按指定的比例获取可接受的差值:如目标金额的万分之一。
+     function getPropV(node){
+       let v = parseFloat((node.data.targetTotalFee * diffProp).toFixed(0));   // node.data.feesIndex.common.totalFee
+       return Math.max(v, 1);
+     }
+
+     // 取根结点的:调后金额跟目标金额差值,看还有多少误差需要处理。
+     function  getRootDiff() {
+       let root = tender_obj.tenderTree.roots[0];
+       return (root.data.feesIndex.common.tenderTotalFee - root.data.targetTotalFee).toDecimal(3);
+     }
+
+     // 生成每单位差值的定额结点列表。
+     function getNodeDiffs() {
+       let arr = [];
+       for (let i = 0; i < tender_obj.tenderTree.items.length; i++) {
+         let node = tender_obj.tenderTree.items[i];
+         // 量价还是要参与,因为它贡献了金额,如果它的金额比重很大,它退出了,会导致其它结点过调。
+         // if (calcTools.isRationCategory(node) && (!calcTools.isVP_or_GLJR(node))){
+         if (calcTools.isRationCategory(node)){
+           let coe = getCoe(node);
+           if (coe!= 0) {
+             let diff = Math.abs(node.data.feesIndex.common.tenderTotalFee - node.data.feesIndex.common.totalFee);
+             node.data.tender_diffValuePerCoe  = (diff * G_DIGIT / coe).toDecimal(decimalObj.process);
+             node.data.tender_rowNo = i + 1;       // node在UI上显示的行号
+             arr.push(node);
+           }
+         }
+       };
+       arr.sort(function sortArr(a, b) { return a.data.tender_diffValuePerCoe - b.data.tender_diffValuePerCoe});
+
+       if (isTest){
+         let arr2 = [];
+         for (let i = 0; i < arr.length; i++) {
+           arr2.push({row: arr[i].data.tender_rowNo, code: arr[i].data.code + ' ' + arr[i].data.name, diff: arr[i].data.tender_diffValuePerCoe});
+         }
+         console.log(arr2);
+       }
+
+       return arr;
+     }
+
+     // 每单位差值的定额结点列表中,取离给定值最近的定额结点。
+     function getCloseNode(arr, value) {
+       let index = 0;
+       let d_value = Number.MAX_VALUE;
+       for (let i = 0; i < arr.length; i++) {
+         let new_d_value = Math.abs(arr[i].data.tender_diffValuePerCoe - value);
+         if (new_d_value <= d_value) {
+           if (new_d_value === d_value && arr[i].data.tender_diffValuePerCoe < arr[index].data.tender_diffValuePerCoe) {
+             continue;
+           }
+           index = i;
+           d_value = new_d_value;
+         }
+       }
+       return {idx: index, node: arr[index]}
+     }
+
+     // 逼近(单轮)。arr 参考取值列表。返回{type, node, nodeIdx}。
+     // type:1正常,2无结点,3结点过调。node:结点。nodeIdx:结点在列表中的索引位置。node.data.tender_rowNo 在UI上的行号。
+     function approach(arr){
+       let v = getRootDiff();
+       let obj = getCloseNode(arr, Math.abs(v));    // {idx, node}  极端:{0, undefind}
+       let closeNode = obj.node;
+       if (!closeNode)
+         return {type: 2, node: undefined, nodeIdx: -1};    // arr 被清空了
+
+       let d = (v > 0) ? -G_DIGIT : G_DIGIT;
+       let coe = getCoe(closeNode);
+       if ((coe + d) < 0)
+         return {type: 3, node: obj.node, nodeIdx: obj.idx};  // 再调的话,系数就变负数了,过调
+
+       if (calcTools.isVP_or_GLJR(closeNode)){
+         closeNode.data.tender_previousCoe = closeNode.data.rationQuantityCoe;  // tender_previousCoe: 上一次的调整系数,用于撤回
+         closeNode.data.rationQuantityCoe = (closeNode.data.rationQuantityCoe + d).toDecimal(decimalObj.process);
+       }
+       else {
+         closeNode.data.tender_previousCoe = closeNode.data.quantityCoe.labour;
+         closeNode.data.quantityCoe.labour = (closeNode.data.quantityCoe.labour + d).toDecimal(decimalObj.process)
+         closeNode.data.quantityCoe.material = (closeNode.data.quantityCoe.material + d).toDecimal(decimalObj.process)
+         closeNode.data.quantityCoe.machine = (closeNode.data.quantityCoe.machine + d).toDecimal(decimalObj.process)
+         closeNode.data.quantityCoe.main = (closeNode.data.quantityCoe.main + d).toDecimal(decimalObj.process)
+         closeNode.data.quantityCoe.equipment = (closeNode.data.quantityCoe.equipment + d).toDecimal(decimalObj.process)
+       };
+       me.calculate(closeNode, true, true, tenderTypes.ttCalc);
+       return {type: 1, node: obj.node, nodeIdx: obj.idx};
+     }
+
+     // 撤消最后一轮逼近(调过头了,回退一步)
+     function undoLastApproach(obj){
+       let closeNode = obj.node;
+       let coe = closeNode.data.tender_previousCoe;
+       if (calcTools.isVP_or_GLJR(closeNode)){
+         closeNode.data.rationQuantityCoe = coe
+       }
+       else {
+         closeNode.data.quantityCoe.labour = coe
+         closeNode.data.quantityCoe.material = coe
+         closeNode.data.quantityCoe.machine = coe
+         closeNode.data.quantityCoe.main = coe
+         closeNode.data.quantityCoe.equipment = coe
+       };
+       me.calculate(closeNode);
+       if (isTest) console.log(`       [索引${obj.nodeIdx} 行${obj.node.data.tender_rowNo}]过调,已回退`);
+     }
+
+     let root = tender_obj.tenderTree.roots[0];
+     let vArr = getNodeDiffs();
+     let propV = getPropV(root);
+     let d1 = getRootDiff();
+
+     if (isTest){
+       let tq = root.data.feesIndex.common.totalFee;
+       let mb = root.data.targetTotalFee;
+       let bbj = root.data.feesIndex.common.tenderTotalFee;
+       let ms = (calcModel == 2) ? `速度优先(0~${propV})` : "精度优先";
+       console.log(`调前${tq}|目标${mb}|调后无逼近${bbj}|差值${d1}|${ms}`);
+     }
+
+     // 多轮逼进
+     for (let i = 1; i <= times; i++) {
+       // 与目标金额差值在1以内,整数部分已相同,结果很棒。很多时候能达到完美的差值0,但不能用0判断,因为金额取小数时有问题。
+       if (Math.abs(d1) < 1) break;
+
+       // 速度优先模式下,结果到达差值范围内,即熔断逼近。
+       if (calcModel == 2){
+         if (Math.abs(d1) < propV) break;
+       };
+
+       let obj = approach(vArr);
+       if (obj.type == 2) break;        // 结点耗完,列表已清空,无结点可调。
+       if (obj.type == 3) {             // 结点过调,该结点任务已完成,不再参与调节,从列表中清除
+         vArr.splice(obj.nodeIdx, 1);
+         continue;
+       };
+
+       let d2 = getRootDiff();
+
+       if (isTest) console.log(`【第 ${i} 轮】调整[索引${obj.nodeIdx} 行${obj.node.data.tender_rowNo}] ${obj.node.data.tender_previousCoe} → ${getCoe(obj.node)},差值${d1} → ${d2}`);
+
+       if (Math.abs(d2) > Math.abs(d1)){    // 逼近后差距反而变大,证明过调,回退一步
+         undoLastApproach(obj);
+         vArr.splice(obj.nodeIdx, 1);
+       }
+       else if (Math.abs(d2) == Math.abs(d1)){  // 调前调后差距相同时,会死循环,也从列表中清除
+         vArr.splice(obj.nodeIdx, 1);
+       }
+       else {d1 = d2}
+     };
+
+     // 系数已处理就绪,直接正算即可。
+     this.calcAllNodesAndSave(calcAllType.catAll, callback, tenderTypes.ttCalc);
+   };
+
    setRationMap() {
      if (this.rationMap == null) {
        this.rationMap = {};
@@ -2682,11 +2818,16 @@
          tender_obj.createTreeNodes();
        }
 
-       this.initReverseTenderDatas();
+       // 反向调价原理:清理调价缓存 → 从子往父汇总(处理满载) → 从父往子分摊目标金额 → 反向计算调整系数(再由系数计算调后金额)等 → 调后金额逼近(挑结点,改系数,改完正算) → 全局正算、存储
+       this.reverseTenderInitDatas();
        this.prepareForDistribute(tender_obj.tenderTree.roots[0]);
        this.distributeTargetTotalFee(tender_obj.tenderTree.roots[0]);
-     };
-     this.calcAllNodesAndSave(calcAllType.catAll, callback, tender);
+       this.calcAllNodes(calcAllType.catAll, tender);    // 先来个反算(计算coe),得到基本的调后值(此值误差大,需要逼近)
+       this.reverseTenderApproach(callback);   // 逼近上述调后值
+     }
+     else{
+       this.calcAllNodesAndSave(calcAllType.catAll, callback, tender);
+     }
    };
  };
 

+ 7 - 7
web/building_saas/main/js/views/tender_price_view.js

@@ -60,12 +60,12 @@ let tender_obj={
                 newNode.sourceType = mainNode.sourceType;
                 newNode.mainNode = mainNode;
 
-                // 只显示到叶子清单层
-                if (calcTools.isLeafBill(newNode))
-                    newNode.expanded = false;
-
-                if (calcTools.isRationCategory(newNode))
-                    newNode.visible = false;
+                // 只显示到叶子清单层AAAAAA
+                // if (calcTools.isLeafBill(newNode))
+                //     newNode.expanded = false;
+                //
+                // if (calcTools.isRationCategory(newNode))
+                //     newNode.visible = false;
 
                 if (mainNode.children.length > 0) {
                     for (let c of mainNode.children) {
@@ -578,4 +578,4 @@ $(function () {
         gljCol.showTenderFields(showFields, true);
     });
 
-});
+});