Browse Source

Merge branch 'master' of http://smartcost.f3322.net:3000/SmartCost/ConstructionCost

zhangweicheng 7 years ago
parent
commit
f551960019

+ 2 - 1
.gitignore

@@ -2,4 +2,5 @@ node_modules/
 .git/
 dist/
 .idea/
-tmp/
+tmp/
+test/unit/logs

+ 1 - 1
config/gulpConfig.js

@@ -79,7 +79,7 @@ module.exports = {
         'web/building_saas/main/js/calc/calc_fees.js',
         'web/building_saas/main/js/calc/ration_calc.js',
         'web/building_saas/main/js/calc/bills_calc.js',
-        'public/calc_util.js',
+        // 'public/calc_util.js',
         'public/web/tree_sheet/tree_sheet_controller.js',
         'public/web/tree_sheet/tree_sheet_helper.js',
         'public/web/sheet/sheet_data_helper.js',

+ 0 - 2
modules/complementary_glj_lib/controllers/gljController.js

@@ -128,8 +128,6 @@ class GljController extends BaseController{
     mixUpdateGljItems(req, res){
         let user_id = req.session.sessionUser.ssoId,
             compilation_id = req.session.sessionCompilation._id;
-        console.log(user_id);
-        console.log(compilation_id);
         let data = JSON.parse(req.body.data);
         let updateItems = data.updateItems,
             addItems = data.addItems,

+ 1 - 1
modules/pm/controllers/pm_controller.js

@@ -280,7 +280,7 @@ module.exports = {
                 proj.unitPriceFiles = [];
                 proj.feeRateFiles = [];
             }
-            callback(request, response, null, 'success', rst);
+            callback(request, response, 0, 'success', rst);
         }
         catch (error){
             callback(request, response, true, error, null);

+ 3 - 1
modules/pm/models/project_model.js

@@ -5,6 +5,7 @@ import mongoose from 'mongoose';
 import async_c from 'async';
 import UnitPriceFileModel from "../../glj/models/unit_price_file_model";
 import UnitPriceFiles from '../../glj/models/schemas/unit_price_file';
+import billsQuantityDecimal from './project_property_bills_quantity_decimal';
 let FeeRateFiles = mongoose.model('fee_rate_file');
 let counter = require("../../../public/counter/counter.js");
 
@@ -103,6 +104,7 @@ ProjectsDAO.prototype.updateUserProjects = async function(userId, datas, callbac
                 }
                 if(data.updateData.projType === projectType.tender){
                     data.updateData.property.decimal = defaultDecimal;
+                    data.updateData.property.billsQuantityDecimal = billsQuantityDecimal;
                 }
                 newProject = new Projects(data.updateData);
                 // 查找同级是否存在同名数据
@@ -454,7 +456,7 @@ ProjectsDAO.prototype.recGC = async function(userID, datas, callback){
     }
     async_c.parallel(functions, function (err, results) {
         if(err) callback(err, 'fail', null);
-        else callback(false, 'success', null);
+        else callback(0, 'success', null);
     });
 };
 

+ 51 - 0
modules/pm/models/project_property_bills_quantity_decimal.js

@@ -0,0 +1,51 @@
+/**
+ * Created by Zhong on 2017/11/16.
+ */
+/*
+* 单位工程清单工程量精度模板
+* */
+const billsQuantityDecimal = [
+    {unit: '其他未列单位', decimal: 2},
+    {unit: 't', decimal: 3},
+    {unit: '吨', decimal: 3},
+    {unit: 'km', decimal: 3},
+    {unit: '千米', decimal: 3},
+    {unit: 'm', decimal: 2},
+    {unit: '米', decimal: 2},
+    {unit: 'm2', decimal: 2},
+    {unit: '平方米', decimal: 2},
+    {unit: 'm3', decimal: 2},
+    {unit: '立方米', decimal: 2},
+    {unit: 'kg', decimal: 2},
+    {unit: '千克', decimal: 2},
+    {unit: '公斤', decimal: 2},
+    {unit: '元', decimal: 2},
+    {unit: '工日', decimal: 0},
+    {unit: '台班', decimal: 0},
+    {unit: '个', decimal: 0},
+    {unit: '件', decimal: 0},
+    {unit: '根', decimal: 0},
+    {unit: '组', decimal: 0},
+    {unit: '宗', decimal: 0},
+    {unit: '付', decimal: 0},
+    {unit: '处', decimal: 0},
+    {unit: '系统', decimal: 0},
+    {unit: '部', decimal: 0},
+    {unit: '台', decimal: 0},
+    {unit: '辆', decimal: 0},
+    {unit: '套', decimal: 0},
+    {unit: '株', decimal: 0},
+    {unit: '从', decimal: 0},
+    {unit: '缸', decimal: 0},
+    {unit: '支', decimal: 0},
+    {unit: '只', decimal: 0},
+    {unit: '块', decimal: 0},
+    {unit: '座', decimal: 0},
+    {unit: '对', decimal: 0},
+    {unit: '份', decimal: 0},
+    {unit: '樘', decimal: 0},
+    {unit: '攒', decimal: 0},
+    {unit: '榀', decimal: 0}
+];
+
+export default billsQuantityDecimal;

+ 21 - 3
modules/reports/rpt_component/jpc_ex.js

@@ -208,6 +208,18 @@ JpcExSrv.prototype.createNew = function(){
     };
     JpcResult.outputAsSimpleJSONPage = function(rptTpl, dataObj, bands, page, controls) {
         let me = this, rst = null;
+        function getPageMergeBorder() {
+            let rst = null;
+            if (bands[JV.BAND_PROP_MERGE_BAND]) {
+                let mergedBand = bands[JV.BAND_PROP_MERGE_BAND];
+                rst = {};
+                rst[JV.PROP_LEFT] = parseInt(mergedBand[JV.PROP_LEFT].toFixed(0));
+                rst[JV.PROP_RIGHT] = parseInt(mergedBand[JV.PROP_RIGHT].toFixed(0));
+                rst[JV.PROP_TOP] = parseInt(mergedBand[JV.PROP_TOP].toFixed(0));
+                rst[JV.PROP_BOTTOM] = parseInt(mergedBand[JV.PROP_BOTTOM].toFixed(0));
+            }
+            return rst;
+        }
         if (me.totalPages >= page) {
             rst = {};
             rst[JV.PROP_PAGE_SEQ] = page;
@@ -218,7 +230,7 @@ JpcExSrv.prototype.createNew = function(){
                     if (me.flowTab.paging_option === JV.PAGING_OPTION_INFINITY) {
                         adHocMergePos = {};
                     }
-                    rst.cells = me.flowTab.outputAsSimpleJSONPage(rptTpl, dataObj, page, bands, controls, adHocMergePos, me);
+                    rst[JV.PROP_CELLS] = me.flowTab.outputAsSimpleJSONPage(rptTpl, dataObj, page, bands, controls, adHocMergePos, me);
                     if (adHocMergePos) {
                         adHocMergePos[JV.NODE_PAGE_SIZE] = JpcCommonHelper.getPageSize(rptTpl);
                         rst[JV.PAGE_SPECIAL_MERGE_POS] = adHocMergePos;
@@ -226,14 +238,20 @@ JpcExSrv.prototype.createNew = function(){
 
                 } else {
                     if (!me.isFollowMode) {
-                        rst.cells = me.flowTabEx.outputAsSimpleJSONPage(rptTpl, dataObj, page - (me.totalPages - me.exTotalPages), bands, controls, adHocMergePos, me);
+                        rst[JV.PROP_CELLS] = me.flowTabEx.outputAsSimpleJSONPage(rptTpl, dataObj, page - (me.totalPages - me.exTotalPages), bands, controls, adHocMergePos, me);
                     }
                 }
             } else if (me.crossTab) {
-                rst.cells = me.crossTab.outputAsSimpleJSONPage(rptTpl, dataObj, page, bands, controls, me);
+                rst[JV.PROP_CELLS] = me.crossTab.outputAsSimpleJSONPage(rptTpl, dataObj, page, bands, controls, me);
             } else if (me.billTab) {
                 //
             }
+            if (!(me.flowTab && me.flowTab.paging_option === JV.PAGING_OPTION_INFINITY)) {
+                let pageMergeBorder = getPageMergeBorder();
+                if (pageMergeBorder) {
+                    rst[JV.PROP_PAGE_MERGE_BORDER] = pageMergeBorder;
+                }
+            }
         }
         return rst;
     };

+ 248 - 18
modules/reports/util/rpt_construct_data_util.js

@@ -12,6 +12,14 @@ let treeUtil = require('../../../public/treeUtil');
 let projectConst = consts.projectConst;
 let projectConstList = consts.projectConstList;
 
+const GLJ_TYPE = {
+    Labour: 1,
+    Material: 2,
+    Machine: 3,
+    Main_Material: 4,
+    Equipment: 5
+}
+
 class Rpt_Common{
     initialize(rpt_tpl, currentDataObj) {
         this.template = rpt_tpl;
@@ -152,7 +160,7 @@ class Rpt_Data_Extractor {
                 let srcData = getModuleDataByKey(rawDataObj.prjData, preHandle[JV.PROP_DATA_KEY]);
                 switch(preHandle[JV.PROP_HANDLE_TYPE]) {
                     case JV.PROP_HANDLE_TYPE_SORT:
-                        sortData(srcData, preHandle);
+                        sortData(srcData, preHandle, rawDataObj.prjData);
                         break;
                     case JV.PROP_HANDLE_TYPE_FILTER:
                         filterData(srcData, preHandle, rawDataObj.prjData);
@@ -160,6 +168,12 @@ class Rpt_Data_Extractor {
                     case JV.PROP_HANDLE_TYPE_SUM:
                         summaryData(srcData, preHandle, rawDataObj.prjData);
                         break;
+                    case JV.PROP_HANDLE_TYPE_ADD_DUMMY:
+                        addDummyData(srcData, preHandle);
+                        break;
+                    case JV.PROP_HANDLE_TYPE_ADJUST:
+                        adjustData(srcData, preHandle);
+                        break;
                     default:
                         break;
                 }
@@ -320,7 +334,133 @@ function filterData(sourceData, handleCfg, prjData) {
     // fsUtil.wirteObjToFile(sourceData.data, "D:/GitHome/ConstructionCost/tmp/filteredRst.js");
 }
 
-function sortData(sourceData, sortCfg) {
+function adjustData(sourceData, adjustCfg) {
+    let rstArr = [];
+    for (let item of sourceData.data) {
+        if (item._doc) {
+            rstArr.push(item._doc);
+        } else {
+            rstArr.push(item);
+        }
+    }
+    for (let item of adjustCfg[JV.PROP_ADJUST_COLLECTION]) {
+        for (let rec of rstArr) {
+            if (item[JV.PROP_ADJUST_ACTION] === "prefix") {
+                rec[item.key] = item[JV.PROP_ADJUST_ACTION_VAL] + rec[item.key];
+            } else if (item[JV.PROP_ADJUST_ACTION] === "suffix") {
+                rec[item.key] = rec[item.key] + item[JV.PROP_ADJUST_ACTION_VAL];
+            }
+        }
+    }
+    delete sourceData.data;
+    sourceData.data = rstArr;
+}
+
+function getDupGrpKeyVals(sourceData, segKeys) {
+    let rst = [];
+    function pushKeyVal(item) {
+        let tr = {};
+        for (let i = 0; i < segKeys.length; i++) {
+            tr[segKeys[i]] = item[segKeys[i]];
+        }
+        rst.push(tr);
+    }
+    for (let idx = 0; idx < sourceData.length; idx++) {
+        let itemRec = sourceData[idx];
+        if (idx === 0) {
+            pushKeyVal(itemRec);
+            continue;
+        }
+        let hasDiff = false;
+        for (let i = 0; i < segKeys.length; i++) {
+            if (itemRec[segKeys[i]] !== sourceData[idx - 1][segKeys[i]]) {
+                hasDiff = true;
+                break;
+            }
+        }
+        if (hasDiff) {
+            pushKeyVal(itemRec);
+        }
+    }
+    return rst;
+}
+
+function addDummyData(sourceData, addCfg) {
+    let rstArr = [], tempRstArr = [];
+    for (let item of sourceData.data) {
+        if (item._doc) {
+            tempRstArr.push(item._doc);
+        } else {
+            tempRstArr.push(item);
+        }
+    }
+    for (let item of addCfg[JV.PROP_DUMMY_COLLECTION]) {
+        let newRecStr = JSON.stringify(item[JV.PROP_DUMMY_VAL]), cacheGrpKeyRecs = null;
+        if (item[JV.PROP_FREQUENCY] === "OncePerGrp") {
+            if (!cacheGrpKeyRecs) {
+                cacheGrpKeyRecs = {};
+            }
+            let cacheKey = "";
+            for (let key of item[JV.PROP_GRP_KEYS]) {
+                cacheKey += "_" + key;
+            }
+            if (!cacheGrpKeyRecs[cacheKey]) {
+                cacheGrpKeyRecs[cacheKey] = getDupGrpKeyVals(tempRstArr, item[JV.PROP_GRP_KEYS]);
+            }
+            for (let kv of cacheGrpKeyRecs[cacheKey]) {
+                let rec = JSON.parse(newRecStr);
+                for (let key of item[JV.PROP_GRP_KEYS]) {
+                    rec[key] = kv[key];
+                }
+                rstArr.push(rec);
+            }
+
+        } else if (item[JV.PROP_FREQUENCY] === "Once") {
+            rstArr.push(JSON.parse(newRecStr));
+        }
+    }
+    rstArr = rstArr.concat(tempRstArr);
+    delete sourceData.data;
+    sourceData.data = rstArr;
+}
+
+function getGLJBizType(orgType, orgCode, orgName) {
+    let rst = orgType;
+    if (orgName.indexOf("其他材料费") >= 0) {
+        rst = 299;
+    } else if (orgType === GLJ_TYPE.Labour) {
+        rst = 11;
+        if (orgCode === "000000") rst = 10;
+    } else if (orgType === GLJ_TYPE.Main_Material || orgType === GLJ_TYPE.Equipment) {
+        //未计价材料(主材 + 设备)
+        rst = 30 + orgType;
+    } else if (orgType === GLJ_TYPE.Material || (orgType >= 200 && orgType < 300)) {
+        //材料
+        if (orgCode === "000000") {
+            rst = 20; //2.材料
+        } else if (orgCode === "000000_1") {
+            rst = 30; //(1) 未计价材料
+        } else if (orgCode === "000000_2") {
+            rst = 40; //(2) 辅助材料
+        } else if (orgCode === "000000_3") {
+            rst = 50; //(3) 其他材料费
+        } else {
+            rst = 45; //到这里就只有辅助材料没有预处理了
+        }
+    } else if (orgType === GLJ_TYPE.Machine || (orgType >= 300 && orgType < 400)) {
+        //机械
+        if (orgCode === "000000") {
+            rst = 300; //3.机械
+        } else if (orgType === GLJ_TYPE.Machine) {
+            rst = 300.5;
+        } else {
+            rst = orgType;
+        }
+    }
+    return rst;
+}
+
+function sortData(sourceData, sortCfg, prjData) {
     let rst = sourceData.data, tempRstArr = [];
     let sortType = sortCfg[JV.PROP_SORT_TYPE];
     for (let item of sourceData.data) {
@@ -330,6 +470,56 @@ function sortData(sourceData, sortCfg) {
             tempRstArr.push(item);
         }
     }
+    function private_normal_sort(destArr, sortKeys) {
+        destArr.sort(function(a, b){
+            let compRst = 0;
+            for (let comp of sortKeys) {
+                let reverse = (comp.order === 'ascend')?1:(-1);
+                //
+                if (a[comp.key] > b[comp.key]) {
+                    compRst = reverse;
+                    break;
+                } else if (a[comp.key] < b[comp.key]) {
+                    compRst = -reverse;
+                    break;
+                }
+            }
+            return compRst;
+        });
+    }
+    function private_parent_sort(parentArr, parentKeys, childArr, childKeys) {
+        let tmpRst = {}, rst = [];
+        for (let pItem of parentArr) {
+            let pKey = "key";
+            for (let key of parentKeys) {
+                pKey += "_" + pItem[key];
+            }
+            tmpRst[pKey] = [];
+        }
+        for (let cItem of childArr) {
+            let cKey = "key";
+            for (let key of childKeys) {
+                cKey += "_" + cItem[key];
+            }
+            if (tmpRst[cKey]) {
+                tmpRst[cKey].push(cItem);
+            } else {
+                //unknown child value! should be filtered!
+            }
+        }
+        // childArr.splice(0);
+        for (let pItem of parentArr) {
+            let pKey = "key";
+            for (let key of parentKeys) {
+                pKey += "_" + pItem[key];
+            }
+            rst.push(tmpRst[pKey]);
+            // for (let rItem of tmpRst[pKey]) {
+            //     childArr.push(rItem);
+            // }
+        }
+        return rst;
+    }
     switch (sortType) {
         case "tree":
             rst = treeUtil.buildTreeNodeDirectly(tempRstArr);
@@ -340,23 +530,59 @@ function sortData(sourceData, sortCfg) {
             // fsUtil.wirteObjToFile(sourceData.data, "D:/GitHome/ConstructionCost/tmp/sortedAndFlattedRst.js");
             break;
         case "normal":
-            tempRstArr.sort(function(a, b){
-                let compRst = 0;
-                for (let comp of sortCfg[JV.PROP_SORT_KEYS]) {
-                    let reverse = (comp.order === 'ascend')?1:(-1);
-                    if (a[comp.key] > b[comp.key]) {
-                        compRst = reverse;
-                        break;
-                    } else if (a[comp.key] < b[comp.key]) {
-                        compRst = -reverse;
-                        break;
+            private_normal_sort(tempRstArr, sortCfg[JV.PROP_SORT_KEYS]);
+            delete sourceData.data;
+            sourceData.data = tempRstArr;
+            // fsUtil.wirteObjToFile(sourceData.data, "D:/GitHome/ConstructionCost/tmp/normalSortedRst.js");
+            break;
+        case "accord_to_parent":
+            let pcKey = sortCfg[JV.PROP_PARENT_CHILD_SORT_KEY];
+            let parentSrcData = getModuleDataByKey(prjData, pcKey[JV.PROP_PARENT_DATA_KEY]);
+            if (parentSrcData) {
+                let tempParentArr = [];
+                for (let item of parentSrcData.data) {
+                    if (item._doc) {
+                        tempParentArr.push(item._doc);
+                    } else {
+                        tempParentArr.push(item);
                     }
                 }
-                return compRst;
-            });
+                let sortedRstArr = private_parent_sort(tempParentArr, pcKey[JV.PROP_PARENT_SORT_KEYS], tempRstArr, pcKey[JV.PROP_CHILD_SORT_KEYS]);
+                if (sortCfg[JV.PROP_OTHER_SUB_SORT] && sortCfg[JV.PROP_OTHER_SUB_SORT].length > 0) {
+                    for (let sort of sortCfg[JV.PROP_OTHER_SUB_SORT]) {
+                        if (sort[JV.PROP_SORT_TYPE] === 'normal') {
+                            for (let subArr of sortedRstArr) {
+                                private_normal_sort(subArr, sort[JV.PROP_SORT_KEYS]);
+                            }
+                        } else if (sort[JV.PROP_SORT_TYPE] === 'self_define') {
+                            for (let subArr of sortedRstArr) {
+                                // console.log(subArr);
+                                let selfDefFunc = null;
+                                eval('selfDefFunc = ' + sort[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC]);
+                                subArr.sort(selfDefFunc);
+                                // console.log(subArr);
+                            }
+                        }
+                    }
+                }
+                tempRstArr.splice(0);
+                for (let item of sortedRstArr) {
+                    for (let subItem of item) {
+                        tempRstArr.push(subItem);
+                    }
+                }
+            }
+            delete sourceData.data;
+            sourceData.data = tempRstArr;
+            break;
+        case "self_define":
+            if (sortCfg[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC]) {
+                let selfDefFunc = null;
+                eval('selfDefFunc = ' + sortCfg[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC]);
+                tempRstArr.sort(selfDefFunc);
+            }
             delete sourceData.data;
             sourceData.data = tempRstArr;
-            // fsUtil.wirteObjToFile(sourceData.data, "D:/GitHome/ConstructionCost/tmp/normalSortedRst.js");
             break;
         default:
             //
@@ -583,7 +809,7 @@ function ext_getArrayItemByKey(arrayKey, itemKey, itemKeyValue, itemRstKey){
 
 }
 
-function ext_getPropertyByForeignId(foreignIdVal, adHocIdKey, propKey) {
+function ext_getPropertyByForeignId(foreignIdVal, adHocIdKey, propKey, dftValIfNotFound) {
     let rst = [], parentObj = this;
     let IdKey = (adHocIdKey)?adHocIdKey:"ID";
     let dtObj = parentObj["myOwnRawDataObj"];
@@ -618,7 +844,9 @@ function ext_getPropertyByForeignId(foreignIdVal, adHocIdKey, propKey) {
                         break;
                     }
                 }
-                // if (!isFound) rst.push[null];
+                if (!isFound) {
+                    rst.push(dftValIfNotFound);
+                }
             }
         } else {
             for (let item of dtObj.data) {
@@ -628,7 +856,9 @@ function ext_getPropertyByForeignId(foreignIdVal, adHocIdKey, propKey) {
                     break;
                 }
             }
-            // if (!isFound) rst.push[null];
+            if (!isFound) {
+                rst.push(dftValIfNotFound);
+            }
         }
     }
     return rst;

+ 0 - 437
public/calc_util.js

@@ -4,445 +4,8 @@
  * added by CSL, 2017-09-01 增加公式解析对象analyzer,用于解析用户修改公式、自定义表达式。
  */
 
-// 需求说小数位数固定为2位,这里预留缓冲接口,防止以后需求变卦。
-const Digit_Calc_Program = -2;              // 需求指定计算程序用到的小数位数。
-const Digit_Calc_Program_Default = -6;      // 需求末指定时默认用到的小数位数。
-function round(value, useDef = false) {
-    let digit = useDef ? Digit_Calc_Program_Default : Digit_Calc_Program;
-    return scMathUtil.roundTo(value, digit);
-};
-
-let executeObj = {
-    treeNode: null,
-    template: null,
-    calcBase: null,
-
-    at: function(ID) {
-        let me = executeObj,
-            rst = 0;
-        rst = me.template.compiledCalcItems[ID].unitFee;
-        rst = parseFloat(rst);
-        return rst;
-    },
-    base: function(calcBaseName) {
-        let me = executeObj, rst = 0,
-            //base = getRationCalcBase(calcBaseName);
-            base = me.calcBase[calcBaseName];
-
-        if (base != null) {
-            let price = 0, aprice = 0, mprice = 0, tmpSum = 0, mdSum = 0;
-
-            function isSubset(sub, arr){
-                // if(!(sub instanceof Array) || !(arr instanceof Array)) return false;
-                // if(sub.length > arr.length) return false;
-                for(var i = 0, len = sub.length; i < len; i++){
-                    if(arr.indexOf(sub[i]) == -1) return false;
-                }
-                return true;
-            }
-
-            // 机上人工费:多一层
-            if (isSubset(base.gljTypes, [gljType.MACHINE_LABOUR])) {
-                for (let glj of me.treeNode.data.gljList) {
-                       if (glj.type == gljType.GENERAL_MACHINE) {
-                            // 获取机械组成物
-                           let mds = projectObj.project.composition.getCompositionByCode(glj.code);
-                           if (!mds) mds = [];
-                           for (let md of mds){
-                               if (base.gljTypes.indexOf(md.glj_type) >= 0) {
-                                   price = md["base_price"];
-                                   if (!price) price = 0;
-                                   mdSum = mdSum +  round(md["consumption"] * price);
-                                   mdSum = round(mdSum, true);
-                               }
-                           };
-                           tmpSum = tmpSum + round(glj["quantity"] * mdSum, true);
-                           tmpSum = round(tmpSum, true);
-                       }
-                };
-            }else{
-                for (let glj of me.treeNode.data.gljList) {
-                    if (base.gljTypes.indexOf(glj.type) >= 0) {
-                        if (base.calcType == baseCalc){ price = glj["basePrice"];}
-                        else if (base.calcType == adjustCalc){price = glj["adjustPrice"];}
-                        else if (base.calcType == budgetCalc){price = glj["marketPrice"];}
-                        else if (base.calcType == diffCalc){
-                            aprice = glj["adjustPrice"];
-                            if (!aprice) aprice = 0;
-                            mprice = glj["marketPrice"];
-                            if (!mprice) mprice = 0;
-                            price = mprice - aprice;
-                        };
-                        if (!price) price = 0;
-                        tmpSum = round(tmpSum + round(glj["quantity"] * price, true), true);
-                    };
-                };
-            };
-
-            rst = round(tmpSum);
-        };
-
-        return rst;
-    }
-};
-
-let analyzer = {
-    calcTemplate: null,
-    success: true,
-
-    standard: function(expr){
-        let str = expr;
-        str = str.replace(/\s/g, "");               // 去空格、去中文空格
-        str = str.replace(/(/g, "(");              // 中文括号"("换成英文括号"("
-        str = str.replace(/)/g, ")");              // 中文括号")"换成英文括号")"
-        str = str.replace(/f/g, "F");               // f换成F
-        return str;
-    },
-
-    analyzeCalcBase: function(expr){
-        // 前提:必须无空格、无特殊符号
-        function getCalcBase(expr){
-            let base = '',
-                iPos1 = -1, iPos2 = -1;
-            for (let i = 0; i < expr.length; i++) {
-                if (expr[i] === '['){
-                    iPos1 = i;
-                }
-                else if (iPos1 != -1 && expr[i]===']'){
-                    iPos2 = i;
-                };
-
-                if (iPos1 != -1 && iPos2 != -1){
-                    base = expr.slice(iPos1, iPos2 + 1);
-                    break;
-                }
-            };
-            return base;
-        };
-        function calcBaseToIDExpr(base){
-            /*// for test. 公路模式,基数到ID
-            let id = -1;
-            if (base == '[人工费]'){
-                id = 111;
-            }
-            else if (base == '[材料费]'){
-                id = 222;
-            }
-            else if (base == '[机械费]'){
-                id = 333;
-            }
-            else id = "错误";
-
-            return "@('" + id + "')";*/
-            let baseValue = base.slice(1, -1);
-            return "base('" + baseValue + "')";
-        };
-
-        while (expr.indexOf('[') > 0) {
-            let base = getCalcBase(expr);
-            let id = calcBaseToIDExpr(base);
-            let baseValue = base.slice(1, -1);   // []会给下面的正则带来干扰,这里去掉
-            var pattBase =new RegExp(baseValue, "g");
-            expr = expr.replace(pattBase, id);
-            expr = expr.replace(/\[base\('/g, "base('");      // [@('       [base('
-            expr = expr.replace(/'\)\]/g, "')");        // ')]
-        };
-
-        return expr;
-    },
-
-    analyzeLineRef: function(expr){
-        let me = this;
-        function isOperator(char){
-            var operator = "+-*/()";
-            return operator.indexOf(char) > -1;
-        };
-        function lineNumToID(lineNum){
-            if (lineNum > me.calcTemplate.calcItems.length){
-                me.success = false;
-                return '越界';
-            }
-            else{
-                let id = me.calcTemplate.calcItems[lineNum - 1].ID;
-                return id;
-            }
-        };
-        // 前提:必须无空格、无特殊符号、标准大写F
-        function getSection(expr){
-            let section = '',
-                iPos1 = -1, iPos2 = -1;
-            for (let i = 0; i < expr.length; i++) {
-                if (expr[i] === 'F'){
-                    iPos1 = i;
-                }
-                else if (iPos1 != -1 && isOperator(expr[i])){
-                    iPos2 = i;
-                }
-                else if (iPos1 != -1 && i == expr.length - 1){
-                    iPos2 = i + 1;
-
-                };
-                if (iPos1 != -1 && iPos2 != -1){
-                    section = expr.slice(iPos1, iPos2);
-                    break;
-                }
-            };
-            return section;
-        };
-        function sectionToIDExpr(section){
-            if (section){
-                let lineNum = section.slice(1);
-                if (isNaN(lineNum)){
-                    me.success = false;
-                    return '错误';      // 这里的返回提示不能加上section,因为会无限循环
-                }
-                else
-                    return "@('" + lineNumToID(lineNum) + "')";
-            }
-            else return '';
-        };
-
-        while (expr.indexOf('F') > 0) {
-            let sec = getSection(expr);
-            let id = sectionToIDExpr(sec);
-            var pattSec =new RegExp(sec, "g");
-            expr = expr.replace(pattSec, id);
-        };
-        return expr;
-    },
-
-    analyzeUserExpr: function(calcTemplate, calcItem){
-        let me = this;
-        me.calcTemplate = calcTemplate;
-        let expr = calcItem.dispExpr;
-        // 标准化:处理特殊字符、中文符号、大小写
-        expr = me.standard(expr);
-        calcItem.dispExpr = expr;
-        // 先换掉计算基数
-        expr = me.analyzeCalcBase(expr);
-        // 再换掉行引用
-        expr = me.analyzeLineRef(expr);
-        calcItem.expression = expr;
-        return me.success;
-    }
-};
-
 class Calculation {
-    // 先编译公用的基础数据
-    compilePublics(feeRates, labourCoes, feeTypes, calcBases){
-        let me = this;
-        me.compiledFeeRates = {};
-        me.compiledLabourCoes = {};
-        me.compiledTemplates = {};
-        me.compiledFeeTypes = {};
-        me.compiledFeeTypeNames = [];
-        me.compiledCalcBases = {};
-        me.saveForReports = [];
-
-        let private_compile_feeRateFile = function() {
-            if (feeRates) {
-                for (let rate of feeRates) {
-                    me.compiledFeeRates["feeRate_" + rate.ID] = rate;
-                }
-            }
-        };
-        let private_compile_labourCoeFile = function() {
-            if (labourCoes) {
-                for (let coe of labourCoes) {
-                    me.compiledLabourCoes["LabourCoe_" + coe.ID] = coe;
-                }
-            }
-        };
-        let private_compile_feeType = function() {
-            if (feeTypes) {
-                for (let ft of feeTypes) {
-                    me.compiledFeeTypes[ft.type] = ft.name;
-                    me.compiledFeeTypes[ft.name] = ft.type;    // 中文预编译,可靠性有待验证
-                    me.compiledFeeTypeNames.push(ft.name);
-                }
-            }
-        };
-        let private_compile_calcBase = function() {
-            if (calcBases) {
-                for (let cb of calcBases) {
-                    me.compiledCalcBases[cb.dispName] = cb;         // 中文预编译,可靠性有待验证
-                }
-            }
-        };
-
-        private_compile_feeRateFile();
-        private_compile_labourCoeFile();
-        private_compile_feeType();
-        private_compile_calcBase();
-    };
-
-    compileTemplate(template){
-        let me = this;
-        me.compiledTemplates[template.ID] = template;
-        template.hasCompiled = false;
-        template.errs = [];
-
-        let private_extract_ID = function(str, idx){
-            let rst = '', lBracket = 0, rBracket = 0, firstIdx = idx, lastIdx = 0;
-            for (let i = idx; i < str.length; i++) {
-                if (str[i] === '(') {
-                    lBracket++;
-                    if (lBracket == 1) firstIdx = i + 1;
-                }
-                if (str[i] === ')') {
-                    rBracket++;
-                    if (lBracket == rBracket) {
-                        lastIdx = i - 1;
-                        if (lastIdx > firstIdx) {
-                            if (str[firstIdx] === "'") firstIdx++;
-                            if (str[lastIdx] !== "'") lastIdx++;
-                            if (lastIdx > firstIdx) {
-                                rst = str.slice(firstIdx, lastIdx);
-                            }
-                        }
-                        break;
-                    }
-                }
-            }
-            return rst;
-        };
-        let private_parse_ref = function(item, itemIdx){
-            let idx = item.expression.indexOf('@(', 0);
-            while (idx >= 0) {
-                let ID = private_extract_ID(item.expression, idx);
-                if (ID.length > 0) {
-                    let subItem = template.compiledCalcItems[ID];
-                    if (subItem) {
-                        if (subItem.ID !== item.ID) {
-                            private_parse_ref(subItem, template.compiledCalcItems[ID + "_idx"]);
-                        } else {
-                            template.errs.push("There exists the self refer ID: " + ID);
-                        }
-                    } else {
-                        template.errs.push("There exists the invalid ID by which could not find the item: " + ID);
-                        console.log('invalid ID: ' + ID);
-                    }
-                }
-                idx = item.expression.indexOf('@(', idx + ID.length + 3);
-            }
-            if (template.compiledSeq.indexOf(itemIdx) < 0) {
-                template.compiledSeq.push(itemIdx);
-            }
-        };
-        let private_setup_seq = function(item, itemIdx){
-            if (template.compiledSeq.indexOf(itemIdx) < 0) {
-                private_parse_ref(item, itemIdx);
-            }
-        };
-        let private_compile_items = function() {
-            for (let idx of template.compiledSeq) {
-                let item = template.calcItems[idx];
-                item.dispExprUser = item.dispExpr;    // 用于界面显示。disExpr是公式模板,不允许修改:人工系数占位符被修改后变成数值,第二次无法正确替换。
-                item.compiledExpr = item.expression.split('@(').join('$CE.at(');
-                item.compiledExpr = item.compiledExpr.split('base(').join('$CE.base(');
-                if (item.labourCoeID){
-                    let lc = me.compiledLabourCoes["LabourCoe_" + item.labourCoeID].coe;
-                    item.dispExprUser = item.dispExpr.replace(/L/gi, lc.toString());
-                    item.compiledExpr = item.compiledExpr.replace(/L/gi, lc.toString());
-                };
-
-                if (item.feeRateID) {
-                    let orgFeeRate = item.feeRate;
-                    let cmf = me.compiledFeeRates["feeRate_" + item.feeRateID];
-                    item.feeRate = cmf?cmf.rate:100;
-
-                    if (!orgFeeRate || (orgFeeRate && orgFeeRate != item.feeRate)){
-                        me.saveForReports.push({templatesID: template.ID, calcItem: item});
-                    }
-                };
-
-                // 字段名映射
-                item.displayFieldName = me.compiledFeeTypes[item.fieldName];
-            }
-        };
-
-        if (template && template.calcItems && template.calcItems.length > 0) {
-            template.compiledSeq = [];
-            template.compiledCalcItems = {};
-
-            for (let i = 0; i < template.calcItems.length; i++) {
-                let item = template.calcItems[i];
-                template.compiledCalcItems[item.ID] = item;
-                template.compiledCalcItems[item.ID + "_idx"] = i;
-            }
-
-            for (let i = 0; i < template.calcItems.length; i++) {
-                private_setup_seq(template.calcItems[i], i);
-            }
-            if (template.errs.length == 0) {
-                private_compile_items();
-                template.hasCompiled = true;
-            } else {
-                console.log('errors: ' + template.errs.toString());
-            }
-        };
-    };
-
-    calculate($treeNode){
-        let me = this;
-        let templateID = $treeNode.data.programID;
-        if (!templateID) templateID = 1;
-        let template = me.compiledTemplates[templateID];
-        $treeNode.data.calcTemplate = template;
-
-        if ($treeNode && template.hasCompiled) {
-            let $CE = executeObj;
-            $CE.treeNode = $treeNode;
-            $CE.template = template;
-            $CE.calcBase = me.compiledCalcBases;
-
-            if (!$treeNode.data.fees) {
-                $treeNode.data.fees = [];
-                $treeNode.data.feesIndex = {};
-                $treeNode.changed = true;
-            };
-
-            for (let idx of template.compiledSeq) {
-                let calcItem = template.calcItems[idx];
-
-                let feeRate = calcItem.feeRate;
-                if (!feeRate) feeRate = 100;    // 100%
-                calcItem.unitFee = round(eval(calcItem.compiledExpr) * feeRate * 0.01);   // 如果eval()对清单树有影响,就换成小麦的Expression对象再试
-
-                let quantity = $treeNode.data.quantity;
-                if (!quantity) quantity = 0;
-                calcItem.totalFee = round(calcItem.unitFee * quantity);
-
-                // 费用同步到定额
-                // 引入小麦的字段检测后,快速切换定额出现计算卡顿现象,过多的循环造成。这里把她的代码拆出来,减少微循环。
-                if (calcItem.fieldName != '') {
-                    if (!$treeNode.data.feesIndex[calcItem.fieldName]){
-                        let fee = {
-                            'fieldName': calcItem.fieldName,
-                            'unitFee': calcItem.unitFee,
-                            'totalFee': calcItem.totalFee,
-                            'tenderUnitFee': 0,
-                            'tenderTotalFee': 0
-                        };
-                        $treeNode.data.fees.push(fee);
-                        $treeNode.data.feesIndex[calcItem.fieldName] = fee;
-                        $treeNode.changed = true;
-                    }
-                    else{
-                        if ($treeNode.data.feesIndex[calcItem.fieldName].unitFee != calcItem.unitFee){
-                            $treeNode.data.feesIndex[calcItem.fieldName].unitFee = calcItem.unitFee;
-                            $treeNode.changed = true;
-                        };
 
-                        if ($treeNode.data.feesIndex[calcItem.fieldName].totalFee != calcItem.totalFee){
-                            $treeNode.data.feesIndex[calcItem.fieldName].totalFee = calcItem.totalFee;
-                            $treeNode.changed = true;
-                        };
-                    }
-                };
-            };
-        }
-    }
 };
 
 /*

+ 9 - 0
public/stringUtil.js

@@ -136,6 +136,15 @@ module.exports = {
         }
         return rst;
     },
+    trim: function(str) {
+        return str.replace(/(^\s*)|(\s*$)/g, "");
+    },
+    leftTrim: function(str) {
+        return str.replace(/(^\s*)/g,"");
+    },
+    rightTrim: function(rst) {
+        return str.replace(/(\s*$)/g,"");
+    },
     convertNumToChinese : function(num, isCurrency) {
         if (!/^\d*(\.\d*)?$/.test(num)) { return "Number is wrong!"; }
         let AA, BB;

+ 17 - 0
public/web/rpt_value_define.js

@@ -47,6 +47,11 @@ const JV = {
 
     NODE_MAP_DATA_HANDLE_INFO: "映射数据预处理",
     PROP_DATA_KEY: "映射数据对象",
+    PROP_PARENT_DATA_KEY: "父映射数据对象",
+    PROP_PARENT_CHILD_SORT_KEY: "父子排序键",
+    PROP_PARENT_SORT_KEYS: "父排序键值集",
+    PROP_CHILD_SORT_KEYS: "子排序键值集",
+    PROP_OTHER_SUB_SORT: "其他子排序",
     PROP_HANDLE_TYPE: "预处理类型",
     PROP_FILTER_KEY: "过滤键值集",
     PROP_FILTER_COMPARE_OBJ: "compareObjKey",
@@ -56,7 +61,18 @@ const JV = {
     PROP_HANDLE_TYPE_FILTER: "过滤",
     PROP_HANDLE_TYPE_SUM: "合计",
     PROP_HANDLE_TYPE_SORT: "排序",
+    PROP_HANDLE_TYPE_ADD_DUMMY: "增加Dummy数据",
+    PROP_HANDLE_TYPE_ADJUST: "数据调整",
+
+    PROP_ADJUST_COLLECTION: "数据调整集",
+    PROP_ADJUST_ACTION: "action",
+    PROP_ADJUST_ACTION_VAL: "actionValue",
+    PROP_DUMMY_COLLECTION: "Dummy数据集",
+    PROP_DUMMY_VAL: "Dummy数据对象值",
+    PROP_FREQUENCY: "频率",
+    PROP_GRP_KEYS: "GrpKeyIds",
     PROP_SORT_TYPE: "排序方式",
+    PROP_SORT_TYPE_SELF_DEFINE_LOGIC: "自定义逻辑",
     PROP_SORT_KEYS: "排序键值集",
     PROP_SUM_GROUP_KEYS: "分组键值集",
     PROP_SUM_SUM_KEYS: "统计键值集",
@@ -227,6 +243,7 @@ const JV = {
     OFFSET_IDX_BOTTOM: 3,
 
     PROP_PAGE_SEQ: "page_seq",
+    PROP_PAGE_MERGE_BORDER: "page_merge_border",
     PROP_CELLS: "cells",
 
     PAGING_OPTION_NORMAL: 'normal',

+ 7 - 0
public/web/scMathUtil.js

@@ -60,7 +60,14 @@ let scMathUtil = {
         let result = bin;
         let iDot = bin.indexOf('.');
         if (iDot < 0){return result};
+        // 二进制浮点数带小数位数最大长度
+        let floatLength = 53;
         let iLength = bin.length;
+        // 长度小于53说明该二进制数尾数长度小于Double类型限制,未被截断,无需进位
+        if (iLength < floatLength) {
+            return result;
+        }
+        iLength = bin.length;
         for (let i = iLength - 1; i > iDot; i--){
             let num = Number(bin[i]);
             if (num === 0){

+ 4 - 3
test/unit/reports/test_tpl_09_1.js

@@ -39,7 +39,8 @@ let demoPrjId = - 1;
 let demoRptId = 226, pagesize = "A4";
 
 let userId_Leng = 1142; //小冷User Id
-demoPrjId = 720; //QA: DW3
+// demoPrjId = 720; //QA: DW3
+demoPrjId = 838; //QA:
 /*/
 let userId_Dft = userId_Leng;
 /*/
@@ -90,7 +91,7 @@ test('测试 - 获取project部分数据: ', function (t) {
                     //     newData.push(JSON.stringify(item));
                     // }
                     // fsUtil.writeArrayToFile(newData, "D:/GitHome/ConstructionCost/tmp/getProjectData_partial.js");
-                    fsUtil.wirteObjToFile(prjObj, "D:/GitHome/ConstructionCost/tmp/getProjectObjectNew.js");
+                    // fsUtil.wirteObjToFile(prjObj, "D:/GitHome/ConstructionCost/tmp/getProjectObjectNew.js");
                     fsUtil.wirteObjToFile(results, "D:/GitHome/ConstructionCost/tmp/getProjectData_partialNew.js");
                     t.pass('pass succeeded!');
                     t.end();
@@ -131,7 +132,7 @@ test('测试 - 测试模板啦: ', function (t) {
                     let maxPages = printCom.totalPages;
                     let pageRst = printCom.outputAsSimpleJSONPageArray(rptTpl, tplData, 1, maxPages, defProperties);
                     if (pageRst) {
-                        fsUtil.wirteObjToFile(pageRst, "D:/GitHome/ConstructionCost/tmp/testBuiltPageResult.js");
+                        // fsUtil.wirteObjToFile(pageRst, "D:/GitHome/ConstructionCost/tmp/testBuiltPageResult.js");
                     } else {
                         console.log("oh! no pages were created!")
                     }

+ 4 - 4
web/building_saas/complementary_glj_lib/js/gljComponent.js

@@ -309,7 +309,7 @@ let gljComponentOprObj = {
          if(args.col === 4 && me.currentEditingComponent.code && args.editingText && args.editingText.trim().length > 0){//消耗量
             let consumeAmt = parseFloat(args.editingText);
             if(!isNaN(consumeAmt) && consumeAmt !== me.currentEditingComponent.consumeAmt){
-                let roundCons = scMathUtil.roundTo(consumeAmt, -3);
+                let roundCons = scMathUtil.roundTo(parseFloat(consumeAmt), -3);
                 let component = that.currentGlj.component;
                 for(let i = 0; i < component.length; i++){
                     if(component[i].ID === that.currentComponent[args.row].ID){
@@ -430,7 +430,7 @@ let gljComponentOprObj = {
                 if(row + i < that.currentComponent.length){
                     let currentObj = that.currentComponent[row + i];
                     if(items[i].consumeAmt.trim().length > 0 && items[i].consumeAmt !== currentObj.consumeAmt){
-                        let roundCons = scMathUtil.roundTo(items[i].consumeAmt, 3);
+                        let roundCons = scMathUtil.roundTo(parseFloat(items[i].consumeAmt), 3);
                         isChange = true;
                         currentObj.consumeAmt = roundCons;
                         for(let j = 0; j < component.length; j++){
@@ -496,8 +496,8 @@ let gljComponentOprObj = {
     reCalGljBasePrc: function (component) {
         let me = gljComponentOprObj, gljBasePrc = 0;
         for(let i = 0; i < component.length; i++){
-            let roundBasePrc = scMathUtil.roundTo(component[i].basePrice, -2);
-            gljBasePrc += scMathUtil.roundTo(roundBasePrc * component[i].consumeAmt, -2);
+            let roundBasePrc = scMathUtil.roundTo(parseFloat(component[i].basePrice), -2);
+            gljBasePrc += scMathUtil.roundTo(roundBasePrc * parseFloat(component[i].consumeAmt), -2);
         }
         return gljBasePrc;
     }

+ 4 - 2
web/building_saas/css/main.css

@@ -10,6 +10,9 @@ body {
 .btn.disabled, .btn:disabled {
   color:#999
 }
+.btn-link:focus, .btn-link:hover{
+  text-decoration: none
+}
 /*自定义css*/
 .header {
     background: #e1e1e1
@@ -306,6 +309,5 @@ body {
   border-bottom:1px solid #ddd
 }
 .navbar-crumb span{
-  max-width: 200px;
-  display: inline-block;
+  max-width: 200px
 }

+ 5 - 4
web/building_saas/main/html/main.html

@@ -264,7 +264,7 @@
                                 <li class="nav-item"><a class="nav-link" data-toggle="pill" href="#poj-settings-2" role="tab">工程特征</a></li>
                                 <li class="nav-item"><a class="nav-link" data-toggle="pill" href="#poj-settings-3" role="tab">指标信息</a></li>
                                 <li class="nav-item"><a class="nav-link" data-toggle="pill" href="#poj-settings-4" role="tab">关于计算</a></li>
-                                <li class="nav-item"><a class="nav-link" data-toggle="pill" href="#poj-settings-5" role="tab">清单工程精度</a></li>
+                                <li class="nav-item"><a class="nav-link" data-toggle="pill" href="#poj-settings-billsQuanDecimal" id="tab_poj-settings-bqDecimal" role="tab">清单工程精度</a></li>
                                 <li class="nav-item"><a class="nav-link" data-toggle="pill" href="#poj-settings-decimal" role="tab" id="tab_poj-settings-decimal">小数位数</a></li>
                                 <li class="nav-item"><a class="nav-link" data-toggle="pill" href="#poj-settings-6" role="tab" id="tab_poj-settings-6">人工单价调整</a></li>
                             </ul>
@@ -337,8 +337,8 @@
                                     </div>
                                 </div>
                                 <!--清单工程精度-->
-                                <div class="tab-pane fade" id="poj-settings-5" role="tabpanel">
-                                    <div class="modal-auto-height" id="quantityPrecision">
+                                <div class="tab-pane fade" id="poj-settings-billsQuanDecimal" role="tabpanel">
+                                    <div class="modal-auto-height" style="overflow: hidden;" id="billsQuanDecimal">
                                     </div>
                                 </div>
                                 <!--小数位数-->
@@ -682,7 +682,7 @@
         <script type="text/javascript" src="/web/building_saas/main/js/calc/calc_fees.js"></script>
         <script type="text/javascript" src="/web/building_saas/main/js/calc/ration_calc.js"></script>
         <script type="text/javascript" src="/web/building_saas/main/js/calc/bills_calc.js"></script>
-        <script type="text/javascript" src="/public/calc_util.js"></script>
+        <!--<script type="text/javascript" src="/public/calc_util.js"></script>-->
         <!-- Controller -->
         <script type="text/javascript" src="/public/web/tree_sheet/tree_sheet_controller.js"></script>
         <script type="text/javascript" src="/public/web/tree_sheet/tree_sheet_helper.js"></script>
@@ -693,6 +693,7 @@
         <script type="text/javascript" src="/web/building_saas/main/js/views/project_info.js"></script>
         <script type="text/javascript" src="/web/building_saas/main/js/views/project_view.js"></script>
         <script type="text/javascript" src="/web/building_saas/main/js/views/options_view.js"></script>
+        <script type="text/javascript" src="/web/building_saas/main/js/views/project_property_bills_quantity_decimal.js"></script>
         <script type="text/javascript" src="/web/building_saas/main/js/views/project_property_decimal_view.js"></script>
         <script type="text/javascript" src="/web/building_saas/main/js/main_ajax.js"></script>
         <script type="text/javascript" src="/web/building_saas/main/js/main.js"></script>

+ 600 - 231
web/building_saas/main/js/models/calc_program.js

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

+ 6 - 10
web/building_saas/main/js/models/main_consts.js

@@ -36,14 +36,10 @@ const CP_Col_Width = {          // 多处计算程序界面的列宽统一设置
     totalFee: 90
 };
 
-const CP_GatherType = {
-    rations: 'rations',
-    bills: 'bills'
-};
-
-const childrenType = {
-    ration: 'ration',
-    bill: 'bill',
-    volumePrice: 'volumePrice',
-    formula: 'formula'
+treeNodeCalcType = {
+    ctRationCalcProgram: 1,
+    ctBillCalcProgram: 2,
+    ctGatherRations: 3,
+    ctGatherBills: 4,
+    ctCalcBaseValue: 5
 };

+ 2 - 2
web/building_saas/main/js/views/calc_program_manage.js

@@ -52,9 +52,8 @@ let rationPM = {
         };
         me.mainSpread = sheetCommonObj.buildSheet($('#mainSpread')[0], me.mainSetting, me.datas.length);
         me.detailSpread = sheetCommonObj.buildSheet($('#detailSpread')[0], me.detailSetting, me.datas[0].calcItems.length);
-
         var fieldName = new GC.Spread.Sheets.CellTypes.ComboBox();
-        fieldName.items(projectObj.project.calcProgram.calc.compiledFeeTypeNames);
+        fieldName.items(projectObj.project.calcProgram.compiledFeeTypeNames);
         me.detailSpread.getSheet(0).getRange(-1, 4, -1, 1).cellType(fieldName);
 
         me.mainSpread.getSheet(0).bind(GC.Spread.Sheets.Events.EnterCell, me.onMainEnterCell);
@@ -75,6 +74,7 @@ let rationPM = {
         me.detailSpread.suspendPaint();
         var dSheet = me.detailSpread.getSheet(0);
         var dData = me.datas[row].calcItems;
+        dSheet.setRowCount(dData.length, GC.Spread.Sheets.SheetArea.viewport);
         sheetCommonObj.showData(dSheet, me.detailSetting, dData);
         me.detailSpread.resumePaint();
     },

+ 3 - 17
web/building_saas/main/js/views/calc_program_view.js

@@ -228,23 +228,9 @@ let calcProgramObj = {
     showData: function (treeNode) {
         var me = this;
         me.treeNode = treeNode;
-        me.datas = projectObj.project.calcProgram.getCalcDatas(treeNode);
-
-        function checkSerialNo() {     // 检测序号列。隐藏列的方式焦点难控制,体验不佳,这里动态添加删除。
-            let srlCol = {headerName: "序号", headerWidth: CP_Col_Width.serialNo, dataCode: "serialNo", dataType: "String", hAlign: "center"};
-            if (me.datas.length > 0 && me.datas[0].serialNo){
-                if (me.setting.header[0].dataCode != "serialNo"){
-                    me.setting.header.splice(0, 0, srlCol);
-                };
-            }
-            else {
-                if (me.setting.header[0].dataCode == "serialNo"){
-                    me.setting.header.splice(0, 1);
-                };
-            }
-        };
-
-        checkSerialNo();
+        projectObj.project.calcProgram.calculate(treeNode);
+        projectObj.project.calcProgram.saveNode(treeNode);
+        me.datas = treeNode.data.calcTemplate.calcItems;
         sheetCommonObj.initSheet(me.sheet, me.setting, me.datas.length);
         sheetCommonObj.showData(me.sheet, me.setting, me.datas);
 

+ 1 - 0
web/building_saas/main/js/views/project_info.js

@@ -28,6 +28,7 @@ var projectInfoObj = {
                 that.projectInfo = data;
                 //init decimal
                 setDecimal(decimalObj, data.property.decimal);
+                billsQuanDecimal.datas = data.property.billsQuantityDecimal;
                 $('#fullpath').html(that.getFullPathHtml(that.projectInfo));
             }
         });

+ 348 - 0
web/building_saas/main/js/views/project_property_bills_quantity_decimal.js

@@ -0,0 +1,348 @@
+/**
+ * Created by Zhong on 2017/11/15.
+ */
+/*
+* 清单工程量精度
+* */
+let billsQuanDecimal = {
+    datas: [],
+    //根据单位获取精度, 需要用清单工程量精度的调用此接口
+    decimal: function (unit) {
+        for(let i = 0, len = this.datas.length; i < len; i++){
+            if(unit === this.datas[i].unit){
+                return this.datas[i].decimal;
+            }
+        }
+        return this.datas[0].decimal;
+    }
+};
+
+//点了确定才更新数据
+let billsDecimalView = {
+    default: {min: 0, max: 4, decimal: 3},//以防后面修改,小数数位的取值范围, default.decimal为新增时默认的小数位
+    angleDecimal: {unit: '其他未列单位', decimal: 2},//防止出现工程没有初始默认的清单工程量精度模板(正常不会出现)
+    cache: [],//temp
+    workBook: null,
+    setting:{
+        header: [
+            {name: '计量单位', dataCode: 'unit', width: 120, vAlign: 'center', hAlign: 'left'},
+            {name: '小数位数', dataCode: 'decimal', width: 80, vAlign: 'center', hAlign: 'center'}
+        ],
+        options: {
+            tabStripVisible:  false,
+            allowCopyPasteExcelStyle : false,
+            allowUserDragDrop : false,
+            allowUserDragFill: false,
+            scrollbarMaxAlign : true
+        }
+    },
+
+    renderSheetFuc: function (sheet, fuc) {
+        sheet.suspendPaint();
+        sheet.suspendEvent();
+        fuc();
+        sheet.resumePaint();
+        sheet.resumeEvent();
+    },
+
+    setOptions: function (workbook, opts) {
+        for(let opt in opts){
+            workbook.options[opt] = opts[opt];
+        }
+    },
+
+    getComboItems: function (min, max) {
+        let rst = [];
+        while (min <= max){
+            rst.push(min);
+            min ++;
+        }
+        return rst;
+    },
+    
+    setComboBox: function (sheet, items) {
+        let combo = new GC.Spread.Sheets.CellTypes.ComboBox();
+        combo.items(items);
+        combo.editable(true);
+        sheet.getRange(-1, 1, -1, 1).cellType(combo);
+    },
+
+    buildHeader: function (sheet, headers) {
+        let me = billsDecimalView;
+        let fuc = function () {
+            sheet.setColumnCount(headers.length);
+            sheet.setRowHeight(0, 40, GC.Spread.Sheets.SheetArea.colHeader);
+            me.setComboBox(sheet, me.getComboItems(me.default.min, me.default.max));
+            for(let i = 0, len = headers.length; i < len; i++){
+                sheet.setValue(0, i, headers[i].name, GC.Spread.Sheets.SheetArea.colHeader);
+                sheet.setColumnWidth(i, headers[i].width, GC.Spread.Sheets.SheetArea.colHeader);
+            }
+        };
+        me.renderSheetFuc(sheet, fuc);
+    },
+
+    buildSheet: function () {
+        if(!this.workBook){
+            this.workBook = new GC.Spread.Sheets.Workbook($('#billsQuanDecimal')[0], {sheetCount: 1});
+            this.setOptions(this.workBook, this.setting.options);
+            this.buildHeader(this.workBook.getActiveSheet(), this.setting.header);
+            this.bindEvent(this.workBook);
+            this.onContextmenuOpr();
+        }
+    },
+
+    bindEvent: function (workBook) {
+        const _events = GC.Spread.Sheets.Events;
+        let sheet = workBook.getActiveSheet();
+        sheet.bind(_events.EditStarting, this.onEditStarting);
+        sheet.bind(_events.EditEnded, this.onEditEnded);
+        sheet.bind(_events.ClipboardPasting, this.onClipboardPasting);
+        sheet.bind(_events.ClipboardPasted, this.onClipboardPasted);
+    },
+
+    showData(datas){
+        let sheet = this.workBook.getActiveSheet();
+        let cols = this.setting.header;
+        let fuc = function () {
+            sheet.setRowCount(datas.length);
+            for(let col = 0, cLen = cols.length; col < cLen; col++){
+                sheet.getRange(-1, col, -1, 1).hAlign(GC.Spread.Sheets.HorizontalAlign[cols[col]['hAlign']]);
+                sheet.getRange(-1, col, -1, 1).hAlign(GC.Spread.Sheets.VerticalAlign[cols[col]['vAlign']]);
+                for(let row = 0, rLen = datas.length; row < rLen; row++){
+                    sheet.setValue(row, col, datas[row][cols[col]['dataCode']]);
+                }
+            }
+        };
+        this.renderSheetFuc(sheet, fuc);
+    },
+
+    onEditStarting: function (sender, args) {
+        if(args.col === 0 && args.row === 0){//其他未列单位不可编辑
+            args.cancel = true;
+        }
+    },
+
+    onEditEnded: function (sender, args) {
+        let me = billsDecimalView;
+        let v =  args.editingText ? args.editingText.toString().trim() : '';
+        if(v.length === 0){
+            return;
+        }
+        if(args.col === 0){
+            if(me.hasUnit(me.cache, v)){
+                alert('已存在此计量单位');
+                args.sheet.setValue(args.row, args.col, me.isDef(me.cache[args.row]) ? me.cache[args.row].unit : '');
+            }
+            else {
+                me.cache[args.row].unit = v;
+                if(!me.isValidDecimal(me.cache[args.row].decimal)){
+                    me.cache[args.row].decimal = me.default.decimal;
+                    args.sheet.setValue(args.row, 1, me.default.decimal);
+                }
+            }
+        }
+        else if(args.col === 1){
+            if(!me.isValidDecimal(v)){
+                alert('小数位数只能是'+ me.default.min + '-' + me.default.max + '的整数');
+                args.sheet.setValue(args.row, args.col, me.isDef(me.cache[args.row]) ? me.cache[args.row].decimal : '');
+            }
+            else {
+                me.cache[args.row].decimal = v;
+            }
+        }
+    },
+
+    onClipboardPasting: function (sender, args) {
+        if(args.cellRange.row === 0 && args.cellRange.col === 0){
+            args.cancel = true;
+            alert('不可更改其他未列计量单位');
+        }
+    },
+
+    onClipboardPasted: function (sender, args) {
+        let me = billsDecimalView;
+        let items = sheetCommonObj.analyzePasteData(me.setting, args);
+        for(let i = 0, len = items.length; i < len; i++){
+            let row = args.cellRange.row + i;
+            if(me.isDef(me.cache[row])){
+                if(me.isDef(items[i].unit) && !me.hasUnit(me.cache, items[i].unit)){
+                    me.cache[row].unit = items[i].unit;
+                }
+                if(me.isValidDecimal(items[i].decimal)){
+                    me.cache[row].decimal = items[i].decimal;
+                }
+            }
+        }
+        me.showData(me.cache);
+    },
+
+    onContextmenuOpr: function () {//右键菜单
+        let me = billsDecimalView;
+        $.contextMenu({
+            selector: '#billsQuanDecimal',
+            build: function($triggerElement, e){
+                //控制允许右键菜单在哪个位置出现
+                let target = SheetDataHelper.safeRightClickSelection($triggerElement, e, me.workBook);
+                let sheet = me.workBook.getSheet(0);
+                if(target.hitTestType === 3 && typeof target.row !== 'undefined'){//在表格内
+                    let delDis = false;
+                    let insertDis = false;
+                    //控制按钮是否可用
+                    sheet.setActiveCell(target.row, target.col);
+                    if(target.row === 0){//不可删除其他未列单位行
+                        delDis = true;
+                    }
+                    if(!me.isDef(me.cache) || target.row >= me.cache.length){//有数据才能删除
+                        delDis = true;
+                    }
+                    return {
+                        callback: function(){},
+                        items: {
+                            "insert": {name: "添加", disabled: insertDis, icon: "fa-sign-in", callback: function (key, opt) {
+                                //插入空行
+                                me.addRow(me.cache, sheet, target.row);
+                            }},
+                            "delete": {name: "删除", disabled: delDis, icon: "fa-remove", callback: function (key, opt) {
+                               me.deleteRow(me.cache, sheet, target.row);
+                            }}
+                        }
+                    };
+                }
+                else{
+                    return false;
+                }
+            }
+        });
+    },
+    //新增一空白行
+    addRow: function (all, sheet, row) {
+        let func = function () {
+            let data = Object.create(null);
+            data.unit = '';
+            data.decimal = '';
+            //新增空行
+            if(row === 0){//保证其他未列单位在第一行
+                row = 1;
+            }
+            //新增空白数据
+            all.splice(row, 0, data);
+            sheet.addRows(row, 1);
+        };
+        this.renderSheetFuc(sheet, func);
+    },
+
+    deleteRow: function (all, sheet, row) {
+        let func = function () {
+            all.splice(row, 1);
+            sheet.deleteRows(row, 1);
+        };
+        this.renderSheetFuc(sheet, func);
+    },
+
+    isDef: function (v) {
+        return v !== undefined && v !== null;
+    },
+
+    isData: function (v) {
+        return this.isDef(v) && v.toString().trim().length > 0;
+    },
+
+    isValidDecimal: function (v) {
+        return this.isData(v) && !isNaN(v) && parseInt(v) % 1 === 0 && parseInt(v) >= this.default.min && parseInt(v) <= this.default.max;
+    },
+
+    hasUnit: function (all, v) {
+        for(let i = 0, len = all.length; i < len; i++){
+            if(all[i].unit === v){
+                return true;
+            }
+        }
+        return false;
+    },
+
+    rowHasData: function (sheet, row) {
+        let v = sheet.getValue(row, 0);
+        if(v && v.toString().trim().length > 0) {
+            return true;
+        }
+        return false;
+    },
+
+    toUpdate: function (orgV, newV) {//org: billsDecimal.datas, newV: toBillsDecimalDatas(cache)
+        for(let i = 0, len = orgV.length; i < len; i++){
+            if(!this.isDef(newV[i])){
+                return true;
+            }
+            else {
+                if(orgV[i].unit !== newV[i].unit || orgV[i].decimal !== newV[i].decimal){
+                    return true;
+                }
+            }
+        }
+        return false;
+    },
+
+    //将tempDatas提取出新的请单工程量精度数据
+    toBillsDecimalDatas: function (datas) {
+        let rst = [];
+        for(let i = 0, len = datas.length; i < len; i++){
+            if(this.isData(datas[i].unit) && this.isValidDecimal(datas[i].decimal)){
+                datas[i].decimal = parseInt(datas[i].decimal);
+                rst.push(datas[i]);
+            }
+        }
+        return rst;
+    },
+    
+    a_update: function (datas) {
+        let url = '/pm/api/updateProjects';
+        let updateData = {
+            updateType: 'update',
+            updateData: {
+                ID: parseInt(scUrlUtil.GetQueryString('project')),
+                'property.billsQuantityDecimal': datas
+            }
+        };
+        let postData = {
+            user_id: userID,
+            updateData: [updateData]
+        };
+        CommonAjax.post(url, postData, function () {
+            billsQuanDecimal.datas = datas;
+        });
+    }
+};
+
+$(document).ready(function () {
+    $('#poj-set').on('shown.bs.modal', function (e) {
+        //init Spread
+        if(billsDecimalView.isDef(billsQuanDecimal.datas)){
+            billsDecimalView.cache = billsDecimalView.cache.concat(billsQuanDecimal.datas);
+        }
+        if(billsDecimalView.cache.length === 0){
+            billsDecimalView.cache.push(billsDecimalView.angleDecimal);
+        }
+        billsDecimalView.buildSheet();
+        billsDecimalView.showData(billsDecimalView.cache);
+    });
+
+    $('#poj-set').on('hidden.bs.modal', function (e) {
+        //destroy Spread
+        if(billsDecimalView.workBook){
+            billsDecimalView.workBook.destroy();
+            billsDecimalView.workBook = null;
+        }
+        billsDecimalView.cache = [];
+    });
+
+    $('#tab_poj-settings-bqDecimal').on('shown.bs.tab', function () {
+        billsDecimalView.workBook.refresh();
+    });
+
+    $('#property_ok').bind('click', function () {
+        let newBillsDecimalDatas = billsDecimalView.toBillsDecimalDatas(billsDecimalView.cache);
+        if(billsDecimalView.toUpdate(billsQuanDecimal.datas, newBillsDecimalDatas)){
+            billsDecimalView.a_update(newBillsDecimalDatas);
+        }
+    });
+});

+ 4 - 1
web/building_saas/main/js/views/project_view.js

@@ -258,7 +258,10 @@ var projectObj = {
             if (fieldName === 'code') {
                 projectObj.updateCode(node, value);
             } else if (fieldName === 'quantity' && project.quantity_detail.quantityEditChecking(value,node,fieldName)) {
-                projectObj.updateAndReCalculate(node, fieldName, value);
+                node.data.quantity = value;
+                project.calcProgram.calculate(node);
+                project.calcProgram.saveNode(node);
+                // projectObj.updateAndReCalculate(node, fieldName, value);
             } else if (fieldName === 'feesIndex.common.unitFee') {
                 projectObj.updateAndReCalculate(node, fieldName, value);
             } else if(fieldName ==='feeRate'){

+ 2 - 44
web/building_saas/pm/html/project-management.html

@@ -40,53 +40,11 @@
         </div>
     </div>
     <%include ../../../common/html/header.html %>
-    <nav class="navbar navbar-expand-lg justify-content-between navbar-light p-0">
-        <ul class="nav navbar-nav px-1">
-            <li class="nav-item dropdown">
-                <a class="nav-link dropdown-toggle" href="http://example.com" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">文件</a>
-                <div class="dropdown-menu" aria-labelledby="supportedContentDropdown">
-                    <a class="dropdown-item" href="#">Action</a>
-                    <a class="dropdown-item" href="#">Another action</a>
-                    <a class="dropdown-item" href="#">Something else here</a>
-                </div>
-            </li>
-            <li class="nav-item dropdown">
-                <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">编辑</a>
-                <div class="dropdown-menu">
-                    <a class="dropdown-item" href="#">Action</a>
-                    <a class="dropdown-item" href="#">Another action</a>
-                    <a class="dropdown-item" href="#">Something else here</a>
-                </div>
-            </li>
-            <li class="nav-item dropdown">
-                <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">工具</a>
-                <div class="dropdown-menu">
-                    <a class="dropdown-item" href="#">定额库编辑器</a>
-                    <a class="dropdown-item" href="/complementaryGlj">工料机库编辑器</a>
-                </div>
-            </li>
-            <li class="nav-item dropdown">
-                <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-question-circle-o"></i> 帮助</a>
-                <div class="dropdown-menu">
-                    <a class="dropdown-item" href="#">帮助</a>
-                    <a class="dropdown-item" href="#">升级说明</a>
-                    <a class="dropdown-item" href="#">重庆市2008定额说明</a>
-                    <a class="dropdown-item" href="#">纵横官网</a>
-                    <a class="dropdown-item" href="#">动画教程</a>
-                    <a class="dropdown-item" href="#">联系客服</a>
-                    <a class="dropdown-item" href="#">关于</a>
-                </div>
-            </li>
-        </ul>
-        <form class="form-inline">
-            <input class="form-control form-control-sm mr-1" type="text" placeholder="告诉我你想做什么">
-        </form>
-    </nav>
 </div>
     <div class="main">
         <div class="poj-manage container-fluid">
             <div class="row">
-                <div class="col-lg-2">
+                <div class="col-auto">
                     <div class="poj-cate">
                         <input type="text" class="my-2 form-control form-control-sm" placeholder="搜索所有工程">
                         <ul class="nav nav-pills flex-column">
@@ -111,7 +69,7 @@
                         </ul>
                     </div>
                 </div>
-                <div class="col-lg-10">
+                <div class="col">
                     <div class="tab-content">
                         <div class="tab-pane active" id="pm_all" role="tabpanel">
                         <div class="toolsbar">

+ 7 - 25
web/building_saas/pm/js/pm_gc.js

@@ -551,37 +551,19 @@ function e_recProj(btn){
 }
 
 function a_getGC(callback){
-    $.ajax({
-        type: 'post',
-        url: '/pm/api/getGCDatas',
-        data: {data: JSON.stringify({user_id: userID})},
-        dataType: 'json',
-        timeout: 5000,
-        success: function (result) {
-            if(!result.error){
-                if(callback){
-                    callback(result.data);
-                }
-            }
+    CommonAjax.post('/pm/api/getGCDatas', {user_id: userID}, function (rstData) {
+        if(callback){
+            callback(rstData);
         }
     });
 }
 
 function a_rec(nodes, callback){
-    $.ajax({
-        type: 'post',
-        url: '/pm/api/recGC',
-        data: {data: JSON.stringify({user_id: userID, nodes: nodes})},
-        dataType: 'json',
-        timeout: 5000,
-        success: function (result) {
-            if(!result.error){
-                if(callback){
-                    callback();
-                }
-            }
+    CommonAjax.post('/pm/api/recGC', {user_id: userID, nodes: nodes}, function (rstData) {
+        if(callback){
+            callback();
         }
-    })
+    });
 }
 
 //去除重名,回收站不处理重名,只保证恢复到项目管理中不出现重名

+ 18 - 1
web/building_saas/report/js/jpc_output.js

@@ -175,6 +175,15 @@ let JpcCanvasOutput = {
             private_drawCellText(cell, fonts, controls);
             ctx.restore();
         }
+        function getIniPageMergeBorder(mergedBand) {
+            let rst = {};
+            rst[JV.PROP_LEFT] = mergedBand[JV.PROP_LEFT];
+            rst[JV.PROP_RIGHT] = mergedBand[JV.PROP_RIGHT];
+            rst[JV.PROP_TOP] = mergedBand[JV.PROP_TOP];
+            rst[JV.PROP_BOTTOM] = mergedBand[JV.PROP_BOTTOM];
+            rst[JV.PROP_STYLE] = mergedBand[JV.PROP_STYLE];
+            return rst;
+        }
 
         if (pageObj && pageObj.items.length > 0 && canvas && pageObj.items.length >= pageIdx) {
             let page = pageObj.items[pageIdx - 1],
@@ -182,9 +191,17 @@ let JpcCanvasOutput = {
                 styles = pageObj[JV.NODE_STYLE_COLLECTION],
                 controls = pageObj[JV.NODE_CONTROL_COLLECTION],
                 mergedBand = pageObj[JV.BAND_PROP_MERGE_BAND];
+            let newPageMergeBand = getIniPageMergeBorder(mergedBand);
+            if (page[JV.PROP_PAGE_MERGE_BORDER]) {
+                newPageMergeBand[JV.PROP_LEFT] = page[JV.PROP_PAGE_MERGE_BORDER][JV.PROP_LEFT];
+                newPageMergeBand[JV.PROP_RIGHT] = page[JV.PROP_PAGE_MERGE_BORDER][JV.PROP_RIGHT];
+                newPageMergeBand[JV.PROP_TOP] = page[JV.PROP_PAGE_MERGE_BORDER][JV.PROP_TOP];
+                newPageMergeBand[JV.PROP_BOTTOM] = page[JV.PROP_PAGE_MERGE_BORDER][JV.PROP_BOTTOM];
+            }
             for (let j = 0; j < page.cells.length; j++) {
                 let cell = page.cells[j];
-                private_drawCell(cell, fonts, styles, controls, mergedBand);
+                // private_drawCell(cell, fonts, styles, controls, mergedBand);
+                private_drawCell(cell, fonts, styles, controls, newPageMergeBand);
             }
         }
     },

+ 1 - 0
web/building_saas/report/js/jpc_output_value_define.js

@@ -13,6 +13,7 @@ let JV = {
     NODE_CONTROL_COLLECTION: "control_collection",
 
     BAND_PROP_MERGE_BAND: "MergeBand",
+    PROP_PAGE_MERGE_BORDER: "page_merge_border",
 
     PROP_NAME: "Name",
     PROP_VALUE: "Value",

+ 29 - 2
web/common/html/header.html

@@ -1,11 +1,38 @@
-<nav class="navbar navbar-expand-lg navbar-light bg-light p-0 justify-content-between">
+<nav class="navbar navbar-expand-lg p-0 d-flex">
     <span class="header-logo px-2">Smartcost</span>
     <div class="navbar-text navbar-crumb p-0" id="fullpath">
         <% if (action !== 'index' || controller !== 'pm') {%>
         <span class="text-truncate"><a href="/pm">项目管理</a></span>
         <% } %>
     </div>
-    <div class="float-lg-right navbar-text p-0">
+    <ul class="nav navbar-nav px-1">
+        <li class="nav-item">
+            <a class="nav-link" href="#" aria-expanded="false" data-toggle="modal" data-target="#poj-set"><i class="fa fa-cube"></i> 项目属性</a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link" href="#" aria-expanded="false"><i class="fa fa-sliders"></i> 选项</a>
+        </li>
+        <li class="nav-item dropdown">
+            <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-wrench"></i> 工具</a>
+            <div class="dropdown-menu">
+                <a class="dropdown-item" href="#">定额库编辑器</a>
+                <a class="dropdown-item" href="/complementaryGlj">工料机库编辑器</a>
+            </div>
+        </li>
+        <li class="nav-item dropdown">
+            <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-question-circle-o"></i> 帮助</a>
+            <div class="dropdown-menu">
+                <a class="dropdown-item" href="#">帮助</a>
+                <a class="dropdown-item" href="#">升级说明</a>
+                <a class="dropdown-item" href="#">重庆市2008定额说明</a>
+                <a class="dropdown-item" href="#">纵横官网</a>
+                <a class="dropdown-item" href="#">动画教程</a>
+                <a class="dropdown-item" href="#">联系客服</a>
+                <a class="dropdown-item" href="#">关于</a>
+            </div>
+        </li>
+    </ul>
+    <div class="ml-auto navbar-text p-0">
         <div class="dropdown d-inline-block navbar-nav">
             <button class="btn btn-link btn-sm dropdown-toggle" type="button" data-toggle="dropdown"><%= sessionUser.email %></button>
             <div class="dropdown-menu dropdown-menu-right">