Просмотр исходного кода

1. 成本报审,导入合同相关
2. 成本报审,新增税率,应付实付不含税计算

MaiXinRong 4 месяцев назад
Родитель
Сommit
a713e19212

+ 17 - 0
app/controller/cost_controller.js

@@ -227,6 +227,8 @@ module.exports = app => {
                 }).filter(x => { return x.groupList.length > 0; });
                 // 是否已验证手机短信
                 const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const commonJson = ctx.subProject.common_json ? JSON.parse(ctx.subProject.common_json) : null;
+                const contractTypes = commonJson && commonJson.tender_contract_type ? commonJson.tender_contract_type : [];
                 const renderData = {
                     auditConst: audit.common,
                     auditType: audit.auditType,
@@ -236,6 +238,7 @@ module.exports = app => {
                     authMobile: pa.auth_mobile,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost[`cost_stage_${stageTypeInfo.key}`]),
                     shenpi_status: ctx.tender.info.shenpi[stageTypeInfo.shenpi_status] || 1,
+                    contractTypes,
                 };
                 await this.layout(`cost/${stageTypeInfo.key}.ejs`, renderData, `cost/${stageTypeInfo.key}_modal.ejs`);
             } catch (err) {
@@ -278,6 +281,17 @@ module.exports = app => {
                         case 'tags':
                             responseData.data[f] = await ctx.service.costStageTag.getDatas(ctx.costStage.id);
                             break;
+                        case 'contract':
+                            const contractTree = await ctx.service.contractTree.getAllDataByCondition({
+                                columns: ['id', 'contract_id', 'contract_pid', 'order', 'level', 'is_leaf', 'full_path', 'code', 'name'],
+                                where: {tid: ctx.costStage.tid, contract_type: 1}
+                            });
+                            const contractData = await this.ctx.service.contract.getAllDataByCondition({
+                                columns: ['id', 'contract_id', 'contract_pid', 'order', 'level', 'is_leaf', 'full_path', 'c_code', 'name'],
+                                where:  {tid: ctx.costStage.tid, contract_type: 1}
+                            });
+                            responseData.data[f] = [...contractTree, ...contractData];
+                            break;
                         default:
                             responseData.data[f] = [];
                             break;
@@ -350,6 +364,9 @@ module.exports = app => {
                     }
                 } else if (data.target === 'detail') {
                     responseData.data = await this.ctx.service.costStageDetail.updateDatas(data);
+                } else if (data.target === 'importContract') {
+                    if (!data.types && !data.ids) throw '数据错误';
+                    responseData.data = await this.ctx.service.costStageDetail.importContract(data);
                 }
                 ctx.body = responseData;
             } catch (err) {

+ 171 - 40
app/public/js/cost_stage_ledger.js

@@ -31,10 +31,13 @@ $(document).ready(function() {
                     {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
                     {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
                     {title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
                     {title: '付款金额', colSpan: '1', rowSpan: '1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
                     {title: '扣款金额', colSpan: '1', rowSpan: '1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
                     {title: '应付金额', colSpan: '1', rowSpan: '1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -372,10 +375,11 @@ $(document).ready(function() {
             }
 
             switch (col.field) {
+                case 'tax':
                 case 'pay_tp':
                 case 'cut_tp':
                 case 'sf_tp':
-                    info.cancel = (node.children && node.children.length > 0);
+                    info.cancel = (node.children && node.children.length > 0) || node.had_detail;
                     if (!info.cancel) {
                         const detailRange = detailObj.data.getPartData(node.id);
                         if (detailRange && detailRange.length > 0) info.cancel = true;
@@ -441,11 +445,11 @@ $(document).ready(function() {
                 if (bDel) datas.push(data);
             }
             if (datas.length > 0) {
-                postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
+                postData(window.location.pathname + '/update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
                     const refreshNode = sheet.zh_tree.loadPostData(result);
                     billsObj.refreshTree(refreshNode);
                 }, function () {
-                    SpreadJsObj.reLoadRowData(info.sheet, sel.row, sel.rowCount);
+                    SpreadJsObj.reLoadRowData(sheet, sel.row, sel.rowCount);
                 });
             }
         }
@@ -505,14 +509,17 @@ $(document).ready(function() {
             this.data = createAncillaryGcl({ id: 'id', masterId: 'ledger_id', sort: [['d_order', 'asc']] });
             this.dealSpreadSetting = {
                 cols: [
-                    {title: '合同编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@', readOnly: true},
-                    {title: '合同名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', readOnly: true},
+                    {title: '合同编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@', readOnly: true},
+                    {title: '合同名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@', readOnly: true},
                     {title: '乙方', colSpan: '1', rowSpan: '1', field: 'party_b', hAlign: 0, width: 150, formatter: '@', readOnly: true},
                     {title: '合同金额', colSpan: '1', rowSpan: '1', field: 'deal_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
-                    {title: '付款金额', colSpan: '1', rowSpan: '1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '扣款金额', colSpan: '1', rowSpan: '1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '付款金额', colSpan: '1', rowSpan: '1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '扣款金额', colSpan: '1', rowSpan: '1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '应付金额', colSpan: '1', rowSpan: '1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
-                    {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -561,12 +568,15 @@ $(document).ready(function() {
             };
             this.commonSpreadSetting = {
                 cols: [
-                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@'},
-                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@'},
+                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
                     {title: '付款金额', colSpan: '1', rowSpan: '1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
                     {title: '扣款金额', colSpan: '1', rowSpan: '1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
                     {title: '应付金额', colSpan: '1', rowSpan: '1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '应付金额(无税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    {title: '实付金额(无税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -641,7 +651,15 @@ $(document).ready(function() {
             this.loadDetailData(SpreadJsObj.getSelectObject(billsObj.sheet));
         }
         refreshSheet() {
-            SpreadJsObj.reLoadSheetData(this.sheet);
+            if (this.sheet.zh_data.length === 0) {
+                this.reloadDetailData();
+            } else {
+                SpreadJsObj.reLoadSheetData(this.sheet);
+            }
+        }
+        reloadDetailData() {
+            const data = this.billsNode ? this.data.getPartData(this.billsNode.id) || [] : [];
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, data);
         }
         loadDetailData(bills) {
             detailObj.billsNode = bills;
@@ -652,8 +670,7 @@ $(document).ready(function() {
                 spreadSetting.readOnly = readOnly;
             }
             SpreadJsObj.initSheet(this.sheet, spreadSetting);
-            const data = bills ? this.data.getPartData(bills.id) || [] : [];
-            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, data);
+            this.reloadDetailData();
         }
         loadRelaData() {
             const select = SpreadJsObj.getSelectObject(this.sheet);
@@ -661,36 +678,27 @@ $(document).ready(function() {
         }
         baseOpr(type) {
             const self = this;
-            const select = SpreadJsObj.getSelectObject(this.sheet);
-
-            const updateData = { target: 'detail', postType: type, postData: { id: select.id } };
             if (type === 'delete') {
+                const updateData = { target: 'detail', del: [] };
+                const sel = this.sheet.getSelections()[0];
+                for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                    const node = this.sheet.zh_data[iRow];
+                    if (node) updateData.del.push(node.id);
+                }
+                if (updateData.del.length === 0) return;
+                
                 deleteAfterHint(function () {
                     postData(window.location.pathname + '/update', updateData, function (result) {
-                        self.data.updateDatas(result);
+                        billsTag.afterDeletePos(result.detail.del.map(x => { return detailObj.data.getItem(x); }));
+                        costFile.deleteFileBySubNodeId(result.detail.del);
+                        self.data.updateDatas(result.detail);
                         self.refreshSheet();
-                        if (sel) {
-                            sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
-                        }
+                        if (sel) self.sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
+                        result.ledger.tree_id = detailObj.billsNode.tree_id;
+                        const refreshNode = billsObj.tree.loadPostData({ update: result.ledger });
+                        billsObj.refreshTree(refreshNode);
                     });
-                });
-            } else {
-                postData(window.location.pathname + '/update', updateData, function (result) {
-                    self.data.updateDatas(result);
-                    self.refreshSheet();
-                    if (['up-move', 'down-move'].indexOf(type) > -1) {
-                        if (sel) {
-                            sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
-                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
-                        }
-                    } else if (type === 'add') {
-                        const sel = sheet.getSelections()[0];
-                        if (sel) {
-                            sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
-                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
-                        }
-                    }
-                });
+                }, '确认删除「当前明细数据」?');
             }
         }
         // 事件
@@ -878,6 +886,7 @@ $(document).ready(function() {
         user_name: 'user_name',
         lid: 'rela_id',
         pos_id: 'rela_sub_id',
+        posMasterId: 'ledger_id',
         updateUrl: window.location.pathname + '/tag',
         afterModify: function (nodes) {
             billsObj.sheet.repaint();
@@ -907,6 +916,105 @@ $(document).ready(function() {
         items: {}
     };
     if (!readOnly) {
+        $('select[name=contract_type]').select2({
+            language: 'zh-CN',
+            width: '100%',
+            multiple: true,
+            maximumSelectionlength: 6,
+        });
+        const importDeal = (function(){
+            let curNode;
+            const contractSelectSpread = SpreadJsObj.createNewSpread($('#contract-spread')[0]);
+            const contractSelectSheet = contractSelectSpread.getActiveSheet();
+            SpreadJsObj.initSheet(contractSelectSheet, {
+                cols: [
+                    {title: '选择', colSpan: '1', rowSpan: '1', field: 'selected', hAlign: 1, width: 40, formatter: '@', cellType: 'checkbox'},
+                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 120, formatter: '@', cellType: 'tree'},
+                    {title: '合同编号', colSpan: '1', rowSpan: '1', field: 'c_code', hAlign: 0, width: 120, formatter: '@'},
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 200, formatter: '@'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: true,
+            });
+            let contractTree;
+            contractSelectSpread.bind(spreadNS.Events.ButtonClicked, function(e, info) {
+                if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+                const col = info.sheet.zh_setting.cols[info.col];
+                if (col.field !== 'selected') return;
+
+                const node = SpreadJsObj.getSelectObject(info.sheet);
+                node.selected = !node.selected;
+                if (node.children && node.children.length > 0) {
+                    const posterity = info.sheet.zh_tree.getPosterity(node);
+                    for (const p of posterity) {
+                        p.selected = node.selected;
+                    }
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row, posterity.length + 1);
+                } else {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+                }
+            });
+            $('#import-deal-type-ok').click(function() {
+                const updateData = { target: 'importContract', ledger_id: curNode.id, cost_id: curNode.cost_id, types: $('[name=contract_type]').val() };
+                if (updateData.types.length === 0) {
+                    toastr.warning('请至少选择一种类型');
+                    return;
+                }
+                postData(window.location.pathname + '/update', updateData, function(result) {
+                    detailObj.data.updateDatas(result.detail);
+                    detailObj.reloadDetailData();
+                    result.ledger.tree_id = curNode.tree_id;
+                    const refreshNode = billsObj.tree.loadPostData({ update: result.ledger });
+                    billsObj.refreshTree(refreshNode);
+                    $('#import-deal-type').modal('hide');
+                });
+            });
+            const importByType = function(node) {
+                curNode = node;
+                $('#import-deal-type').modal('show');
+            };
+            const loadContractTree = function() {
+                postData(window.location.pathname + '/load', { filter: 'contract'}, function(result) {
+                    contractTree = createNewPathTree('base', { id: 'contract_id', pid: 'contract_pid', order: 'order', level: 'level', fullPath: 'full_path', rootId: -1 });
+                    contractTree.loadDatas(result.contract);
+                    SpreadJsObj.loadSheetData(contractSelectSheet, SpreadJsObj.DataType.Tree, contractTree);
+                });
+            };
+            const getSelectContractId = function() {
+                if (!contractTree) return [];
+                return contractTree.nodes.filter(x => { return x.selected && x.c_code; }).map(x => { return x.id; });
+            };
+            $('#import-deal-select').on('shown.bs.modal', function() {
+                contractSelectSpread.refresh();
+            });
+            $('#import-deal-select-ok').click(function() {
+                const updateData = { target: 'importContract', ledger_id: curNode.id, cost_id: curNode.cost_id, ids: getSelectContractId() };
+                if (updateData.ids.length === 0) {
+                    toastr.warning('请至少选择一项合同数据');
+                    return;
+                }
+                postData(window.location.pathname + '/update', updateData, function(result) {
+                    detailObj.data.updateDatas(result.detail);
+                    detailObj.refreshSheet();
+                    result.ledger.tree_id = curNode.tree_id;
+                    const refreshNode = billsObj.tree.loadPostData({ update: result.ledger });
+                    billsObj.refreshTree(refreshNode);
+                    $('#import-deal-select').modal('hide');
+                });
+            });
+            const importBySelect = function(node) {
+                curNode = node;
+                $('#import-deal-select').modal('show');
+                 if (!contractTree) loadContractTree();
+            };
+            return {importByType, importBySelect};
+        })();
         billsContextMenuOptions.items.create = {
             name: '新增',
             icon: 'fa-sign-in',
@@ -964,12 +1072,14 @@ $(document).ready(function() {
         billsContextMenuOptions.items.importDealByTree = {
             name: '导入合同数据(自选)',
             callback: function(key, opt, menu, e) {
-
+                const node = SpreadJsObj.getSelectObject(billsObj.sheet);
+                importDeal.importBySelect(node)
             },
             disabled: function(key, opt) {
                 const node = SpreadJsObj.getSelectObject(billsObj.sheet);
                 if (!node) return true;
                 if (node.children && node.children.length > 0) return true;
+                if (node.had_detail && !node.is_deal) return true;
                 const detailRange = detailObj.data.getPartData(node.id);
                 if (!detailRange || detailRange.length === 0) return false;
                 return !detailRange[0].is_deal;
@@ -978,12 +1088,14 @@ $(document).ready(function() {
         billsContextMenuOptions.items.importDealByType = {
             name: '导入合同数据(分类)',
             callback: function(key, opt, menu, e) {
-
+                const node = SpreadJsObj.getSelectObject(billsObj.sheet);
+                importDeal.importByType(node)
             },
             disabled: function(key, opt) {
                 const node = SpreadJsObj.getSelectObject(billsObj.sheet);
                 if (!node) return true;
                 if (node.children && node.children.length > 0) return true;
+                if (node.had_detail && !node.is_deal) return true;
                 const detailRange = detailObj.data.getPartData(node.id);
                 if (!detailRange || detailRange.length === 0) return false;
                 return !detailRange[0].is_deal;
@@ -1005,7 +1117,26 @@ $(document).ready(function() {
     $.contextMenu(billsContextMenuOptions);
     $.contextMenu({
         selector: '#detail-spread',
+        build: function ($trigger, e) {
+            const target = SpreadJsObj.safeRightClickSelection($trigger, e, detailObj.spread);
+            detailObj.loadRelaData();
+            return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+        },
         items: {
+            del: {
+                name: '删除',
+                icon: 'fa-remove',
+                callback: function (key, opt) {
+                    detailObj.baseOpr('delete');
+                },
+                visible: function() {
+                    return !readOnly;
+                },
+                disabled: function() {
+                    const detail = SpreadJsObj.getSelectObject(detailObj.sheet);
+                    return !detail;
+                }
+            },
             tag: {
                 name: '书签',
                 callback: function (key, opt, menu, e) {

+ 2 - 1
app/public/js/shares/cs_tools.js

@@ -1133,6 +1133,7 @@ const showSelectTab = function(select, spread, afterShow) {
         if (!setting.user_name) setting.user_name = 'u_name';
         if (!setting.lid) setting.lid = 'lid';
         if (!setting.pos_id) setting.pos_id = 'pos_id';
+        if (!setting.posMasterId) setting.posMasterId = 'lid';
         const obj = $(setting.selector);
         const relaTree = setting.relaTree;
         const html = [], pageLength = 15;
@@ -1423,7 +1424,7 @@ const showSelectTab = function(select, spread, afterShow) {
         };
         const afterDeletePos = function (pos) {
             for (const p of pos) {
-                const bi = billsIndexes[p[setting.lid]];
+                const bi = billsIndexes[p[setting.posMasterId]];
                 if (!bi) continue;
 
                 const pi = bi.filter(x => { return x[setting.pos_id] === p.id; });

+ 23 - 1
app/public/js/shares/tools_att.js

@@ -590,7 +590,29 @@
             }
             refreshCurAttHtml();
         };
+        const deleteFileBySubNodeId = function(subNodeIds) {
+            for (const id of subNodeIds) {
+                const subNodeIndex = subNodeIndexes[id];
+                if (!subNodeIndex || subNodeIndex.length === 0) continue;
+                let nodeIndex;
+                for (const att of subNodeIndex) {
+                    const att_index = findFileIndex(allAtts, att.id);
+                    allAtts.splice(att_index, 1);
+
+                    if (!nodeIndex) nodeIndex = nodeIndexes[att.node[setting.masterKey]];
+                    const att_node_index = findFileIndex(nodeIndex, att.id);
+                    nodeIndex.splice(att_node_index, 1);
+                }
+                delete nodeIndexes[id];
+            }
+            if (allAtts.length === 1) {
+                getAllAttHtml();
+            } else {
+                refreshAllAttHtml();
+            }
+            refreshCurAttHtml();
+        };
 
-        return { loadDatas, getCurAttHtml, deleteFileByNodeId }
+        return { loadDatas, getCurAttHtml, deleteFileByNodeId, deleteFileBySubNodeId }
     };
 })(jQuery);

+ 79 - 8
app/service/cost_stage_detail.js

@@ -10,15 +10,16 @@
 
 const costFields = {
     textFields: ['code', 'name', 'party_b', 'postil', 'memo'],
-    preFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp'],
-    curFields: ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp'],
-    readFields: ['read_pay_tp', 'read_cut_tp', 'read_yf_tp', 'read_sf_tp'],
+    preFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp', 'pre_yf_excl_tax_tp', 'pre_sf_excl_tax_tp'],
+    curFields: ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp'],
+    readFields: ['read_pay_tp', 'read_cut_tp', 'read_yf_tp', 'read_sf_tp', 'read_yf_excl_tax_tp', 'read_sf_excl_tax_tp'],
+    taxFields: ['tax'],
     baseFields: ['id', 'tender_id', 'stage_id', 'ledger_id', 'cost_id', 'd_order', 'is_deal', 'is_used'],
 };
 costFields.calcFields = [...costFields.curFields];
-costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields];
-costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields];
-costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, 'calc_his'];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, ...costFields.taxFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields, ...costFields.taxFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, ...costFields.taxFields, 'calc_his'];
 
 module.exports = app => {
     class CostStageDetail extends app.BaseService {
@@ -26,6 +27,7 @@ module.exports = app => {
         constructor(ctx) {
             super(ctx);
             this.tableName = 'cost_stage_detail';
+            this.taxDecimal = 2;
         }
 
         async getEditData(stage) {
@@ -104,6 +106,13 @@ module.exports = app => {
                     nd.yf_tp = this.ctx.helper.sub(nd.pay_tp, nd.cut_tp);
                     nd.sf_tp = nd.yf_tp;
                 }
+                if (d.sf_tp !== undefined) nd.sf_tp = this.ctx.helper.round(d.sf_tp || 0, this.ctx.costStage.decimal.tp);
+                if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
+                    nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.taxDecimal) : 0;
+                    const divNum = this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100));
+                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
+                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
+                }
                 insertData.push(nd);
             }
             const billsUpdate = await this._getLedgerUpdateData(insertData, insertData[0].ledger_id);
@@ -147,6 +156,8 @@ module.exports = app => {
             try {
                 await conn.delete(this.tableName, { id: data });
                 if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                await conn.delete(this.ctx.service.costStageTag.tableName, { rela_sub_id: data });
+                await conn.delete(this.ctx.service.costStageFile.tableName, { rela_sub_id: data });
                 await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
                 await conn.commit();
                 return [data, updateData, billsUpdate];
@@ -178,9 +189,20 @@ module.exports = app => {
                     nd.pay_tp = d.pay_tp !== undefined ? this.ctx.helper.round(d.pay_tp || 0, this.ctx.costStage.decimal.tp) : od.pay_tp;
                     nd.cut_tp = d.cut_tp !== undefined ? this.ctx.helper.round(d.cut_tp || 0, this.ctx.costStage.decimal.tp) : od.cut_tp;
                     nd.yf_tp = this.ctx.helper.sub(nd.pay_tp, nd.cut_tp);
-                    if (od.yf_tp === od.sf_tp) nd.sf_tp = nd.yf_tp;
+                    nd.sf_tp = od.yf_tp === od.sf_tp ? nd.yf_tp : od.sf_tp;
+                } else {
+                    nd.pay_tp = od.pay_tp;
+                    nd.cut_tp = od.cut_tp;
+                    nd.yf_tp = od.yf_tp;
+                    nd.sf_tp = od.sf_tp;
+                }
+                if (d.sf_tp !== undefined) nd.sf_tp = this.ctx.helper.round(d.sf_tp || 0, this.ctx.costStage.decimal.tp);
+                if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
+                    nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.taxDecimal) : od.tax;
+                    const divNum = this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100));
+                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : od.yf_tp, divNum, this.ctx.costStage.decimal.tp);
+                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : od.sf_tp, divNum, this.ctx.costStage.decimal.tp);
                 }
-                if (d.yf_tp !== undefined) nd.yf_tp = this.ctx.helper.round(d.yf_tp || 0, decimal.tp);
                 uDatas.push(nd);
             }
             if (uDatas.length > 0) {
@@ -222,6 +244,55 @@ module.exports = app => {
             }
         }
 
+        async _importContractByIds(ledgerId, costId, ids) {
+            if (!ledgerId || !costId) throw '导入明细数据,提交的数据错误';
+            const user_id = this.ctx.session.sessionUser.accountId;
+            const insertDetails = [];
+            const paySql = `SELECT cp.*, c.c_code, c.name, c.party_b FROM ${this.ctx.service.contractPay.tableName} cp LEFT JOIN ${this.ctx.service.contract.tableName} c ON cp.cid = c.id WHERE cp.cid IN(${ids.map(x => { return `'${x}'`}).join(', ')}) AND Date_FORMAT(cp.pay_time, '%Y-%m') = ?`;
+            const validPays = await this.db.query(paySql, [this.ctx.costStage.stage_date]);
+            const max = await this.db.queryOne(`SELECT Max(d_order) as max_order FROM ${this.tableName} WHERE stage_id = ?`, [this.ctx.costStage.id]);
+            let maxDetailOrder = max.max_order;
+            for (const pay of validPays) {
+                const idetail = {
+                    id: this.uuid.v4(), tender_id: this.ctx.costStage.tid, stage_id: this.ctx.costStage.id, is_deal: 1,
+                    ledger_id: ledgerId, cost_id: costId, d_order: maxDetailOrder++,
+                    add_user_id: user_id, update_user_id: user_id,
+                    code: pay.c_code, name: pay.name, party_b: pay.party_b,
+                    pay_tp: pay.pay_price || 0, cut_tp: pay.debit_price || 0, yf_tp: pay.yf_price || 0, sf_tp: pay.sf_price || 0,
+                    tax: pay.tax || 0,
+                };
+                const divNum = this.ctx.helper.add(1, this.ctx.helper.div(idetail.tax, 100));
+                idetail.yf_excl_tax_tp = this.ctx.helper.div(idetail.yf_tp, divNum, this.ctx.costStage.decimal.tp);
+                idetail.sf_excl_tax_tp = this.ctx.helper.div(idetail.sf_tp, divNum, this.ctx.costStage.decimal.tp);
+                insertDetails.push(idetail);
+            }
+            const billsUpdate = await this._getLedgerUpdateData(insertDetails, ledgerId);
+            billsUpdate.is_deal = 1;
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.insert(this.tableName, insertDetails);
+                await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
+            const addData = await this.getAllDataByCondition({
+                where: { id: this.ctx.helper._.map(insertDetails, 'id') }
+            });
+            const ledgerData = await this.ctx.service.costStageLedger.getDataById(addData[0].ledger_id);
+            return { detail: { add: addData}, ledger: ledgerData };
+        }
+        async _importContractByTypes(ledgerId, costId, types) {
+            const contracts = await this.ctx.service.contract.getAllDataByCondition({ where: { tid: this.ctx.costStage.tid, contract_type: 1, type: types } });
+            await this._importContractByIds(ledgerId, costId, contracts.map(x => { return x.id; }));
+
+        }
+        async importContract(data) {
+            if (data.types) return this._importContractByTypes(data.ledger_id, data.cost_id, data.types);
+            if (data.ids) return this._importContractByIds(data.ledger_id, data.cost_id, data.ids);
+        }
+
         async deletePartData(transaction, tender_id, ledger_id) {
             await transaction.delete(this.tableName, { tid, ledger_id });
         }

+ 20 - 7
app/service/cost_stage_ledger.js

@@ -10,16 +10,17 @@
 const billsUtils = require('../lib/bills_utils');
 const costFields = {
     textFields: ['code', 'name', 'unit', 'postil', 'memo'],
-    preFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp'],
-    curFields: ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp'],
-    readFields: ['read_pay_tp', 'read_cut_tp', 'read_yf_tp', 'read_sf_tp'],
+    preFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp', 'pre_yf_excl_tax_tp', 'pre_sf_excl_tax_tp'],
+    curFields: ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp'],
+    readFields: ['read_pay_tp', 'read_cut_tp', 'read_yf_tp', 'read_sf_tp', 'read_yf_excl_tax_tp', 'read_sf_excl_tax_tp'],
+    taxFields: ['tax'],
     treeFields: ['tree_id', 'tree_pid', 'tree_level', 'tree_order', 'tree_full_path', 'tree_is_leaf'],
-    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'is_deal', 'is_used'],
+    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'is_deal', 'is_used', 'had_detail'],
 };
 costFields.calcFields = [...costFields.curFields];
-costFields.editQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields];
-costFields.readQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields];
-costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, 'calc_his'];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, ...costFields.taxFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields, ...costFields.taxFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, ...costFields.taxFields, 'calc_his'];
 
 const auditConst = require('../const/audit').costStage;
 
@@ -48,6 +49,7 @@ module.exports = app => {
             // this.depart = ctx.app.config.table_depart.light;
             this.tableName = 'cost_stage_ledger';
             this.decimal = { tp: 6 };
+            this.taxDecimal = 2;
         }
 
         // 继承方法
@@ -131,10 +133,14 @@ module.exports = app => {
                 where: { stage_id: preStage.id },
                 columns: costFields.editQueryFields
             });
+            const preDetail = await this.ctx.service.costStageDetail.getReadData(preStage);
             const insertData = [];
             for (const bills of preBills) {
                 bills.id = this.uuid.v4();
                 bills.stage_id = stage.id;
+                if (!bills.had_detail) {
+                    bills.had_detail = preDetail.findIndex(x => { return x.cost_id === bills.cost_id; }) >= 0;
+                }
                 delete bills.postil;
                 for(const prop of costFields.curFields) {
                     bills['pre_' + prop] = this.ctx.helper.add(bills['pre_' + prop], bills[prop]);
@@ -435,8 +441,15 @@ module.exports = app => {
                     nData.pay_tp = row.pay_tp !== undefined ? helper.round(row.pay_tp || 0, decimal.tp) : oData.pay_tp || 0;
                     nData.cut_tp = row.cut_tp !== undefined ? helper.round(row.cut_tp || 0, decimal.tp) : oData.cut_tp || 0;
                     nData.yf_tp = helper.sub(nData.pay_tp, nData.cut_tp);
+                    if (od.yf_tp === od.sf_tp) nData.sf_tp = nData.yf_tp;
                 }
                 if (row.sf_tp !== undefined) nData.sf_tp = helper.round(row.sf_tp || 0, decimal.tp);
+                if (row.tax !== undefined || nData.sf_tp !== undefined || nData.yf_tp !== undefined) {
+                    nData.tax = row.tax !== undefined ? helper.round(row.tax || 0, this.taxDecimal) : oData.tax;
+                    const divNum = helper.add(1, helper.div(nData.tax, 100));
+                    nData.yf_excl_tax_tp = helper.div(nData.yf_tp !== undefined ? nData.yf_tp : oData.yf_tp, divNum, decimal.tp);
+                    nData.sf_excl_tax_tp = helper.div(nData.sf_tp !== undefined ? nData.sf_tp : oData.sf_tp, divNum, decimal.tp);
+                }
                 for (const field of costFields.textFields) {
                     if (row[field] !== undefined) nData[field] = row[field] || '';
                 }

+ 43 - 1
app/view/cost/ledger_modal.ejs

@@ -1,4 +1,46 @@
 <% include ../shares/delete_hint_modal.ejs %>
 <% include ./audit_modal.ejs %>
 <% include ../shares/upload_att.ejs %>
-<% include ../shares/new_tag_modal.ejs %>
+<% include ../shares/new_tag_modal.ejs %>
+<div class="modal" id="import-deal-type" data-backdrop="static" aria-modal="true" role="dialog">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入合同数据(类型)</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group form-group-sm">
+                    <label>选择类型</label>
+                    <select class="form-control form-control-sm" name="contract_type">
+                        <% for (const ct of contractTypes) { %>
+                        <option value="<%- ct %>"><%- ct %></option>
+                        <% } %>
+                    </select>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="import-deal-type-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="import-deal-select" data-backdrop="static">
+    <div class="modal-dialog" role="document" style="max-width: 600px">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入合同数据(自选)</h5>
+            </div>
+            <div class="modal-body">
+                <div class="modal-height-400" id="contract-spread"></div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-primary" id="import-deal-select-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<link href="/public/css/bootstrap/select2.min.css" rel="stylesheet" />
+<script src="/public/js/bootstrap/select2.min.js"></script>

+ 15 - 0
sql/update.sql

@@ -128,10 +128,15 @@ CREATE TABLE `zh_cost_stage_ledger` (
   `pre_cut_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-扣款',
   `pre_yf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-应付',
   `pre_sf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-实付',
+  `pre_yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-应付不含税',
+  `pre_sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-实付不含税',
+  `tax` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '税率(%)',
   `pay_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '付款',
   `cut_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '扣款',
   `yf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '应付',
   `sf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '实付',
+  `yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '应付不含税',
+  `sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付不含税',
   `postil` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
   `memo` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
   `add_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人',
@@ -142,9 +147,12 @@ CREATE TABLE `zh_cost_stage_ledger` (
   `read_cut_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期扣款-只读',
   `read_yf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期应付-只读',
   `read_sf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期实付-只读',
+  `read_yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期应付不含税-只读',
+  `read_sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期实付不含税-只读',
   `calc_his` json DEFAULT NULL COMMENT '本期历史',
   `is_used` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否已使用',
   `is_deal` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否为合同节点',
+  `had_detail` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '往期是否有明细数据',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
 
@@ -162,10 +170,15 @@ CREATE TABLE `zh_cost_stage_detail`  (
   `pre_cut_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-扣款',
   `pre_yf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-应付',
   `pre_sf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-实付',
+  `pre_yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-应付不含税',
+  `pre_sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-实付不含税',
+  `tax` decimal(24, 8) NOT NULL DEFAULT 0 COMMENT '税率(%)',
   `pay_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '付款',
   `cut_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '扣款',
   `yf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '应付',
   `sf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付',
+  `yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '应付不含税',
+  `sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付不含税',
   `postil` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
   `memo` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
   `add_user_id` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
@@ -176,6 +189,8 @@ CREATE TABLE `zh_cost_stage_detail`  (
   `read_cut_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期扣款-只读',
   `read_yf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期应付-只读',
   `read_sf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期实付-只读',
+  `read_yf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期应付不含税-只读',
+  `read_sf_excl_tax_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期实付不含税-只读',
   `calc_his` json NULL COMMENT '本期历史',
   `is_used` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否已使用',
   `is_deal` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否为合同节点',