Procházet zdrojové kódy

台账分解,批量插入子后项

MaiXinRong před 7 roky
rodič
revize
2d4f525e70

+ 46 - 0
app/controller/ledger_controller.js

@@ -289,6 +289,52 @@ module.exports = app => {
         }
 
         /**
+         * 批量插入数据
+         *
+         * data = {id, batchData, batchType}
+         * data.batchType = 'batchInsertChild'/'batchInsertNext'
+         * data.batchData = [{name, children}] -- 项目节列表
+         * data.batchData.children = [{code, name, unit, unit_price, quantity}] -- 工程量清单列表
+         *
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async batchInsert(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            try {
+                const tenderId = ctx.session.sessionUser.tenderId;
+                if (!tenderId) {
+                    throw '当前未打开标段';
+                }
+                const data = JSON.parse(ctx.request.body.data);
+                if ((isNaN(data.id) || data.id <= 0) || !data.batchType) {
+                    throw '参数错误';
+                }
+
+                console.log(data.batchType);
+                switch (data.batchType) {
+                    case 'batchInsertChild':
+                        responseData.data = await ctx.service.ledger.batchInsertChild(tenderId, data.id, data.batchData);
+                        break;
+                    case 'batchInsertNext':
+                        responseData.data = await ctx.service.ledger.batchInsertNext(tenderId, data.id, data.batchData);
+                        break;
+                    default:
+                        throw '参数错误';
+                }
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
+
+        /**
          * 台账变更页面
          *
          * @param {object} ctx - egg全局变量

+ 180 - 3
app/public/js/ledger.js

@@ -40,6 +40,14 @@ $(document).ready(function() {
     SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
 
     const treeOperationObj = {
+        getSelectNode: function (sheet) {
+            if (!sheet || !sheet.zh_tree) {
+                return null;
+            } else {
+                const sel = sheet.getSelections()[0];
+                return sheet.zh_tree.nodes[sel.row];
+            }
+        },
         /**
          * 刷新顶部按钮是否可用
          * @param sheet
@@ -355,6 +363,23 @@ $(document).ready(function() {
                 self.refreshTree(sheet, result);
                 self.refreshOperationValid(sheet, sheet.getSelections());
             });
+        },
+        batchInsertData: function (spread, data, fun) {
+            const self = this;
+            const sheet = spread.getActiveSheet();
+            const row = sheet.getSelections()[0].row;
+
+            const tree = sheet.zh_tree;
+            if (!tree) { return; }
+
+            const node = tree.nodes[row];
+            if (!node) { return; }
+
+            tree.batchInsert('batch-insert', node, data, function (result) {
+                self.refreshTree(sheet, result);
+                self.refreshOperationValid(sheet, sheet.getSelections());
+                fun();
+            });
         }
     };
 
@@ -391,6 +416,7 @@ $(document).ready(function() {
     });
     treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet(), ledgerSpread.getActiveSheet().getSelections());
 
+    let batchInsertObj;
     // 右键菜单
     $.contextMenu({
         selector: '#ledger-spread',
@@ -475,7 +501,39 @@ $(document).ready(function() {
                         document.execCommand('paste');
                     }
                 }
-            }
+            },
+            'batchInsertChild': {
+                name: '批量插入子项',
+                icon: 'fa-sign-in',
+                disabled: function (key, opt) {
+                    return false;
+                },
+                callback: function(key, opt) {
+                    $('h5', $('#batch')).text('批量插入子项');
+                    if (!batchInsertObj) {
+                        batchInsertObj = new BatchInsertObj($('#batch'), key);
+                    } else {
+                        batchInsertObj.batchType = key;
+                    }
+                    $('#batch').modal('show');
+                },
+            },
+            'batchInsertNext': {
+                name: '批量插入后项',
+                icon: 'fa-sign-in',
+                disabled: function (key, opt) {
+                    return false;
+                },
+                callback: function (key, opt) {
+                    $('h5', $('#batch')).text('批量插入后项');
+                    if (!batchInsertObj) {
+                        batchInsertObj = new BatchInsertObj($('#batch'), key);
+                    } else {
+                        batchInsertObj.batchType = key;
+                    }
+                    $('#batch').modal('show');
+                },
+            },
         }
     });
 
@@ -537,7 +595,7 @@ $(document).ready(function() {
             } else if (tab.attr('content') === '#deal-bills' && !dealBills) {
                 dealBills = new DealBills($('#deal-bills-spread')[0], {
                     cols: [
-                        {title: '清单编号', field: 'code', width: 120, readOnly: true, cellType: 'tree'},
+                        {title: '清单编号', field: 'code', width: 120, readOnly: true},
                         {title: '名称', field: 'name', width: 230, readOnly: true},
                         {title: '单位', field: 'unit', width: 50, readOnly: true},
                         {title: '单价', field: 'unit_price', width: 50, readOnly: true},
@@ -595,7 +653,7 @@ $(document).ready(function() {
                 SpreadJsObj.loadSheetData(self.spread.getActiveSheet(), 'tree', self.pathTree);
             });
         }
-    };
+    }
     class DealBills {
         constructor (obj, spreadSetting) {
             const self = this;
@@ -623,5 +681,124 @@ $(document).ready(function() {
             });
         }
     }
+    class BatchInsertObj {
+        constructor (obj, batchType) {
+            const self = this;
+            this.obj = obj;
+            this.batchType = batchType;
+
+            this.xmSpreadSetting = {
+                cols: [
+                    {title: '部位', field: 'bw', width: 80, readOnly: true},
+                    {title: '图册号', field: 'drawingCode', width: 60, readOnly: true},
+                    {title: '数量1', field: 'bills1', width: 50, readOnly: true},
+                    {title: '数量2', field: 'bills2', width: 50, readOnly: true},
+                ],
+                emptyRows: 6,
+            };
+            this.xmSpread = SpreadJsObj.createNewSpread($('.batch-l-t')[0]);
+            SpreadJsObj.initSheet(this.xmSpread.getActiveSheet(), this.xmSpreadSetting);
+
+            this.gclSpreadSetting = {
+                cols: [
+                    {title: '编号', field: 'code', width: 80, readOnly: true},
+                    {title: '名称', field: 'name', width: 120, readOnly: true},
+                    {title: '单位', field: 'unit', width: 50, readOnly: true},
+                    {title: '单价', field: 'unit_price', width: 50, readOnly: true},
+                    {title: '图册号', field: 'name', width: 60, readOnly: true},
+                ],
+                emptyRows: 2,
+            };
+            this.gclSpread = SpreadJsObj.createNewSpread($('.batch-l-b')[0]);
+            SpreadJsObj.initSheet(this.gclSpread.getActiveSheet(), this.gclSpreadSetting);
+            this.gclSpread.getActiveSheet().setColumnWidth(0, 45, GC.Spread.Sheets.SheetArea.rowHeader);
+            this.gclSpread.getActiveSheet().getCell(0, 0, GC.Spread.Sheets.SheetArea.rowHeader).text('清单1');
+            this.gclSpread.getActiveSheet().getCell(1, 0, GC.Spread.Sheets.SheetArea.rowHeader).text('清单2');
+
+            this.dealSpreadSetting = {
+                cols: [
+                    {title: '清单编号', field: 'code', width: 80, readOnly: true},
+                    {title: '名称', field: 'name', width: 120, readOnly: true},
+                    {title: '单位', field: 'unit', width: 50, readOnly: true},
+                    {title: '单价', field: 'unit_price', width: 50, readOnly: true},
+                ],
+                emptyRows: 0,
+            };
+            this.dealSpread = SpreadJsObj.createNewSpread($('.batch-r')[0]);
+            SpreadJsObj.initSheet(this.dealSpread.getActiveSheet(), this.dealSpreadSetting);
+            postData('/deal/get-data', {}, function (data) {
+                SpreadJsObj.loadSheetData(self.dealSpread.getActiveSheet(), 'data', data);
+
+                self.dealSpread.bind(GC.Spread.Sheets.Events.CellDoubleClick, function (e, info) {
+                    const deal = info.sheet.zh_data[info.row];
+                    const sel = self.gclSpread.getActiveSheet().getSelections()[0];
+                    self.gclSpread.getActiveSheet().getCell(sel.row, 0).value(deal.code);
+                    self.gclSpread.getActiveSheet().getCell(sel.row, 1).value(deal.name);
+                    self.gclSpread.getActiveSheet().getCell(sel.row, 2).value(deal.unit);
+                    self.gclSpread.getActiveSheet().getCell(sel.row, 3).value(deal.unit_price);
+                    if (sel.row + 1 === self.gclSpread.getActiveSheet().getRowCount()) {
+                        const count = sel.row + 2;
+                        self.gclSpread.getActiveSheet().setRowCount(count);
+                        self.gclSpread.getActiveSheet().getCell(sel.row + 1, 0, GC.Spread.Sheets.SheetArea.rowHeader).text('清单' + count);
+
+                        const colCount = self.xmSpread.getActiveSheet().getColumnCount() + 1
+                        self.xmSpread.getActiveSheet().setColumnCount(colCount);
+                        self.xmSpread.getActiveSheet().getCell(0, colCount - 1, GC.Spread.Sheets.SheetArea.colHeader).text('数量' + count);
+                    }
+                    self.gclSpread.getActiveSheet().setSelection(sel.row + 1, sel.col, 1, 1);
+                })
+            });
+
+            this.obj.bind('shown.bs.modal', function () {
+                self.xmSpread.refresh();
+                self.gclSpread.refresh();
+                self.dealSpread.refresh();
+            });
+
+            $('#batch-ok').click(function () {
+                treeOperationObj.batchInsertData(ledgerSpread, {
+                    batchData: self.getBatchData(),
+                    batchType: self.batchType,
+                }, function () {
+                    self.obj.modal('hide');
+                });
+            });
+        }
+        getBatchData () {
+            const result = [];
+            const xmSheet = this.xmSpread.getActiveSheet(), gclSheet = this.gclSpread.getActiveSheet();
+            for (let iRow = 0; iRow < xmSheet.getRowCount(); iRow++) {
+                if (xmSheet.getText(iRow, 0) === '') { continue; }
+                const xmj = {
+                    name: xmSheet.getText(iRow, 0),
+                    children: [],
+                };
+                result.push(xmj);
+                for (let iCol = 2; iCol < xmSheet.getColumnCount(); iCol++) {
+                    let value;
+                    try {
+                        value = parseFloat(xmSheet.getText(iRow, iCol));
+                        if (value !== 0 && !isNaN(value)) {
+                            const gcl = {
+                                b_code: gclSheet.getText(iCol - 2, 0),
+                                name: gclSheet.getText(iCol - 2, 1),
+                                unit: gclSheet.getText(iCol - 2, 2),
+                                quantity: value,
+                            }
+                            try {
+                                gcl.unit_price = parseFloat(gclSheet.getText(iCol - 2, 3));
+                            } catch (err) {
+                            }
+                            if (gcl.b_code !== '' || gcl.name !== '') {
+                                xmj.children.push(gcl);
+                            }
+                        }
+                    } catch (err) {
+                    }
+                }
+            }
+            return result;
+        }
+    }
 });
 

+ 18 - 0
app/public/js/path_tree.js

@@ -393,6 +393,24 @@ const createNewPathTree = function (setting) {
             }
             callback(result);
         });
+    };
+
+    proto.batchInsert = function (url, node, data, callback) {
+        const self = this;
+        data.id = node[treeSetting.id];
+        postData(url, data, function (datas) {
+            const result = {};
+            if (datas.update) {
+                result.update = self._updateData(datas.update);
+            }
+            if (datas.create) {
+                result.create = self._loadData(datas.create);
+            }
+            if (datas.delete) {
+                result.delete = self._freeData(datas.delete);
+            }
+            callback(result);
+        });
     }
 
     return new PathTree();

+ 2 - 0
app/router.js

@@ -39,6 +39,8 @@ module.exports = app => {
     app.post('/ledger/update-info', sessionAuth, 'ledgerController.updateInfo');
     app.post('/ledger/paste-block', sessionAuth, 'ledgerController.pasteBlock');
     app.post('/ledger/add-by-std', sessionAuth, 'ledgerController.addFromStandardLib');
+    app.post('/ledger/batch-insert', sessionAuth, 'ledgerController.batchInsert');
+
     app.get('/ledger/change', sessionAuth, 'ledgerController.change');
     app.get('/ledger/index', sessionAuth, 'ledgerController.index');
 

+ 183 - 10
app/service/ledger.js

@@ -87,9 +87,9 @@ module.exports = app => {
                 if (!result) {
                     throw '新增数据错误';
                 }
-                this.transaction.commit();
+                await this.transaction.commit();
             } catch (error) {
-                this.transaction.rollback();
+                await this.transaction.rollback();
                 result = false;
             }
 
@@ -239,7 +239,7 @@ module.exports = app => {
             });
             this.sqlBuilder.orderBy = [['order', 'DESC']];
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
-            const resultData = this.db.queryOne(sql, sqlParam);
+            const resultData = await this.db.queryOne(sql, sqlParam);
 
             return resultData;
         }
@@ -704,7 +704,7 @@ module.exports = app => {
                         }
                     }
                 }
-                this.transaction.commit();
+                await this.transaction.commit();
             } catch (err) {
                 await this.transaction.rollback();
                 throw err;
@@ -772,7 +772,7 @@ module.exports = app => {
                 const operate = await this._deleteNodeData(tenderId, selectData);
                 // 选中节点--父节点 只有一个子节点时,应升级is_leaf
                 if (parentData) {
-                    const count = this.db.count(this.tableName, { ledger_pid: selectData.ledger_pid });
+                    const count = await this.db.count(this.tableName, { tender_id: tenderId, ledger_pid: selectData.ledger_pid });
                     if (count === 1) {
                         await this.transaction.update(this.tableName, { id: parentData.id, is_leaf: true });
                     }
@@ -903,7 +903,7 @@ module.exports = app => {
                 literal: 'Replace',
             });
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
-            const data = this.transaction.query(sql, sqlParam);
+            const data = await this.transaction.query(sql, sqlParam);
 
             return data;
         }
@@ -1022,7 +1022,7 @@ module.exports = app => {
                 await this._syncUplevelChildren(selectData);
                 // 选中节点--全部后兄弟节点 收编为子节点 修改pid, order, full_path
                 await this._syncUpLevelNexts(selectData);
-                this.transaction.commit();
+                await this.transaction.commit();
             } catch (err) {
                 await this.transaction.rollback();
                 throw err;
@@ -1417,13 +1417,13 @@ module.exports = app => {
                 throw '标段不存在';
             }
             if (!data) {
-                throw '提交数据错误1';
+                throw '提交数据错误';
             }
             const datas = data instanceof Array ? data : [data];
             const ids = [];
             for (const row of datas) {
                 if (tenderId !== row.tender_id) {
-                    throw '提交数据错误2';
+                    throw '提交数据错误';
                 }
                 ids.push(row.id);
             }
@@ -1434,7 +1434,7 @@ module.exports = app => {
                 for (const row of datas) {
                     const updateNode = await this.getDataById(row.id);
                     if (!updateNode || tenderId !== updateNode.tender_id || row.ledger_id !== updateNode.ledger_id) {
-                        throw '提交数据错误3';
+                        throw '提交数据错误';
                     }
                     let updateData;
                     if (this._checkCalcField(row)) {
@@ -1473,6 +1473,179 @@ module.exports = app => {
                 return result1;
             }
         }
+
+        /**
+         *
+         * @param tenderId
+         * @param xmj
+         * @param order
+         * @param parentData
+         * @returns {Promise<*[]>}
+         * @private
+         */
+        async _sortBatchInsertData(tenderId, xmj, order, parentData) {
+            const result = [], newIds = [];
+            let tp = 0;
+            const cacheKey = 'tender_node_maxId:' + tenderId;
+            let maxId = parseInt(await this.cache.get(cacheKey));
+            if (!maxId) {
+                maxId = await this._getMaxNodeId(tenderId);
+            }
+            this.cache.set(cacheKey, maxId + xmj.children.length + 1, 'EX', this.ctx.app.config.cacheTime);
+            // 添加xmj数据
+            const parent = {
+                tender_id: tenderId,
+                ledger_id: maxId + 1,
+                ledger_pid: parentData.ledger_id,
+                is_leaf: false,
+                order: order,
+                level: parentData.level + 1,
+                name: xmj.name,
+            };
+            parent.full_path = parentData.full_path + '.' + parent.ledger_id;
+            // 添加gcl数据
+            for (let i = 0, iLen = xmj.children.length; i < iLen; i++) {
+                const gcl = xmj.children[i];
+                const child = {
+                    tender_id: tenderId,
+                    ledger_id: maxId + 1 + i + 1,
+                    ledger_pid: parent.ledger_id,
+                    is_leaf: true,
+                    order: i+1,
+                    level: parent.level + 1,
+                    b_code: gcl.b_code,
+                    name: gcl.name,
+                    unit: gcl.unit,
+                    unit_price: gcl.unit_price,
+                    quantity: gcl.quantity,
+                };
+                child.full_path = parent.full_path + '.' + child.ledger_id;
+                child.total_price = child.unit_price * child.quantity;
+                tp = tp + child.total_price;
+                result.push(child);
+                newIds.push(child.ledger_id);
+            }
+            parent.total_price = tp;
+            result.push(parent);
+            newIds.push(parent.ledger_id);
+            return [result, tp, newIds];
+        }
+
+        /**
+         * 批量插入子项
+         * @param {Number} tenderId - 标段Id
+         * @param {Number} selectId - 选中节点Id
+         * @param {Object} data - 批量插入数据
+         * @returns {Promise<void>}
+         */
+        async batchInsertChild(tenderId, selectId, data) {
+            if ((tenderId <= 0) || (selectId <= 0)) {
+                return [];
+            }
+            const selectData = await this.getDataByNodeId(tenderId, selectId);
+            if (!selectData) {
+                throw '位置数据错误';
+            }
+
+            this.transaction = await this.db.beginTransaction();
+            let incre = 0, newIds = [];
+            try {
+                const lastChild = await this.getLastChildData(tenderId, selectId);
+                if (!lastChild) {
+                    await this.transaction.update(this.tableName, {
+                        id: selectData.id,
+                        is_leaf: false,
+                    });
+                }
+                const order = lastChild ? lastChild.order : 0;
+                // 数据库创建新增节点数据
+                for (let i = 0, iLen = data.length; i < iLen; i++) {
+                    const xmj = data[i];
+                    const [insertData, tp, ids] = await this._sortBatchInsertData(tenderId, xmj, order + i + 1, selectData);
+                    incre = incre + tp;
+                    newIds = newIds.concat(ids);
+                    await this.transaction.insert(this.tableName, insertData);
+                }
+
+                // 更新父节点金额
+                if (this.ctx.helper.checkZero(incre)) {
+                    const updateMap = {};
+                    updateMap[selectData.full_path] = incre;
+                    await this._increCalcParent(tenderId, updateMap);
+                }
+                await this.transaction.commit();
+            } catch(err) {
+                await this.transaction.rollback();
+                throw err;
+            }
+
+            // 查询应返回的结果
+            const createData = await this.getDataByNodeIds(selectData.tender_id, newIds);
+            if (this.ctx.helper.checkZero(incre) || selectData.is_leaf) {
+                const updateData = await this.getFullLevelDataByFullPath(selectData.tender_id, selectData.full_path);
+                return { create: createData, update: updateData };
+            } else {
+                return { create: createData };
+            }
+        }
+
+        /**
+         * 批量插入后项
+         * @param {Number} tenderId - 标段Id
+         * @param {Number} selectId - 选中节点Id
+         * @param {Object} data - 批量插入数据
+         * @returns {Promise<void>}
+         */
+        async batchInsertNext(tenderId, selectId, data) {
+            if ((tenderId <= 0) || (selectId <= 0)) {
+                return [];
+            }
+            const selectData = await this.getDataByNodeId(tenderId, selectId);
+            if (!selectData) {
+                throw '位置数据错误';
+            }
+            const parentData = await this.getDataByNodeId(tenderId, selectData.ledger_pid);
+            if (!parentData) {
+                throw '位置数据错误';
+            }
+
+            this.transaction = await this.db.beginTransaction();
+            let incre = 0, newIds = [];
+            try {
+                // 选中节点的所有后兄弟节点,order+粘贴节点个数
+                await this._updateSelectNextsOrder(selectData, data.length);
+                const order = selectData.order;
+                // 数据库创建新增节点数据
+                for (let i = 0, iLen = data.length; i < iLen; i++) {
+                    const xmj = data[i];
+                    const [insertData, tp, ids] = await this._sortBatchInsertData(tenderId, xmj, order + i + 1, parentData);
+                    incre = incre + tp;
+                    newIds = newIds.concat(ids);
+                    await this.transaction.insert(this.tableName, insertData);
+                }
+
+                // 更新父节点金额
+                if (this.ctx.helper.checkZero(incre)) {
+                    const updateMap = {};
+                    updateMap[parentData.full_path] = incre;
+                    await this._increCalcParent(tenderId, updateMap);
+                }
+                await this.transaction.commit();
+            } catch(err) {
+                await this.transaction.rollback();
+                throw err;
+            }
+
+            // 查询应返回的结果
+            const createData = await this.getDataByNodeIds(parentData.tender_id, newIds);
+            const updateData = await this.getNextsData(selectData.tender_id, selectData.ledger_pid, selectData.order + data.length);
+            if (this.ctx.helper.checkZero(incre)) {
+                const updateData1 = await this.getFullLevelDataByFullPath(tenderId, parentData.full_path);
+                return { create: createData, update: updateData.concat(updateData1) };
+            } else {
+                return { create: createData, update: updateData };
+            }
+        }
     }
 
     return Ledger;

+ 38 - 14
app/service/tender.js

@@ -101,20 +101,44 @@ module.exports = app => {
          * @return {Boolean} - 返回新增结果
          */
         async add(postData) {
-            // 获取当前用户信息
-            const sessionUser = this.ctx.session.sessionUser;
-            // 获取当前项目信息
-            const sessionProject = this.ctx.session.sessionProject;
-            const insertData = {
-                name: postData.name,
-                status: tenderConst.status.APPROVAL,
-                project_id: sessionProject.id,
-                user_id: sessionUser.accountId,
-                create_time: postData.create_time,
-                type: postData.type,
-            };
-            const operate = await this.db.insert(this.tableName, insertData);
-            return operate.insertId > 0;
+            let result = false;
+            this.transaction = await this.db.beginTransaction();
+            try {
+                // 获取当前用户信息
+                const sessionUser = this.ctx.session.sessionUser;
+                // 获取当前项目信息
+                const sessionProject = this.ctx.session.sessionProject;
+
+                const insertData = {
+                    name: postData.name,
+                    status: tenderConst.status.APPROVAL,
+                    project_id: sessionProject.id,
+                    user_id: sessionUser.accountId,
+                    create_time: postData.create_time,
+                    type: postData.type,
+                };
+                const operate = await this.transaction.insert(this.tableName, insertData);
+
+                result = operate.insertId > 0;
+                if (!result) {
+                    throw '新增标段数据失败';
+                }
+
+                // 获取标段项目节点模板
+                const tenderNodeTemplateData = await this.ctx.service.tenderNodeTemplate.getData();
+                // 复制模板数据到标段数据表
+                result = await this.ctx.service.ledger.innerAdd(tenderNodeTemplateData, operate.insertId, this.transaction);
+
+                if (!result) {
+                    throw '新增标段项目节点失败';
+                }
+                this.transaction.commit();
+            } catch (error) {
+                console.log(error);
+                result = false;
+                this.transaction.rollback();
+            }
+            return result;
         }
 
         /**

+ 1 - 1
app/view/ledger/explode_modal.ejs

@@ -54,7 +54,7 @@
             </div>
             <div class="modal-footer">
                 <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
-                <button type="button" class="btn btn-primary" >确定</button>
+                <button type="button" class="btn btn-primary" id="batch-ok">确定</button>
             </div>
         </div>
     </div>

+ 3 - 3
test/app/service/deal_bills.test.js

@@ -11,11 +11,11 @@
 const { app, assert } = require('egg-mock/bootstrap');
 const excel = require('node-xlsx');
 
-describe('test/app/service/ledger.test.js', () => {
-    it('clear history test data', function* () {
+describe('test/app/service/deal_bills.test.js', () => {
+    it('test import Excel data', function* () {
         const ctx = app.mockContext();
         const file = app.baseDir  + '/test/app/test_file/deal-load-test.xls';
-        const sheets = excel.parse(fileName), testTenderId = 3;
+        const sheets = excel.parse(file), testTenderId = 3;
 
         const result = yield ctx.service.dealBills.importData(sheets[0], testTenderId);
         assert(result.length === 1);

+ 173 - 0
test/app/service/ledger.test.js

@@ -941,6 +941,179 @@ describe('test/app/service/ledger.test.js', () => {
         assert(result3.update[0].order === 2);
         assert(result3.expand.length === 5);
     });
+    /* 期望运行结果:
+        1                                                   122.00040759
+        ├── 1-1                                             90.00009719
+        │   ├── 1-1-1                                       30.00003573
+        │   │   └── 202-2                                   30.00003573
+        │   │       ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │   │       └── 202-2-e     2.00000001  3.0000005   6.00000103
+        │   ├── 1-1-4
+        │   ├── 1-1-5
+        │   └── 202-2                                       60.00007146
+        │       ├── 202-2-c         4.00000025  6.0000083   24.0000347
+        │       ├── 202-2-e         2.00000001  3.0000005   6.00000103
+        │       └── 202-2                                   30.00003573
+        │           ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │           └── 202-2-e     2.00000001  3.0000005   6.00000103
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2                                             32.0003004
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       32.0003004
+        │   │   └── 202-2                                   32.0003004
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+            └── 1-4-2
+                └── 1-4-2-1
+                    ├── 1-4-2-1-1
+                    └── 1-4-2-1-2
+     */
+    // 批量插入
+    it('test batchInsertChild', function* () {
+        const ctx = app.mockContext();
+
+        const batchData = [
+            {
+                name: 'X1',
+                children: [
+                    {b_code: 401-1, name: 'A1', unit: 'B1', unit_price: 2, quantity: 3},
+                    {b_code: 402-1, name: 'A2', unit: 'B2', unit_price: 3, quantity: 4},
+                ]
+            }, {
+                name: 'X2',
+                children: [
+                    {b_code: 401-1, name: 'A1', unit: 'B1', unit_price: 2, quantity: 3},
+                    {b_code: 402-1, name: 'A2', unit: 'B2', unit_price: 3, quantity: 4},
+                ]
+            }
+        ]
+        // 选中1-1-3(id=14)
+        const result = yield ctx.service.ledger.batchInsertChild(testTenderId, 14, batchData);
+
+        assert(result.create.length === 6);
+
+        assert(result.update.length === 3);
+        let node = findById(result.update, 1);
+        assert(node.total_price.toFixed(8) == 158.00040759);
+        node = findById(result.update, 13);
+        assert(node.total_price.toFixed(8) == 36);
+        node = findById(result.update, 14);
+        assert(node.total_price.toFixed(8) == 36);
+    });
+    /* 期望运行结果:
+        1                                                   158.00040759
+        ├── 1-1                                             90.00009719
+        │   ├── 1-1-1                                       30.00003573
+        │   │   └── 202-2                                   30.00003573
+        │   │       ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │   │       └── 202-2-e     2.00000001  3.0000005   6.00000103
+        │   ├── 1-1-4
+        │   ├── 1-1-5
+        │   └── 202-2                                       60.00007146
+        │       ├── 202-2-c         4.00000025  6.0000083   24.0000347
+        │       ├── 202-2-e         2.00000001  3.0000005   6.00000103
+        │       └── 202-2                                   30.00003573
+        │           ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │           └── 202-2-e     2.00000001  3.0000005   6.00000103
+        ├── 1-1-2                                           36
+        │   └── 1-1-3                                       36
+        │       ├──     X1                                  18
+        │       │   ├── 401-1       2           3           6
+        │       │   └── 402-1       3           4           12
+        │       └──     X2                                  18
+        │           ├── 401-1       2           3           6
+        │           └── 402-1       3           4           12
+        ├── 1-2                                             32.0003004
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       32.0003004
+        │   │   └── 202-2                                   32.0003004
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+            └── 1-4-2
+                └── 1-4-2-1
+                    ├── 1-4-2-1-1
+                    └── 1-4-2-1-2
+     */
+    // 批量插入
+    it('test batchInsertNext', function* () {
+        const ctx = app.mockContext();
+
+        const batchData = [
+            {
+                name: 'Y1',
+                children: [
+                    {b_code: 401-1, name: 'A1', unit: 'B1', unit_price: 2, quantity: 3},
+                    {b_code: 402-1, name: 'A2', unit: 'B2', unit_price: 3, quantity: 4},
+                ]
+            }, {
+                name: 'Y2',
+                children: [
+                    {b_code: 401-1, name: 'A1', unit: 'B1', unit_price: 2, quantity: 3},
+                    {b_code: 402-1, name: 'A2', unit: 'B2', unit_price: 3, quantity: 4},
+                ]
+            }
+        ]
+        // 选中1-1-3(id=14)
+        const result = yield ctx.service.ledger.batchInsertNext(testTenderId, 14, batchData);
+
+        assert(result.create.length === 6);
+
+        assert(result.update.length === 2);
+        let node = findById(result.update, 1);
+        assert(node.total_price.toFixed(8) == 194.00040759);
+        node = findById(result.update, 13);
+        assert(node.total_price.toFixed(8) == 72);
+    });
+    /* 期望运行结果:
+        1                                                   194.00040759
+        ├── 1-1                                             90.00009719
+        │   ├── 1-1-1                                       30.00003573
+        │   │   └── 202-2                                   30.00003573
+        │   │       ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │   │       └── 202-2-e     2.00000001  3.0000005   6.00000103
+        │   ├── 1-1-4
+        │   ├── 1-1-5
+        │   └── 202-2                                       60.00007146
+        │       ├── 202-2-c         4.00000025  6.0000083   24.0000347
+        │       ├── 202-2-e         2.00000001  3.0000005   6.00000103
+        │       └── 202-2                                   30.00003573
+        │           ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │           └── 202-2-e     2.00000001  3.0000005   6.00000103
+        ├── 1-1-2                                           72
+        │   ├── 1-1-3                                       36
+        │   │   ├──     X1                                  18
+        │   │   │   ├── 401-1       2           3           6
+        │   │   │   └── 402-1       3           4           12
+        │   │   └──     X2                                  18
+        │   │       ├── 401-1       2           3           6
+        │   │       └── 402-1       3           4           12
+        │   ├──     X1                                      18
+        │   │   ├── 401-1           2           3           6
+        │   │   └── 402-1           3           4           12
+        │   └──     X2                                      18
+        │       ├── 401-1           2           3           6
+        │       └── 402-1           3           4           12
+        ├── 1-2                                             32.0003004
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       32.0003004
+        │   │   └── 202-2                                   32.0003004
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+            └── 1-4-2
+                └── 1-4-2-1
+                    ├── 1-4-2-1-1
+                    └── 1-4-2-1-2
+     */
 
     // 测试统计类方法
     it('test addUpChildren', function* () {