人财事物信息化 - financial_statements.js

甲、关键功能的详细解读

该网页内容为 ERPNext 财务报表功能相关的 JavaScript 代码文件(financial_statements.js),主要实现了财务报表的格式化展示、过滤条件配置、总账跳转及多报表导航等功能。以下是关键功能的详细解读:

一、核心对象与功能模块

erpnext.financial_statements 对象

该对象是财务报表功能的核心,包含以下属性和方法:

  1. 数据与格式处理

    • filters:存储报表过滤条件(通过 get_filters() 函数动态生成)。
    • baseData:存放报表基础数据。
    • formatter:自定义数据格式化函数,根据不同视图(增长视图、利润率视图)渲染数值,并添加颜色标识(绿色成功、红色危险)。
      • 增长视图(Growth):处理第 3 列及之后的数值,显示百分比增减(如 +10%),负值标红,正值标绿。
      • 利润率视图(Margin):处理第 2 列及之后的数值,直接显示百分比,颜色逻辑同上。
      • 基础格式化:对首列(账户名称)添加加粗样式,负值警告时标红,并为可点击的账户链接绑定 open_general_ledger 事件。
  2. 交互功能

    • open_general_ledger:点击账户名称时跳转至总账(General Ledger)或应付/应收报表,传递公司、日期、项目等过滤参数。
    • tree 与层级配置:启用树状结构展示账户层级,initial_depth: 3 表示初始展开三级账户。
  3. 报表加载事件(onload

    • 初始化过滤条件,若未设置日期范围则自动填充当前财年的起止日期。
    • 在报表页面添加自定义按钮组,提供跳转至资产负债表(Balance Sheet)、损益表(Profit and Loss)、现金流量表(Cash Flow Statement)的快捷入口。

二、过滤条件配置(get_filters 函数)

该函数返回报表的过滤条件数组,包含以下核心字段:

字段名 类型 说明
company 链接字段 必选,选择公司(默认取用户默认公司)。
finance_book 链接字段 财务账簿(可选)。
filter_based_on 选择框 过滤依据(财年或日期范围),切换时动态显示/隐藏相关日期或财年字段。
period_start_date 日期字段 起始日期(仅当过滤依据为“日期范围”时显示)。
period_end_date 日期字段 结束日期(仅当过滤依据为“日期范围”时显示)。
from_fiscal_year 财年链接 开始财年(仅当过滤依据为“财年”时显示,默认当前财年)。
to_fiscal_year 财年链接 结束财年(仅当过滤依据为“财年”时显示,默认当前财年)。
periodicity 选择框 报表周期(月、季、半年、年,默认“年”)。
presentation_currency 选择框 展示货币(从 ERPNext 获取货币列表)。
cost_center 多选列表 成本中心(动态加载,基于当前公司)。
project 多选列表 项目(动态加载,基于当前公司)。

动态默认值:自动获取当前财年(erpnext.utils.get_fiscal_year),并设置为 from_fiscal_yearto_fiscal_year 的默认值。

三、关键逻辑与技术细节

  1. 视图切换逻辑

    • 通过 frappe.query_report.get_filter_value("selected_view") 判断当前视图(Growth 或 Margin),动态调整数值显示格式。
    • 使用 jQuery 动态生成 HTML 元素(如 <span><p>)并添加样式类(text-danger/text-success),实现数据可视化。
  2. 树状结构与层级管理

    • name_field: "account"parent_field: "parent_account" 定义账户层级关系,initial_depth: 3 控制初始展开深度。
    • 根节点(无父账户或父节)的名称加粗显示,增强层级区分度。
  3. 多报表导航

    • 在报表加载时(onload)添加顶部菜单按钮,点击后通过 frappe.set_route 跳转至其他财务报表,并传递当前公司参数。

四、代码用途与场景

该文件属于 ERPNext(开源 ERP 系统)的财务模块,主要用于: - 生成动态财务报表(如资产负债表、损益表),支持多维度过滤(公司、财年、项目等)。 - 以树状结构展示账户层级,方便用户钻取查看明细数据(如点击账户跳转至总账)。 - 提供不同分析视角(增长、利润率),通过颜色编码快速识别数据趋势(正负值)。

五、总结

该代码实现了 ERPNext 财务报表的核心交互逻辑,通过灵活的过滤条件、可视化格式和多报表联动,帮助用户高效分析财务数据。关键亮点包括动态视图切换、树状层级展示、快捷导航功能,以及与总账系统的无缝集成。开发者可通过修改 formatterfilters 扩展报表展示逻辑或过滤条件。我来解读一下这个财务报表相关的 JavaScript 文件。这个文件是 ERPNext 系统中用于生成和处理财务报表的核心前端代码,主要功能包括:

  1. 报表初始化与配置
frappe.query_reports["Profit and Loss Statement"] = {
    "filters": [
        {
            "fieldname":"company",
            "label": __("Company"),
            "fieldtype": "Link",
            "options": "Company",
            "default": frappe.defaults.get_user_default("Company")
        },
        // 其他筛选器...
    ],
    // 报表配置...
}

这段代码定义了"损益表"报表的基本结构,包括筛选器(公司、财政年度、报告期间等)和基本配置选项。

  1. 数据处理与计算
calculate_net_profit = function(data) {
    var total_row = data[data.length-1];
    var net_profit = 0.0;

    if(total_row && total_row.account == __("Total Expenses")) {
        net_profit = -flt(total_row[report_settings.colname]);
    }

    // 计算净利润...

    return net_profit;
}

这个函数用于计算净利润,通过处理报表数据中的收入和支出项得出最终结果。

  1. 报表渲染与可视化
render_chart: function(data) {
    if(!data || !data.length) return;

    var values = [];
    // 准备图表数据...

    frappe.chart.render({
        parent: $(wrapper).find(".chart-container")[0],
        title: __("Profit and Loss Statement"),
        data: {
            labels: labels,
            datasets: [{
                name: __("Profit / Loss"),
                values: values
            }]
        },
        type: 'bar',
        height: 250
    });
}

这段代码负责渲染损益表的图表视图,使用 Frappe Charts 库生成直观的财务数据可视化效果。

  1. 数据格式化与显示
format_value: function(value, row, column, data) {
    if(!value) return "";

    var formatted_value = format_currency(value, null, 
        row.currency || frappe.defaults.get_default("currency"));

    if(value < 0) {
        return "<span style='color:red;'>" + formatted_value + "</span>";
    }

    return formatted_value;
}

这个函数用于格式化财务数值,包括货币符号显示和负值颜色标记,提升数据可读性。

  1. 特殊报表处理
if(report_name === "Balance Sheet") {
    // 资产负债表特殊处理逻辑...
} else if(report_name === "Cash Flow Statement") {
    // 现金流量表特殊处理逻辑...
}

代码中包含针对不同类型财务报表的特殊处理逻辑,确保每种报表都能正确显示和计算。

整个文件通过模块化的设计,将财务报表的各个方面(数据获取、处理、计算、展示)解耦,便于维护和扩展。代码遵循 ERPNext 的框架规范,利用了其提供的报表引擎和 UI 组件,同时也针对财务领域的专业性做了很多特殊处理。


乙、如何增加中国报表格式

以下是在 ERPNext 中基于现有财务报表功能(参考 financial_statements.js 代码逻辑)增加中国报表格式(如资产负债表、利润表、现金流量表的中国标准格式)的具体思路和实现建议:

一、中国财务报表格式的核心差异

中国企业会计准则(CAS)要求的报表格式与国际通用格式(如 IFRS)的主要区别包括:

  1. 报表名称与结构
    • 资产负债表:需区分“流动资产/非流动资产”“流动负债/非流动负债”,按“资产=负债+所有者权益”结构列示。
    • 利润表:需包含“营业收入”“营业利润”“利润总额”“净利润”等层级,区分“主营业务收入”与“其他业务收入”。
    • 现金流量表:需按“经营活动/投资活动/筹资活动”分类,且“现金及现金等价物净增加额”需与资产负债表勾稽。
  2. 科目名称与分类:需使用中文科目名称(如“应付职工薪酬”“应交税费”),并符合《企业会计准则——应用指南》的科目体系。
  3. 数据展示格式:数值需按人民币(CNY)展示,支持千位分隔符,负数用红色括号或负号表示。
  4. 报表附注:需添加附注说明重要会计政策、关联方交易等(可能需扩展报表功能)。

二、代码层修改建议(基于 financial_statements.js

1. 新增中国报表类型配置

erpnext.financial_statements.onload 或报表路由中添加中国标准报表的跳转链接。例如:

// 在现有菜单中新增中国报表选项
report.page.add_custom_menu_item(views_menu, __("中国资产负债表"), function () {
    var filters = report.get_values();
    frappe.set_route("query-report", "China Balance Sheet", { 
        company: filters.company, 
        report_type: "china" // 标识中国格式
    });
});

2. 自定义中国报表的字段映射与格式

修改 formatter 函数,根据报表类型(如通过 report_type 参数)调整显示逻辑:

formatter: function (value, row, column, data, default_formatter, filter) {
    // 判断是否为中国报表格式
    const isChinaReport = frappe.query_report.get_filter_value("report_type") === "china";

    if (isChinaReport) {
        // 中国格式特殊处理:数值千位分隔、人民币符号、负数用括号
        if (column.fieldname === "amount") {
            value = format_cny(value); // 自定义人民币格式化函数
        }
        // 科目名称中文显示(需确保数据中包含中文科目名)
        if (data.account_name_cn) { // 假设数据中新增中文科目字段
            value = data.account_name_cn;
        }
    }

    // 原有逻辑...
}

// 新增人民币格式化函数
function format_cny(amount) {
    if (amount === undefined || amount === null) return "NA";
    const formatted = amount.toLocaleString('zh-CN', { 
        style: 'currency', 
        currency: 'CNY', 
        signDisplay: 'minusSign' // 负数显示为“-CNY 1,000”或括号(需调整)
    });
    return formatted.replace(/CNY/g, "人民币"); // 替换为中文“人民币”
}

3. 调整过滤条件与科目映射

  • 新增科目体系选择:在过滤条件中添加“科目体系”字段,允许选择“中国会计准则”或“国际准则”,并根据选择加载对应的科目数据。
  • 修改 get_filters 函数
    {
        fieldname: "chart_of_accounts",
        label: __("科目体系"),
        fieldtype: "Select",
        options: ["中国会计准则", "国际财务报告准则"],
        default: "中国会计准则"
    }
    
  • 数据层适配:在后端 API 中,根据科目体系返回对应的科目名称和分类(如“流动资产”“流动负债”)。

三、中国报表结构的具体实现

1. 资产负债表(China Balance Sheet)

  • 列结构:分为“资产”“负债”“所有者权益”三大栏,每栏下按流动性排序。
  • 代码调整
    // 在报表数据处理中,按中国标准分组
    if (report_name === "China Balance Sheet") {
        data.forEach(row => {
            if (row.category === "current_assets") {
                row.section = "流动资产";
            } else if (row.category === "non_current_assets") {
                row.section = "非流动资产";
            }
            // 类似处理负债和所有者权益
        });
    }
    

2. 利润表(China Profit and Loss Statement)

  • 层级调整:需显示“营业收入”“营业成本”“税金及附加”“销售费用”等具体项目,并计算“营业利润”“利润总额”“净利润”。
  • 代码调整:在数据返回时添加计算字段:
    // 假设后端返回计算后的层级数据
    data.push({
        account_name: "营业利润",
        amount: data.filter(row => row.category === "operating_income").reduce((a, b) => a + b.amount, 0) - 
                 data.filter(row => row.category === "operating_expense").reduce((a, b) => a + b.amount, 0)
    });
    

3. 现金流量表(China Cash Flow Statement)

  • 分类调整:按“经营活动、投资活动、筹资活动”分类,每类下列示具体项目(如“销售商品收到的现金”“购建固定资产支付的现金”)。
  • 代码调整:在 formatter 中为现金流量项目添加图标或前缀:
    if (isChinaReport && column.fieldname === "cash_flow_category") {
        value = `<i class="fa fa-cash"></i> ${value}`; // 添加现金图标
    }
    

四、其他扩展功能

  1. 人民币符号与单位:在报表表头添加“单位:人民币元”,并统一数值格式为“¥X,XXX.XX”。
  2. 季度/年度报表适配:在 periodicity 过滤条件中,确保季度报表符合中国财税申报要求(如按自然季度)。
  3. 报表附注:在报表底部添加 <div class="china-report-note"> 区域,显示重要项目说明(需后端提供附注数据)。

五、测试与验证

  1. 科目映射测试:确保“中国会计准则”下的科目(如“库存现金”“银行存款”)正确关联到系统科目。
  2. 勾稽关系验证:检查资产负债表与利润表的“未分配利润”勾稽关系,现金流量表与资产负债表的“现金”项目勾稽关系。
  3. 本地化测试:验证中文报表名称、单位、数值格式是否符合中国用户习惯。

通过以上修改,可在 ERPNext 中新增符合中国会计准则的财务报表格式。核心逻辑是通过新增报表类型参数(如 report_type: "china"),在前端渲染和数据处理层实现差异化展示,同时结合后端科目体系适配,确保数据结构符合中国标准。


丙、前端自定义映射配置

以下是在 ERPNext 中实现报表项目与会计科目对应关系的前端自定义映射配置,并支持多会计制度(企业会计准则、小企业会计准则等)的方案设计。该方案基于 financial_statements.js 的现有逻辑,通过新增映射表维护功能和多制度切换能力,实现灵活的报表配置:

一、整体架构设计

1. 核心概念

  • 映射表(Mapping Table):定义报表项目(如“货币资金”“应收账款”)与会计科目(系统科目)的对应关系,支持多对一、一对多映射(如“货币资金”=“库存现金”+“银行存款”+“其他货币资金”)。
  • 会计制度模板:预置不同会计制度(企业会计准则、小企业会计准则等)的默认映射规则,用户可基于模板自定义调整。

2. 数据流向

用户维护映射表(前端) → 保存至数据库 → 报表生成时根据映射规则聚合科目数据 → 按所选会计制度渲染报表

二、前端实现步骤(基于 financial_statements.js

1. 新增映射表维护界面

在财务报表模块中添加“报表项目映射”菜单,允许用户通过可视化界面配置映射关系。核心功能包括:

// 在 erpnext.financial_statements 对象中添加映射表管理方法
erpnext.financial_statements = {
    // ... 原有代码 ...

    // 打开映射表维护界面
    open_mapping_editor: function () {
        frappe.ui.form.on("Report Mapping", {
            refresh: function (frm) {
                // 添加会计制度切换按钮
                frm.add_custom_button(__("切换会计制度"), function () {
                    frappe.prompt({
                        title: __("选择会计制度"),
                        fieldname: "accounting_system",
                        fieldtype: "Select",
                        options: ["企业会计准则", "小企业会计准则", "民间非营利组织会计制度"],
                    }, function (value) {
                        frm.reload_doc({ accounting_system: value }); // 加载对应制度的默认映射
                    });
                });
            }
        });

        frappe.new_doc("Report Mapping", {
            title: __("报表项目映射配置"),
            accounting_system: frappe.defaults.get_user_default("accounting_system") || "企业会计准则"
        });
    }
};

2. 映射表数据结构设计

在数据库中创建 Report Mapping 文档类型,字段包括:

字段名 类型 说明
accounting_system 选择框 会计制度(企业会计准则/小企业会计准则等)
report_item 数据项 报表项目名称(如“货币资金”“应付账款”)
mapping_type 选择框 映射类型(汇总/单独科目),汇总指多个科目求和,单独指单一科目
account_codes 多选列表 关联的会计科目(支持多选,按映射类型聚合)
formula 文本框 自定义计算公式(可选,优先级高于科目汇总,如 [A01] + [B02] - [C03]
is_default 复选框 是否为该会计制度的默认映射(用于初始化模板)

3. 前端映射配置界面

使用 Frappe 的表单引擎实现可视化配置:

<!-- 简化的 HTML 结构 -->
<div class="report-mapping-editor">
    <div class="form-group">
        <label>会计制度:</label>
        <select ng-model="currentSystem" ng-options="sys for sys in accountingSystems"></select>
    </div>
    <table class="table">
        <thead>
            <tr>
                <th>报表项目</th>
                <th>映射类型</th>
                <th>关联科目</th>
                <th>计算公式</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="item in mappingItems">
                <td><input type="text" ng-model="item.report_item"></td>
                <td>
                    <select ng-model="item.mapping_type">
                        <option value="sum">汇总科目</option>
                        <option value="single">单一科目</option>
                    </select>
                </td>
                <td>
                    <erpnext-multi-select 
                        fieldname="account_codes" 
                        options="accounts" 
                        ng-model="item.account_codes">
                    </erpnext-multi-select>
                </td>
                <td><input type="text" ng-model="item.formula"></td>
            </tr>
        </tbody>
    </table>
</div>

三、报表生成时的映射逻辑

修改 financial_statements.js 中的数据处理逻辑,根据映射表动态聚合科目数据:

1. 获取映射规则

// 在报表加载时获取当前会计制度的映射规则
onload: function (report) {
    // ... 原有代码 ...

    // 获取用户配置的映射表(优先使用自定义,否则加载默认模板)
    frappe.call({
        method: "erpnext.financial_statements.get_mapping_rules",
        args: {
            accounting_system: frappe.query_report.get_filter_value("accounting_system") || "企业会计准则"
        },
        callback: function (r) {
            erpnext.financial_statements.mappingRules = r.message;
            report.refresh(); // 触发报表数据重新计算
        }
    });
}

2. 按映射规则聚合数据

// 修改数据处理函数,基于映射规则生成报表项目
process_data: function (rawData) {
    const mapping = erpnext.financial_statements.mappingRules;
    const reportData = [];

    // 遍历所有报表项目
    mapping.forEach(item => {
        let total = 0;
        if (item.mapping_type === "sum") {
            // 汇总多个科目的余额
            item.account_codes.forEach(code => {
                const accountData = rawData.find(row => row.account_code === code);
                if (accountData) total += flt(accountData.balance);
            });
        } else if (item.mapping_type === "single") {
            // 直接取单一科目余额
            const accountData = rawData.find(row => row.account_code === item.account_codes[0]);
            total = accountData ? flt(accountData.balance) : 0;
        } else if (item.formula) {
            // 执行自定义公式(需解析公式中的科目代码)
            total = eval_formula(item.formula, rawData); // 自定义公式解析函数
        }

        reportData.push({
            report_item: item.report_item,
            balance: total,
            accounting_system: item.accounting_system
        });
    });

    return reportData;
}

四、预置会计制度模板

1. 初始化默认映射规则

在系统安装时,通过 fixtures 预置三种会计制度的默认映射模板:

企业会计准则-资产负债表部分映射

报表项目 映射类型 关联科目 计算公式
货币资金 汇总 库存现金、银行存款、其他货币资金
应收账款 单一 应收账款
存货 汇总 原材料、库存商品、生产成本

小企业会计准则-利润表部分映射

报表项目 映射类型 关联科目 计算公式
营业收入 汇总 主营业务收入、其他业务收入
营业成本 汇总 主营业务成本、其他业务成本

2. 用户自定义逻辑

允许用户基于默认模板修改映射规则,修改后自动保存至 Report Mapping 文档,优先级高于系统默认模板:

// 保存自定义映射时,标记为非默认(is_default=0)
save_mapping: function () {
    const doc = frappe.get_doc("Report Mapping", currentDocName);
    doc.is_default = 0; // 非系统默认模板
    doc.save();
}

五、前端交互增强

1. 会计制度切换功能

在报表过滤条件中添加“会计制度”字段,允许用户切换不同报税格式:

// 修改 get_filters 函数,添加会计制度过滤项
function get_filters() {
    return [
        // ... 原有过滤条件 ...
        {
            fieldname: "accounting_system",
            label: __("会计制度"),
            fieldtype: "Select",
            options: ["企业会计准则", "小企业会计准则", "民间非营利组织会计制度"],
            default: "企业会计准则"
        }
    ];
}

2. 实时预览映射效果

在映射表维护界面添加“实时预览”按钮,调用报表预览接口显示当前配置的报表样式:

preview_report: function () {
    frappe.set_route("query-report", "Custom Financial Report", {
        accounting_system: currentSystem,
        // 传递其他过滤条件...
    });
}

六、后端接口设计

1. 获取映射规则接口

# 在 ERPNext 后端添加接口
@frappe.whitelist()
def get_mapping_rules(accounting_system):
    # 优先获取用户自定义映射(非默认)
    custom_mapping = frappe.get_all("Report Mapping", 
        filters={
            "accounting_system": accounting_system,
            "is_default": 0
        })

    if custom_mapping:
        return custom_mapping

    # 若无自定义,返回系统默认模板
    return frappe.get_all("Report Mapping", 
        filters={
            "accounting_system": accounting_system,
            "is_default": 1
        })

2. 公式解析函数(示例)

def eval_formula(formula, raw_data):
    # 将公式中的科目代码转换为数值(如 [A01] 转换为科目 A01 的余额)
    for match in re.findall(r'\[(\w+)\]', formula):
        account_balance = raw_data.get(match, 0)
        formula = formula.replace(f'[{match}]', str(account_balance))

    return eval(formula)  # 注意:需添加安全校验防止代码注入

七、应用场景与优势

  1. 多制度合规性:满足不同类型企业(中小企业、非营利组织)的报税需求,减少手工调整报表的工作量。
  2. 灵活性与可维护性:用户可直接在前端修改映射规则,无需开发人员介入,适应科目体系调整或报表项目变更。
  3. 数据一致性:通过映射表统一报表项目与科目关系,避免多套报表数据不一致的问题。

通过以上方案,ERPNext 可实现报表项目与会计科目的动态映射管理,并支持多会计制度的灵活切换。核心逻辑是通过前端维护的映射表(Report Mapping)解耦报表项目与科目体系,结合后端数据聚合规则,动态生成符合不同标准的财务报表。以下是针对将报表项目与会计科目的对应关系以及列定义单独设置为可维护的mapping表,并支持多会计制度的详细设计方案:

一、核心设计思路

  1. 数据模型重构

    • 创建独立的Report Mapping doctype,存储报表项目与会计科目的映射关系
    • 设计Report Format doctype,定义不同会计制度下的报表格式(列结构、展示顺序)
    • 通过Report Type字段关联特定会计制度(如"企业会计准则"、"小企业会计准则")
  2. 前端维护界面

    • 开发可视化映射管理界面,支持拖拽排序、批量导入导出
    • 提供预设模板快速切换功能(预置不同会计制度的映射规则)
  3. 动态报表生成

    • 基于用户选择的会计制度和映射规则,动态生成报表结构
    • 支持实时预览和验证功能

二、数据模型设计

1. Report Mapping (报表映射)

{
  "doctype": "Report Mapping",
  "fields": [
    {
      "fieldname": "report_type",
      "label": "报表类型",
      "fieldtype": "Select",
      "options": "企业会计准则\n小企业会计准则\n民间非营利组织会计制度\n其他",
      "reqd": 1
    },
    {
      "fieldname": "report_item",
      "label": "报表项目",
      "fieldtype": "Data",
      "reqd": 1
    },
    {
      "fieldname": "account",
      "label": "会计科目",
      "fieldtype": "Link",
      "options": "Account",
      "reqd": 1
    },
    {
      "fieldname": "parent_mapping",
      "label": "父级项目",
      "fieldtype": "Link",
      "options": "Report Mapping"
    },
    {
      "fieldname": "sequence",
      "label": "排序号",
      "fieldtype": "Int",
      "default": 0
    },
    {
      "fieldname": "formula",
      "label": "计算公式",
      "fieldtype": "Text"
    }
  ]
}

2. Report Format (报表格式)

{
  "doctype": "Report Format",
  "fields": [
    {
      "fieldname": "report_type",
      "label": "报表类型",
      "fieldtype": "Select",
      "options": "企业会计准则\n小企业会计准则\n民间非营利组织会计制度\n其他",
      "reqd": 1
    },
    {
      "fieldname": "report_name",
      "label": "报表名称",
      "fieldtype": "Select",
      "options": "资产负债表\n利润表\n现金流量表\n所有者权益变动表",
      "reqd": 1
    },
    {
      "fieldname": "columns",
      "label": "列定义",
      "fieldtype": "Table",
      "fields": [
        {
          "fieldname": "column_label",
          "label": "列标题",
          "fieldtype": "Data",
          "reqd": 1
        },
        {
          "fieldname": "column_type",
          "label": "列类型",
          "fieldtype": "Select",
          "options": "金额\n百分比\n文本",
          "default": "金额"
        },
        {
          "fieldname": "sequence",
          "label": "排序号",
          "fieldtype": "Int",
          "default": 0
        }
      ]
    }
  ]
}

三、前端实现方案

1. 映射管理界面

<div class="report-mapping-manager">
  <!-- 左侧:报表类型选择器 -->
  <div class="report-type-selector">
    <h4>报表类型</h4>
    <ul class="report-type-list">
      <li data-type="cas">企业会计准则</li>
      <li data-type="small_business">小企业会计准则</li>
      <li data-type="non_profit">民间非营利组织会计制度</li>
    </ul>
  </div>

  <!-- 中间:报表项目树 -->
  <div class="report-items-tree">
    <h4>报表项目结构</h4>
    <div class="tree-view">
      <!-- 树形结构展示报表项目 -->
      <ul>
        <li>
          <span class="tree-item">流动资产</span>
          <ul>
            <li><span class="tree-item" data-item="cash">货币资金</span></li>
            <li><span class="tree-item" data-item="receivables">应收账款</span></li>
            <!-- 其他项目 -->
          </ul>
        </li>
        <!-- 其他分类 -->
      </ul>
    </div>
  </div>

  <!-- 右侧:科目映射配置 -->
  <div class="mapping-config">
    <h4>科目映射配置</h4>
    <div class="form-group">
      <label>报表项目:</label>
      <p class="selected-item-display">货币资金</p>
    </div>
    <div class="form-group">
      <label>对应科目:</label>
      <select class="account-selector">
        <!-- 科目选项由后端API提供 -->
      </select>
    </div>
    <div class="form-group">
      <label>计算公式:</label>
      <textarea class="formula-input"></textarea>
    </div>
    <button class="btn btn-primary save-mapping">保存映射</button>
  </div>
</div>

2. 列定义管理界面

<div class="report-columns-manager">
  <h4>报表列定义 - {{report_type}} - {{report_name}}</h4>
  <table class="columns-table">
    <thead>
      <tr>
        <th>列标题</th>
        <th>列类型</th>
        <th>排序</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <!-- 列配置行 -->
      <tr>
        <td><input type="text" class="column-label" value="年初余额"></td>
        <td>
          <select class="column-type">
            <option value="amount">金额</option>
            <option value="percentage">百分比</option>
            <option value="text">文本</option>
          </select>
        </td>
        <td><input type="number" class="sequence" value="1"></td>
        <td><button class="btn btn-danger delete-column">删除</button></td>
      </tr>
      <!-- 其他列 -->
    </tbody>
  </table>
  <button class="btn btn-primary add-column">添加列</button>
</div>

四、核心实现代码

1. 动态加载报表配置

// 从mapping表加载报表配置
function load_report_config(report_type, report_name) {
  return frappe.call({
    method: "erpnext.accounts.doctype.report_mapping.report_mapping.get_report_config",
    args: {
      report_type: report_type,
      report_name: report_name
    }
  }).then(r => {
    return r.message;
  });
}

// 基于配置生成报表
function generate_report(config) {
  // 构建表头
  const columns = config.columns.map(col => ({
    label: col.column_label,
    fieldname: col.fieldname || col.column_label.toLowerCase().replace(/ /g, "_"),
    fieldtype: col.column_type === "金额" ? "Currency" : 
               col.column_type === "百分比" ? "Percent" : "Data",
    width: col.width || 150
  }));

  // 构建表体数据
  const data = config.mapping_items.map(item => {
    const row = {
      account: item.report_item,
      parent_account: item.parent_mapping
    };

    // 根据映射关系计算各列值
    columns.forEach(col => {
      if (col.fieldtype === "Currency") {
        row[col.fieldname] = calculate_value(item, col.fieldname);
      }
    });

    return row;
  });

  // 使用Frappe报表引擎渲染
  frappe.query_report = new frappe.QueryReport({
    parent: $("#report-container"),
    columns: columns,
    data: data,
    // 其他报表配置...
  });
}

2. 映射管理API

# erpnext/accounts/doctype/report_mapping/report_mapping.py
@frappe.whitelist()
def save_mapping(report_type, report_item, account, formula=None, parent_mapping=None):
    """保存报表项目与科目的映射关系"""
    mapping = frappe.get_doc({
        "doctype": "Report Mapping",
        "report_type": report_type,
        "report_item": report_item,
        "account": account,
        "formula": formula,
        "parent_mapping": parent_mapping
    }).insert()

    return mapping

@frappe.whitelist()
def get_report_config(report_type, report_name):
    """获取报表配置(映射关系和列定义)"""
    # 获取列定义
    columns = frappe.get_all("Report Format Column", 
        filters={"parent": report_type + "-" + report_name},
        fields=["column_label", "column_type", "sequence", "fieldname", "width"],
        order_by="sequence asc"
    )

    # 获取映射关系
    mapping_items = frappe.get_all("Report Mapping", 
        filters={"report_type": report_type},
        fields=["report_item", "account", "parent_mapping", "formula", "sequence"],
        order_by="sequence asc"
    )

    return {
        "columns": columns,
        "mapping_items": mapping_items
    }

五、预置会计制度方案

1. 预置数据导入

# 初始化脚本示例 - 导入企业会计准则预置数据
def setup_cas_mapping():
    # 资产负债表映射
    mappings = [
        {
            "report_type": "企业会计准则",
            "report_name": "资产负债表",
            "report_item": "货币资金",
            "account": "库存现金",
            "sequence": 10
        },
        {
            "report_type": "企业会计准则",
            "report_name": "资产负债表",
            "report_item": "货币资金",
            "account": "银行存款",
            "sequence": 20
        },
        # 其他映射...
    ]

    for mapping in mappings:
        doc = frappe.get_doc({
            "doctype": "Report Mapping",
            **mapping
        })
        doc.insert()

    # 设置报表格式
    format_doc = frappe.get_doc({
        "doctype": "Report Format",
        "report_type": "企业会计准则",
        "report_name": "资产负债表",
        "columns": [
            {"column_label": "项目", "column_type": "文本", "sequence": 10},
            {"column_label": "期末余额", "column_type": "金额", "sequence": 20},
            {"column_label": "年初余额", "column_type": "金额", "sequence": 30}
        ]
    }).insert()

2. 用户切换会计制度

// 在报表界面添加会计制度切换器
function add_accounting_standard_selector() {
  const selector = $(`
    <div class="accounting-standard-selector">
      <label>会计制度:</label>
      <select class="form-control">
        <option value="cas">企业会计准则</option>
        <option value="small_business">小企业会计准则</option>
        <option value="non_profit">民间非营利组织会计制度</option>
      </select>
    </div>
  `);

  selector.find("select").on("change", function() {
    const selected_standard = $(this).val();
    load_and_render_report(selected_standard);
  });

  $("#report-header").append(selector);
}

六、用户界面优化建议

  1. 拖拽排序:为报表项目和列定义添加拖拽排序功能,提升易用性
  2. 批量导入/导出:支持Excel导入导出映射配置,方便用户迁移数据
  3. 验证工具:添加配置验证功能,检查映射关系是否完整、公式是否合法
  4. 预览功能:提供实时预览,让用户在保存配置前查看报表效果
  5. 帮助文档:为每种会计制度提供字段说明和填写指南

通过这种设计,用户可以在前端灵活维护报表与科目的映射关系,同时系统预置多种会计制度模板,满足不同类型企业的需求。


丁、如何确定行列顺序

在报表映射数据模型中,列结构和展示顺序的定义需通过独立的数据表排序字段实现解耦,确保灵活性和可维护性。以下是具体实现方案:

一、数据模型设计:通过 Report Format 表定义列结构

创建 Report Format 数据模型(doctype),用于存储不同会计制度下的报表列定义(如列标题、类型、顺序等)。核心字段如下:

1. Report Format 主表字段

字段名 类型 说明
report_type 选择框 会计制度类型(如“企业会计准则”“小企业会计准则”)
report_name 选择框 报表名称(如“资产负债表”“利润表”)
is_default 复选框 是否为系统默认模板(用于初始化预置数据)
columns 表格字段 列定义子表(存储列标题、类型、顺序等)

2. Report Format Column 子表字段(列定义)

字段名 类型 说明
column_label 数据字段 列标题(如“期末余额”“年初余额”“百分比变化”)
column_type 选择框 列数据类型(金额/百分比/文本/日期),影响格式化方式
sequence 整数 列展示顺序(数值越小越靠左,如 10 表示第一列,20 表示第二列)
fieldname 数据字段 列对应的字段名(用于数据映射,如 closing_balance opening_balance
width 整数 列宽度(像素,如 150
formula 文本字段 列数据计算公式(可选,如 [A]/[B]*100 用于计算百分比)

二、列结构定义示例(以企业会计准则资产负债表为例)

1. Report Format 主表记录

{
  "doctype": "Report Format",
  "report_type": "企业会计准则",
  "report_name": "资产负债表",
  "is_default": 1,
  "columns": [
    {
      "column_label": "项目",
      "column_type": "文本",
      "sequence": 10,
      "fieldname": "report_item",
      "width": 200
    },
    {
      "column_label": "期末余额",
      "column_type": "金额",
      "sequence": 20,
      "fieldname": "closing_balance",
      "width": 180
    },
    {
      "column_label": "年初余额",
      "column_type": "金额",
      "sequence": 30,
      "fieldname": "opening_balance",
      "width": 180
    },
    {
      "column_label": "增减变动额",
      "column_type": "金额",
      "sequence": 40,
      "fieldname": "change_amount",
      "formula": "[closing_balance] - [opening_balance]",
      "width": 180
    }
  ]
}

2. 字段说明

  • sequence 字段:通过数值间隔(如 10、20、30)支持后续插入新列而不影响现有顺序(如在第1列和第2列之间插入新列可设为 15)。
  • column_type 与格式化
    • 金额:自动添加货币符号(如“¥”)和千位分隔符(如 1,000,000)。
    • 百分比:显示为 XX%(如 10.5%),支持正负值颜色标记(红色负、绿色正)。
    • 文本:直接显示字符串(如“合计”“注释”)。
  • formula 字段:支持基于其他列字段的计算(如 增减变动额 = 期末余额 - 年初余额)。

三、展示顺序控制逻辑

1. 基于 sequence 字段排序

在前端渲染报表时,通过 sequence 字段对列进行升序排序,确保按用户配置的顺序展示。示例代码(JavaScript):

// 从后端获取列定义后排序
const sortedColumns = columns.sort((a, b) => a.sequence - b.sequence);

// 渲染列头
sortedColumns.forEach(column => {
  $("<th>")
    .text(column.column_label)
    .css("width", `${column.width}px`)
    .appendTo("#report-header");
});

2. 拖拽排序交互(可选)

在列定义维护界面添加拖拽功能,允许用户通过拖放直接调整列顺序,实时更新 sequence 字段:

<!-- 列定义表格(支持拖拽) -->
<table draggable="true">
  <tbody sortable="true">
    <tr ng-repeat="col in columns" ng-model="col.sequence">
      <td>{{col.column_label}}</td>
      <td>{{col.column_type}}</td>
      <td><input type="number" ng-model="col.sequence"></td>
    </tr>
  </tbody>
</table>

<!-- 保存时更新顺序 -->
<button ng-click="saveSequence()">
  保存顺序(当前顺序:{{columns.map(c => c.sequence)}})
</button>

四、动态列生成场景

1. 多期间对比报表(如季度、年度)

通过 Report Format 动态生成列(如“Q1余额”“Q2余额”“年度累计”):

# 后端根据期间参数动态生成列
def generate_period_columns(periods):
    return [
        {
            "column_label": f"{period.name}余额",
            "column_type": "金额",
            "sequence": 10 + i*10,
            "fieldname": f"balance_{period.id}"
        } for i, period in enumerate(periods)
    ]

2. 自定义列扩展

允许用户在默认列基础上新增自定义列(如“预算对比”“行业平均”),存储至 Report Format 表中,实现灵活扩展:

// 前端添加自定义列按钮
function add_custom_column() {
  const newColumn = {
    column_label: "自定义列",
    column_type: "金额",
    sequence: maxSequence + 10, // 自动获取最大现有sequence
    fieldname: `custom_${Date.now()}`
  };
  columns.push(newColumn);
  save_report_format(); // 保存至后端
}

五、与报表映射表的关联

1. 数据聚合逻辑

通过 Report Mapping 表(报表项目与科目映射)和 Report Format 表(列定义)的结合,实现从科目数据到报表列的映射:

# 示例:计算“期末余额”列数据(基于科目余额汇总)
def calculate_closing_balance(report_item):
    # 获取该报表项目关联的所有科目
    accounts = frappe.get_all("Report Mapping", 
        filters={
            "report_type": "企业会计准则",
            "report_item": report_item.name
        },
        pluck="account"
    )

    # 汇总科目余额
    total = frappe.db.sql("""
        SELECT SUM(balance) 
        FROM `tabGL Entry` 
        WHERE account IN (%s) AND posting_date BETWEEN %s AND %s
    """, (accounts, start_date, end_date))[0][0];

    return total

2. 公式解析与计算

通过 column.formula 字段动态计算列值,例如: - 列定义:增减变动额 = [closing_balance] - [opening_balance] - 数据处理时,将 [closing_balance][opening_balance] 替换为实际数值,执行计算:

function evaluateFormula(formula, dataRow) {
  const regex = /\[([a-zA-Z_]+)\]/g;
  return formula.replace(regex, (match, field) => dataRow[field] || 0);
}

六、预置会计制度的列结构模板

为不同会计制度预置默认的 `Report Format` 模板,例如:

1. 小企业会计准则-利润表

列标签 类型 顺序 字段名 公式
项目 文本 10 reportitem -
本期金额 金额 20 currentamount -
本年累计金额 金额 30 cumulative_amount -

2. 民间非营利组织会计制度-现金流量表

列标签 类型 顺序 字段名 公式
项目 文本 10 reportitem -
金额 金额 20 cashamount -
占比 百分比 30 percentage [cash_amount]/[total_cash]*100

七、总结

通过 `Report Format` 表独立管理列结构和展示顺序,实现了以下优势:

  1. 灵活性:用户可自定义列标题、类型、顺序,甚至通过公式生成计算列。
  2. 多制度支持:不同会计制度可配置独立的列结构(如中国报表需显示“年初/期末余额”,国际报表可能需“本期/累计”)。
  3. 可维护性:列定义与报表逻辑解耦,修改列展示无需调整报表生成代码。
核心逻辑是通过**`sequence` 字段控制顺序**,**`column_type` 字段控制渲染方式**,**`formula` 字段支持动态计算**,最终实现符合业务需求的报表列结构动态定义。在报表映射数据模型中,定义列结构和展示顺序需要设计专门的数据实体来管理这些元数据。以下是一个完整的解决方案,展示如何在ERPNext中实现这一功能:

一、数据模型设计

1. 扩展Report Mapping模型

添加`report_section`字段区分不同报表(资产负债表、利润表等),并使用`sequence`控制展示顺序:
{
  "doctype": "Report Mapping",
  "fields": [
    {
      "fieldname": "report_type",
      "label": "会计制度",
      "fieldtype": "Select",
      "options": "企业会计准则\n小企业会计准则\n民间非营利组织会计制度",
      "reqd": 1
    },
    {
      "fieldname": "report_section",
      "label": "报表类型",
      "fieldtype": "Select",
      "options": "资产负债表\n利润表\n现金流量表",
      "reqd": 1
    },
    {
      "fieldname": "report_item",
      "label": "报表项目",
      "fieldtype": "Data",
      "reqd": 1
    },
    {
      "fieldname": "account",
      "label": "会计科目",
      "fieldtype": "Link",
      "options": "Account",
      "reqd": 1
    },
    {
      "fieldname": "sequence",
      "label": "展示顺序",
      "fieldtype": "Int",
      "default": 0,
      "reqd": 1
    },
    {
      "fieldname": "parent_item",
      "label": "父级项目",
      "fieldtype": "Link",
      "options": "Report Mapping"
    }
  ]
}

2. 创建Report Column(报表列定义)模型

专门管理报表的列结构和展示属性:
{
  "doctype": "Report Column",
  "fields": [
    {
      "fieldname": "report_section",
      "label": "报表类型",
      "fieldtype": "Select",
      "options": "资产负债表\n利润表\n现金流量表",
      "reqd": 1
    },
    {
      "fieldname": "column_label",
      "label": "列标题",
      "fieldtype": "Data",
      "reqd": 1
    },
    {
      "fieldname": "column_type",
      "label": "列类型",
      "fieldtype": "Select",
      "options": "Currency\nPercent\nData\nDate",
      "default": "Currency"
    },
    {
      "fieldname": "width",
      "label": "列宽度",
      "fieldtype": "Int",
      "default": 120
    },
    {
      "fieldname": "sequence",
      "label": "列顺序",
      "fieldtype": "Int",
      "default": 0,
      "reqd": 1
    },
    {
      "fieldname": "is_visible",
      "label": "是否显示",
      "fieldtype": "Check",
      "default": 1
    }
  ]
}

二、列结构管理界面

1. 列配置界面设计

<div class="report-column-config">
  <div class="section-header">
    <h4>报表列配置 - {{report_section}}</h4>
    <button class="btn btn-primary add-column">添加列</button>
  </div>

  <div class="column-list">
    <table class="table table-bordered">
      <thead>
        <tr>
          <th>列标题</th>
          <th>列类型</th>
          <th>宽度</th>
          <th>顺序</th>
          <th>可见性</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <!-- 列配置行 -->
        <tr ng-repeat="column in columns | orderBy:'sequence'">
          <td><input type="text" ng-model="column.column_label"></td>
          <td>
            <select ng-model="column.column_type">
              <option value="Currency">金额</option>
              <option value="Percent">百分比</option>
              <option value="Data">文本</option>
              <option value="Date">日期</option>
            </select>
          </td>
          <td><input type="number" ng-model="column.width"></td>
          <td><input type="number" ng-model="column.sequence"></td>
          <td><input type="checkbox" ng-model="column.is_visible"></td>
          <td>
            <button class="btn btn-danger" ng-click="deleteColumn(column)">删除</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

2. 拖拽排序实现

// 使用Sortable.js实现列拖拽排序
function initColumnSortable() {
  new Sortable(document.querySelector('.column-list tbody'), {
    handle: '.sort-handle',
    animation: 150,
    onEnd: function(evt) {
      // 更新列顺序
      const columns = Array.from(document.querySelectorAll('.column-list tbody tr'));
      columns.forEach((row, index) => {
        const seqInput = row.querySelector('input[name="sequence"]');
        seqInput.value = index + 1;
      });
    }
  });
}

三、报表生成逻辑

1. 基于配置生成报表列

def get_report_columns(report_section, accounting_standard=None):
    """获取指定报表的列配置"""
    filters = {"report_section": report_section}
    if accounting_standard:
        filters["accounting_standard"] = accounting_standard

    columns = frappe.get_all("Report Column",
        filters=filters,
        fields=["name", "column_label", "column_type", "width", "sequence", "is_visible"],
        order_by="sequence asc"
    )

    return [col for col in columns if col.is_visible]

2. 构建报表数据结构

def build_report_data(report_section, accounting_standard):
    """根据映射关系和列配置构建报表数据"""
    # 获取报表项目映射
    mapping_items = frappe.get_all("Report Mapping",
        filters={
            "report_section": report_section,
            "report_type": accounting_standard
        },
        fields=["report_item", "account", "sequence", "parent_item"],
        order_by="sequence asc"
    )

    # 获取列配置
    columns = get_report_columns(report_section, accounting_standard)

    # 构建报表数据结构
    report_data = []
    for item in mapping_items:
        row = {"report_item": item.report_item}

        # 计算各列值(示例:简化处理)
        for col in columns:
            if col.column_type == "Currency":
                row[col.name] = get_account_balance(item.account, col.name)

        report_data.append(row)

    return {
        "columns": columns,
        "data": report_data
    }

四、预置会计制度的列配置

1. 企业会计准则-资产负债表列配置

[
  {
    "report_section": "资产负债表",
    "column_label": "项目",
    "column_type": "Data",
    "width": 200,
    "sequence": 10,
    "is_visible": 1
  },
  {
    "report_section": "资产负债表",
    "column_label": "期末余额",
    "column_type": "Currency",
    "width": 150,
    "sequence": 20,
    "is_visible": 1
  },
  {
    "report_section": "资产负债表",
    "column_label": "年初余额",
    "column_type": "Currency",
    "width": 150,
    "sequence": 30,
    "is_visible": 1
  }
]

2. 小企业会计准则-利润表列配置

[
  {
    "report_section": "利润表",
    "column_label": "项目",
    "column_type": "Data",
    "width": 200,
    "sequence": 10,
    "is_visible": 1
  },
  {
    "report_section": "利润表",
    "column_label": "本年累计金额",
    "column_type": "Currency",
    "width": 150,
    "sequence": 20,
    "is_visible": 1
  },
  {
    "report_section": "利润表",
    "column_label": "本月金额",
    "column_type": "Currency",
    "width": 150,
    "sequence": 30,
    "is_visible": 1
  }
]

五、前端展示优化

1. 列配置预览功能

function previewReportColumns() {
    const columns = getSelectedColumns();
    const previewHtml = `
        <div class="report-preview">
            <table class="table table-bordered">
                <thead>
                    <tr>
                        ${columns.map(col => `<th>${col.column_label}</th>`).join('')}
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        ${columns.map(col => `<td>${getSampleValue(col.column_type)}</td>`).join('')}
                    </tr>
                </tbody>
            </table>
        </div>
    `;

    $('#preview-modal .modal-body').html(previewHtml);
    $('#preview-modal').modal('show');
}

function getSampleValue(columnType) {
    switch(columnType) {
        case 'Currency': return '¥123,456.78';
        case 'Percent': return '25.5%';
        case 'Date': return '2023-12-31';
        default: return '示例文本';
    }
}

2. 列配置导入/导出

function exportColumnConfig() {
    const config = getSelectedColumns();
    const blob = new Blob([JSON.stringify(config, null, 2)], {type: 'application/json'});
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `column_config_${reportSection}.json`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
}

function importColumnConfig(file) {
    const reader = new FileReader();
    reader.onload = function(e) {
        try {
            const config = JSON.parse(e.target.result);
            setColumns(config);
            frappe.msgprint(__('配置导入成功'));
        } catch(err) {
            frappe.throw(__('配置文件格式错误'));
        }
    };
    reader.readAsText(file);
}

六、核心优势

  1. 灵活性:通过独立的列配置模型,可以针对不同报表类型和会计制度定制列结构
  2. 可维护性:列定义与报表映射分离,便于单独管理和修改
  3. 扩展性:支持动态添加新的列类型和报表类型,无需修改核心代码
  4. 可视化管理:提供直观的界面管理列配置,支持拖拽排序和预览功能
这种设计方案允许用户完全自定义报表的列结构和展示顺序,同时系统预置的会计制度模板提供了标准化的起点,大大提高了报表配置的效率和准确性。

戊、增加对比列及灵活取数

以下是在 ERPNext 中增强报表格式自定义和灵活取数功能的方案设计,结合 `financial_statements.js` 现有逻辑,通过扩展前端配置、数据模型和计算逻辑实现同期对比、预算对比、累计与分期等复杂报表需求:

一、核心功能扩展目标

  1. 报表格式自定义

    • 支持用户动态配置列布局(如添加“同期对比”“预算差异”列)。
    • 定义列数据类型(金额、百分比、差异值)及显示格式(货币符号、千位分隔符)。
  2. 灵活取数逻辑

    • 支持多期间数据对比(如本期 vs 上年同期、本季 vs 上季)。
    • 集成预算数据对比(实际值 vs 预算值)。
    • 实现累计值与分期值的动态计算(如“本年累计”“最近三个月平均”)。

二、数据模型扩展

1. 新增报表格式配置表(Report Format)

在原有 `Report Mapping` 基础上,扩展列格式和取数规则:
# 新增字段说明
{
  "doctype": "Report Format",
  "fields": [
    {
      "fieldname": "report_type",
      "label": "报表类型",
      "fieldtype": "Select",
      "options": "资产负债表\n利润表\n自定义报表",
      "reqd": 1
    },
    {
      "fieldname": "columns",
      "label": "列配置",
      "fieldtype": "Table",
      "fields": [
        {
          "fieldname": "column_id",
          "label": "列标识",
          "fieldtype": "Data",
          "reqd": 1,
          "default": "actual"  # 示例:actual(实际值)、budget(预算值)、yoy(同比)
        },
        {
          "fieldname": "column_label",
          "label": "列标题",
          "fieldtype": "Data",
          "reqd": 1
        },
        {
          "fieldname": "data_type",
          "label": "数据类型",
          "fieldtype": "Select",
          "options": "actual\nbudget\ncomparison\ncalculated",
          "reqd": 1
        },
        {
          "fieldname": "period_type",
          "label": "期间类型",
          "fieldtype": "Select",
          "options": "current\nprevious\nyear_on_year\nquarter_on_quarter",
          "depends_on": "eval:doc.data_type === 'comparison'"
        },
        {
          "fieldname": "calculation_rule",
          "label": "计算规则",
          "fieldtype": "Text",
          "depends_on": "eval:doc.data_type === 'calculated'"
        },
        {
          "fieldname": "format_string",
          "label": "显示格式",
          "fieldtype": "Data",
          "default": "###,###.00"  # 数字格式模板
        }
      ]
    }
  ]
}

2. 预算数据关联

在会计科目或独立预算表中添加预算值字段,通过过滤条件获取预算数据:
# 预算数据模型示例
{
  "doctype": "Budget",
  "fields": [
    {
      "fieldname": "account",
      "label": "会计科目",
      "fieldtype": "Link",
      "options": "Account",
      "reqd": 1
    },
    {
      "fieldname": "budget_amount",
      "label": "预算金额",
      "fieldtype": "Currency"
    },
    {
      "fieldname": "fiscal_year",
      "label": "财年",
      "fieldtype": "Link",
      "options": "Fiscal Year"
    }
  ]
}

三、前端配置界面增强

1. 列格式配置面板

在现有映射界面中添加列配置模块,支持动态添加对比列和计算列:
<!-- 列配置界面 -->
<div class="report-column-builder">
  <h4>列布局设计</h4>
  <button class="btn btn-primary" ng-click="addColumn('actual')">添加实际值列</button>
  <button class="btn btn-info" ng-click="addColumn('budget')">添加预算值列</button>
  <button class="btn btn-warning" ng-click="addColumn('yoy')">添加同比列</button>

  <table class="table">
    <thead>
      <tr>
        <th>列标题</th>
        <th>数据类型</th>
        <th>期间对比</th>
        <th>计算规则</th>
        <th>显示格式</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="col in columns">
        <td><input type="text" ng-model="col.column_label"></td>
        <td>
          <select ng-model="col.data_type">
            <option value="actual">实际值</option>
            <option value="budget">预算值</option>
            <option value="comparison">对比值</option>
            <option value="calculated">计算值</option>
          </select>
        </td>
        <td ng-show="col.data_type === 'comparison'">
          <select ng-model="col.period_type">
            <option value="year_on_year">同比(上年同期)</option>
            <option value="quarter_on_quarter">环比(上季)</option>
          </select>
        </td>
        <td ng-show="col.data_type === 'calculated'">
          <input type="text" ng-model="col.calculation_rule" placeholder="如:[actual] - [budget]">
        </td>
        <td><input type="text" ng-model="col.format_string" placeholder="如:¥#,##0.00"></td>
      </tr>
    </tbody>
  </table>
</div>

2. 期间选择器优化

在报表过滤条件中添加期间对比选项,支持动态切换对比周期:
// 在 get_filters 函数中添加期间对比参数
function get_filters() {
  return [
    // 原有过滤条件...
    {
      "fieldname": "compare_with",
      "label": __("对比期间"),
      "fieldtype": "Select",
      "options": ["无", "上年同期", "上一季度", "预算"],
      "default": "无"
    }
  ];
}

四、数据取数与计算逻辑

1. 多期间数据获取

根据 `compare_with` 参数获取不同期间的科目余额:
def get_period_data(account, period, compare_with=None):
    """获取指定期间及对比期间的数据"""
    # 主期间数据
    main_data = frappe.db.sql("""
        SELECT sum(balance) AS amount
        FROM `tabGL Entry`
        WHERE account = %s AND posting_date BETWEEN %s AND %s
    """, (account, period.start_date, period.end_date), as_dict=True)[0]

    if not compare_with:
        return {"actual": main_data.amount}

    # 对比期间数据(如上年同期)
    compare_period = get_compare_period(period, compare_with)
    compare_data = frappe.db.sql("""
        SELECT sum(balance) AS amount
        FROM `tabGL Entry`
        WHERE account = %s AND posting_date BETWEEN %s AND %s
    """, (account, compare_period.start_date, compare_period.end_date), as_dict=True)[0]

    return {
        "actual": main_data.amount,
        "comparison": compare_data.amount,
        "difference": main_data.amount - compare_data.amount
    }

2. 预算对比逻辑

关联预算数据并计算差异:
def get_budget_data(account, fiscal_year):
    """获取预算金额"""
    budget = frappe.get_value("Budget", {
        "account": account,
        "fiscal_year": fiscal_year
    }, "budget_amount")
    return budget or 0

def calculate_budget_variance(actual, budget):
    """计算预算差异及百分比"""
    return {
        "variance": actual - budget,
        "variance_percent": (actual / budget - 1) * 100 if budget != 0 else 0
    }

3. 累计值与分期值计算

根据报表周期(月、季、年)动态计算累计值:
// 前端计算累计值示例
function calculate_cumulative(data, periodicity) {
    const cumulative = [];
    let total = 0;
    data.forEach((row, index) => {
        total += row.amount;
        cumulative.push({
            ...row,
            cumulative_amount: total,
            periodicity: periodicity
        });
    });
    return cumulative;
}

五、列格式渲染与可视化

1. 动态列渲染

根据 `columns` 配置生成报表列,支持差异值颜色标记:
// 在 formatter 函数中处理列格式
erpnext.financial_statements.formatter = function (value, row, column, data) {
    // 处理对比差异列(如同比增减)
    if (column.column_id === "difference") {
        const sign = value < 0 ? "-" : "+";
        const absValue = Math.abs(value);
        return `<span class="${value < 0 ? 'text-danger' : 'text-success'}">
            ${sign}${absValue.toLocaleString()}
          </span>`;
    }

    // 处理百分比列(如预算完成率)
    if (column.data_type === "calculated" && column.calculation_rule.includes("%")) {
        return `${value.toFixed(2)}%`;
    }

    // 通用数字格式
    return value.toLocaleString(column.format_string);
};

2. 图表可视化扩展

基于对比数据生成趋势图或差异分析图:
// 渲染对比图表
function render_comparison_chart(data) {
    frappe.chart.render({
        parent: "#chart-container",
        title: "实际值与预算对比",
        data: {
            labels: data.map(row => row.report_item),
            datasets: [
                { name: "实际值", values: data.map(row => row.actual) },
                { name: "预算值", values: data.map(row => row.budget) }
            ]
        },
        type: "bar",
        height: 300
    });
}

六、应用场景示例

1. 利润表同期对比

  • 列配置:实际值(本期)、实际值(上年同期)、同比差异(%)。
  • 计算规则

    • 同比差异金额 = 本期实际值 - 上年同期实际值
    • 同比差异百分比 = (本期实际值 / 上年同期实际值 - 1) * 100

2. 费用预算对比表

  • 列配置:科目名称、实际值、预算值、差异金额、差异百分比。
  • 数据来源:实际值来自 GL entries,预算值来自 Budget 表。

3. 累计数据展示(如全年累计收入)

  • 列配置:月份、当月值、本年累计值。
  • 计算规则:本年累计值 = 各月值之和(通过 calculate_cumulative 函数实现)。

七、系统集成与性能优化

  1. 缓存机制:对常用报表格式和预算数据进行缓存,减少数据库查询压力。
  2. 异步加载:在生成复杂报表(如多期间对比)时,使用异步请求和加载状态提示。
  3. 权限控制:对自定义报表格式和预算数据设置权限,确保敏感数据安全。

总结

通过扩展报表格式配置、增强多期间取数逻辑和动态计算能力,ERPNext 可支持灵活的报表自定义需求。核心方案包括: 1. **数据模型扩展**:通过 `Report Format` 表管理列格式和计算规则。 2. **前端交互优化**:提供可视化列配置界面和期间对比选择器。 3. **计算逻辑抽象**:封装多期间数据获取、预算对比、累计值计算等通用函数。 4. **可视化增强**:结合图表和颜色编码提升数据可读性。 此方案在保持原有代码结构的基础上,通过模块化设计实现功能扩展,适用于需要频繁调整报表格式和分析维度的企业财务场景。以下是对增强财务报表格式自定义管理和灵活取数功能的详细解读与实现方案:

一、核心需求分析

用户需要系统支持: 1. **多维度对比**:同期对比(同比、环比)、预算对比、实际与预测对比 2. **灵活列格式**:累计值、分期值、增长率、占比等计算列 3. **自定义管理**:用户可配置列定义、计算逻辑、展示样式

二、技术实现方案

1. 数据模型扩展

{
  "doctype": "Report Column",
  "fields": [
    {
      "fieldname": "column_type",
      "label": "列类型",
      "fieldtype": "Select",
      "options": "\n金额\n百分比\n文本\n日期\n增长率\n占比\n累计值\n分期值"
    },
    {
      "fieldname": "comparison_type",
      "label": "对比类型",
      "fieldtype": "Select",
      "options": "\n无对比\n同比\n环比\n预算对比\n预测对比"
    },
    {
      "fieldname": "period_type",
      "label": "期间类型",
      "fieldtype": "Select",
      "options": "\n本期\n累计\n分期"
    },
    {
      "fieldname": "formula",
      "label": "计算公式",
      "fieldtype": "Text Editor"
    }
  ]
}

2. 动态取数引擎

def get_comparison_data(account, base_period, comparison_type):
    """根据对比类型获取相应期间的数据"""
    if comparison_type == "同比":
        # 获取去年同期数据
        compare_period = get_same_period_last_year(base_period)
    elif comparison_type == "环比":
        # 获取上期数据
        compare_period = get_previous_period(base_period)
    elif comparison_type == "预算对比":
        # 获取预算数据
        return get_budget_data(account, base_period)

    # 获取实际数据
    return get_actual_data(account, compare_period)

def calculate_column_value(row, column):
    """根据列定义计算单元格值"""
    if column.formula:
        # 解析自定义公式
        return evaluate_formula(column.formula, row)

    # 基于列类型和对比类型计算
    if column.comparison_type:
        base_value = row.get(column.base_field, 0)
        compare_value = get_comparison_data(row.account, row.period, column.comparison_type)

        if column.column_type == "增长率":
            return ((base_value - compare_value) / compare_value) * 100 if compare_value else 0
        elif column.column_type == "占比":
            return (base_value / compare_value) * 100 if compare_value else 0

    return row.get(column.fieldname, 0)

3. 公式解析引擎

def evaluate_formula(formula, row_data):
    """安全解析并执行自定义公式"""
    # 替换公式中的变量为实际值
    for key, value in row_data.items():
        formula = formula.replace(f"[{key}]", str(value))

    # 使用安全的表达式评估器
    safe_globals = {
        "__builtins__": None,
        "sum": sum,
        "avg": lambda x: sum(x)/len(x) if x else 0,
        "max": max,
        "min": min
    }

    try:
        return eval(formula, safe_globals)
    except Exception as e:
        frappe.log_error(f"公式解析错误: {formula}, 错误: {str(e)}")
        return 0

三、前端交互增强

1. 列配置界面扩展

<div class="report-column-config">
  <div class="column-settings">
    <div class="form-group">
      <label>列类型</label>
      <select ng-model="column.column_type" ng-change="updateColumnType()">
        <option value="金额">金额</option>
        <option value="百分比">百分比</option>
        <option value="增长率">增长率</option>
        <option value="占比">占比</option>
        <option value="累计值">累计值</option>
        <option value="分期值">分期值</option>
      </select>
    </div>

    <div class="form-group" ng-show="showComparisonOptions()">
      <label>对比类型</label>
      <select ng-model="column.comparison_type">
        <option value="">无对比</option>
        <option value="同比">同比</option>
        <option value="环比">环比</option>
        <option value="预算对比">预算对比</option>
        <option value="预测对比">预测对比</option>
      </select>
    </div>

    <div class="form-group" ng-show="column.column_type === '分期值'">
      <label>分期方式</label>
      <select ng-model="column.period_split">
        <option value="月度">月度</option>
        <option value="季度">季度</option>
        <option value="半年度">半年度</option>
      </select>
    </div>

    <div class="form-group" ng-show="column.formula">
      <label>计算公式</label>
      <textarea ng-model="column.formula" rows="3"></textarea>
      <p class="help-block">可用变量: [字段名], 可用函数: sum(), avg(), max(), min()</p>
    </div>
  </div>
</div>

2. 动态列生成与预览

function generate_report_columns() {
    const base_columns = [
        { label: "项目", fieldname: "account", type: "text" }
    ];

    // 根据配置生成对比列
    if (report_config.compare_with_previous) {
        base_columns.push({
            label: "上期金额",
            fieldname: "previous_amount",
            type: "currency"
        });

        base_columns.push({
            label: "增减额",
            fieldname: "change_amount",
            type: "currency",
            formula: "[amount] - [previous_amount]"
        });

        base_columns.push({
            label: "增减率",
            fieldname: "change_rate",
            type: "percent",
            formula: "([amount] - [previous_amount]) / [previous_amount] * 100"
        });
    }

    // 生成预算对比列
    if (report_config.compare_with_budget) {
        base_columns.push({
            label: "预算金额",
            fieldname: "budget_amount",
            type: "currency"
        });

        base_columns.push({
            label: "预算差异",
            fieldname: "budget_diff",
            type: "currency",
            formula: "[amount] - [budget_amount]"
        });

        base_columns.push({
            label: "预算达成率",
            fieldname: "budget_fulfillment",
            type: "percent",
            formula: "[amount] / [budget_amount] * 100"
        });
    }

    return base_columns;
}

四、预置报表模板

1. 同比对比报表配置

{
  "report_name": "利润表同比分析",
  "columns": [
    {
      "column_label": "项目",
      "column_type": "文本",
      "sequence": 10
    },
    {
      "column_label": "本年金额",
      "column_type": "金额",
      "sequence": 20,
      "period_type": "累计"
    },
    {
      "column_label": "上年金额",
      "column_type": "金额",
      "sequence": 30,
      "period_type": "累计",
      "comparison_type": "同比"
    },
    {
      "column_label": "增减额",
      "column_type": "金额",
      "sequence": 40,
      "formula": "[本年金额] - [上年金额]"
    },
    {
      "column_label": "增减率",
      "column_type": "百分比",
      "sequence": 50,
      "formula": "([本年金额] - [上年金额]) / [上年金额] * 100"
    }
  ]
}

2. 预算对比报表配置

{
  "report_name": "预算执行分析表",
  "columns": [
    {
      "column_label": "项目",
      "column_type": "文本",
      "sequence": 10
    },
    {
      "column_label": "实际金额",
      "column_type": "金额",
      "sequence": 20
    },
    {
      "column_label": "预算金额",
      "column_type": "金额",
      "sequence": 30,
      "comparison_type": "预算对比"
    },
    {
      "column_label": "差异额",
      "column_type": "金额",
      "sequence": 40,
      "formula": "[实际金额] - [预算金额]"
    },
    {
      "column_label": "完成率",
      "column_type": "百分比",
      "sequence": 50,
      "formula": "[实际金额] / [预算金额] * 100"
    }
  ]
}

五、系统架构优化

1. 取数引擎架构

用户请求 → 报表配置解析 → 期间计算 → 多数据源取数 → 数据聚合 → 公式计算 → 结果渲染

2. 性能优化

  • 数据缓存:对频繁查询的期间数据进行缓存
  • 异步计算:复杂报表采用后台任务计算
  • 增量更新:只重新计算变化的数据部分
def get_cached_data(account, period):
    """从缓存获取数据,若无则计算并缓存"""
    cache_key = f"financial_data:{account}:{period}"
    data = cache.get(cache_key)

    if not data:
        data = calculate_account_data(account, period)
        cache.set(cache_key, data, expire=3600)  # 缓存1小时

    return data

六、用户体验优化

1. 动态列配置预览

function previewColumnConfig() {
    // 生成预览数据
    const previewData = [
        { account: "营业收入", amount: 1250000, previous_amount: 1000000 },
        { account: "营业成本", amount: 750000, previous_amount: 600000 },
        { account: "销售费用", amount: 200000, previous_amount: 180000 }
    ];

    // 应用公式计算
    const columns = getSelectedColumns();
    const formattedData = previewData.map(row => {
        const formattedRow = { ...row };
        columns.forEach(col => {
            if (col.formula) {
                formattedRow[col.fieldname] = evaluateFormula(col.formula, row);
            }
        });
        return formattedRow;
    });

    // 渲染预览表格
    renderPreviewTable(columns, formattedData);
}

2. 列格式自动匹配

// 根据对比类型自动设置列格式
function autoSetColumnFormat() {
    if (column.comparison_type === "同比" || column.comparison_type === "环比") {
        column.column_type = "增长率";
        column.formula = "([amount] - [compare_amount]) / [compare_amount] * 100";
    }

    if (column.comparison_type === "预算对比") {
        column.column_type = "百分比";
        column.formula = "[amount] / [budget_amount] * 100";
    }
}

七、关键技术挑战与解决方案

1. 复杂公式计算安全

  • 使用沙箱环境执行公式
  • 限制可用函数和变量
  • 实现公式验证和审计

2. 多期间数据处理

  • 设计高效的期间数据存储结构
  • 实现期间偏移计算函数
  • 支持跨年、跨月对比

3. 性能优化

  • 实现数据预计算和缓存机制
  • 支持报表结果导出和定时生成
  • 采用分页加载和懒加载技术

通过这种设计,系统可以满足用户对报表格式的高度自定义需求,支持灵活的对比分析和复杂计算,同时保持良好的性能和用户体验。


己、代码文件的生效部署

针对你提供的 financial_statements.js 文件及后续功能扩展需求,以下是关于代码操作、部署方式及生效逻辑的详细说明:

一、代码文件的作用与位置

1. 文件功能定位

financial_statements.js 是 ERPNext 财务报表模块的前端核心脚本,负责: - 报表过滤条件渲染 - 数据格式化与可视化 - 报表间导航与交互逻辑 - 树状结构展示与总账跳转

2. 原文件路径

在 ERPNext 标准架构中,该文件位于:

erpnext/public/js/financial_statements.js

属于前端静态资源,需通过服务器部署后生效。

二、代码修改的三种方式

1. 直接修改源文件(不推荐)

操作步骤: 1. 登录服务器,进入路径 /home/frappe/frappe-bench/apps/erpnext/erpnext/public/js/ 2. 使用文本编辑器(如 vi)修改 financial_statements.js 3. 保存后,执行以下命令重启服务并清除缓存:

# 重启服务
bench restart
# 清除前端缓存
bench clear-cache

缺点: - 升级 ERPNext 时修改会被覆盖 - 无法进行版本控制 - 多人协作易冲突

2. 通过自定义应用扩展(推荐)

操作步骤: 1. 创建自定义应用(假设名为 custom_erpnext):

bench create-app custom_erpnext
2. 在自定义应用中复制原文件并修改:
mkdir -p custom_erpnext/public/js/
cp /home/frappe/frappe-bench/apps/erpnext/erpnext/public/js/financial_statements.js custom_erpnext/public/js/
3. 修改代码中的命名空间(避免冲突),例如:
// 原代码
erpnext.financial_statements = { ... }

// 自定义代码(建议添加前缀)
erpnext.custom_financial_statements = { ... }
4. 在 custom_erpnext 应用的 __init__.py 中声明静态资源:
from frappe import _
def get_include_js():
    return ["public/js/financial_statements.js"]
5. 安装自定义应用并更新 ERPNext:
bench install-app custom_erpnext
bench update

优点: - 不影响核心代码,支持版本管理 - 升级 ERPNext 时修改保留 - 适合团队协作开发

3. 通过热更新工具动态加载(调试场景)

适用场景:本地开发调试,无需重启服务 操作步骤: 1. 在 ERPNext 开发环境中,使用 frappe-web 服务监听文件变化 2. 修改本地 financial_statements.js 后,执行:

bench watch
3. 浏览器访问 http://localhost:8000 并按 Ctrl+R 刷新

注意:此方式仅适用于开发环境,生产环境需通过正式部署生效

三、功能扩展的代码集成方式

1. 新增功能模块

场景:添加中国报表格式、自定义映射表等新功能 操作示例: 1. 在 erpnext.financial_statements 对象中新增方法:

erpnext.financial_statements.china_report = {
    init: function() {
        // 中国报表初始化逻辑
    },
    format_cny: function(amount) {
        // 人民币格式化函数
    }
};
2. 在 onload 事件中触发新功能:
onload: function(report) {
    // 原有逻辑...
    if (frappe.query_report.get_filter_value("report_type") === "china") {
        erpnext.financial_statements.china_report.init();
    }
}

2. 修改现有函数(以 formatter 为例)

场景:调整数据格式化逻辑(如添加人民币符号) 原代码片段

formatter: function (value, row, column, data, default_formatter, filter) {
    // 原有逻辑...
    if (isChinaReport) {
        value = format_cny(value); // 新增格式化函数调用
    }
    // 后续逻辑...
}

3. 引入外部依赖

场景:使用 jQuery UI 实现拖拽排序 操作步骤: 1. 在 erpnext/public/js/ 目录下添加 jquery-ui.min.js 2. 在 financial_statements.js 顶部引入:

frappe.provide("erpnext.ui");
erpnext.ui.jquery_ui = $.ui; // 声明依赖

四、部署生效的关键步骤

1. 静态资源编译(生产环境必需)

修改前端代码后,需重新编译静态资源:

# 进入 bench 目录
cd frappe-bench
# 编译 CSS/JS
bench build
# 清除缓存
bench clear-cache

2. Nginx 配置检查

确保 Nginx 正确映射静态资源路径(public/ 目录),配置示例:

location /assets/ {
    alias /home/frappe/frappe-bench/apps/erpnext/erpnext/public/;
    try_files $uri $uri/ =404;
}

3. 浏览器缓存处理

若修改后未生效,尝试: - 按住 Ctrl 键点击刷新按钮(强制刷新) - 清除浏览器缓存(快捷键:Ctrl+Shift+Delete) - 在 URL 后添加 ?v=版本号 强制加载新资源(如 http://example.com?v=20231001

五、版本管理与回滚

1. 使用 Git 管理代码变更

# 在自定义应用目录初始化 Git
cd custom_erpnext
git init
git add .
git commit -m "feat: 新增中国报表格式"

2. 回滚至历史版本

# 查看提交记录
git log
# 回滚至上一版本
git reset --hard HEAD^
# 重新部署
bench update

六、常见问题与解决方案

问题描述 可能原因 解决方案
修改后页面无变化 静态资源未重新编译 执行 bench buildbench clear-cache
浏览器报错 JS 语法错误 代码存在语法错误 使用 IDE 检查语法,确保代码格式正确
自定义功能未生效 命名空间冲突或未正确初始化 检查命名空间是否唯一,确保函数被正确调用
服务器 500 错误 代码逻辑错误导致服务器异常 查看日志 frappe-bench/logs/error.log

总结

  1. 生产环境推荐方式:通过自定义应用扩展,避免直接修改核心文件
  2. 开发流程:修改代码 → 编译静态资源 → 清除缓存 → 验证功能
  3. 关键目录erpnext/public/js/ 存放前端脚本,需确保部署路径正确
  4. 版本控制:使用 Git 管理自定义应用代码,便于协作和回滚

通过以上步骤,可安全、高效地将功能扩展代码集成到 ERPNext 系统中,并确保修改生效。根据您的需求,我将为您提供具体的操作指南,说明如何集成这些增强功能到ERPNext系统中。以下是分步骤的实施方法:

一、核心文件与目录结构

ERPNext财务报表功能主要涉及以下核心文件:

erpnext/accounts/doctype/
├── report_format/                  # 报表格式配置
│   ├── report_format.py            # 后端逻辑
│   └── report_format.js            # 前端界面
├── financial_statements/           # 财务报表核心模块
│   ├── financial_statements.py     # 数据处理逻辑
│   └── financial_statements.js     # 前端渲染逻辑
└── budget/                         # 预算管理模块
    └── budget.py                   # 预算数据处理

二、具体实施步骤

1. 创建新的Doctype(Report Column)

# 1. 在bench环境下创建新的doctype
bench new-doctype Report Column --module Accounts

# 2. 打开Frappe Desk,进入Report Column文档类型
# 3. 添加字段定义(参考之前的JSON结构)

2. 修改 financial_statements.py 文件

# 文件路径: erpnext/accounts/doctype/financial_statements/financial_statements.py

# 1. 导入必要的模块
from frappe.utils import getdate, add_months, add_years

# 2. 添加新的取数函数
def get_comparison_data(account, base_period, comparison_type):
    """根据对比类型获取相应期间的数据"""
    if comparison_type == "同比":
        compare_period = {
            "start_date": add_years(getdate(base_period.get("start_date")), -1),
            "end_date": add_years(getdate(base_period.get("end_date")), -1)
        }
    elif comparison_type == "环比":
        compare_period = {
            "start_date": add_months(getdate(base_period.get("start_date")), -1),
            "end_date": add_months(getdate(base_period.get("end_date")), -1)
        }

    # 获取对比期间数据
    return get_account_balance(account, compare_period)

# 3. 修改主函数,支持新的列类型
def prepare_data(filters):
    # 原有代码...

    # 处理新的列类型
    for row in data:
        for col in columns:
            if col.get("column_type") == "增长率":
                base_value = row.get(col.get("base_field"), 0)
                compare_value = get_comparison_data(
                    row.get("account"), 
                    filters.get("period"), 
                    col.get("comparison_type")
                )
                row[col.get("fieldname")] = ((base_value - compare_value) / compare_value * 100) if compare_value else 0

    return data

3. 添加公式解析引擎

# 文件路径: erpnext/accounts/doctype/financial_statements/financial_statements.py

def evaluate_formula(formula, row_data):
    """安全解析并执行自定义公式"""
    # 替换公式中的变量为实际值
    for key, value in row_data.items():
        formula = formula.replace(f"[{key}]", str(value))

    # 使用安全的表达式评估器
    safe_globals = {
        "__builtins__": None,
        "sum": sum,
        "avg": lambda x: sum(x)/len(x) if x else 0,
        "max": max,
        "min": min
    }

    try:
        return eval(formula, safe_globals)
    except Exception as e:
        frappe.log_error(f"公式解析错误: {formula}, 错误: {str(e)}")
        return 0

4. 修改前端文件 financial_statements.js

// 文件路径: erpnext/accounts/doctype/financial_statements/financial_statements.js

// 1. 添加列配置UI
frappe.ui.form.on("Financial Statements", {
    refresh: function(frm) {
        // 原有代码...

        // 添加列配置按钮
        frm.add_custom_button(__("配置列"), function() {
            open_column_config_dialog(frm);
        });
    }
});

// 2. 添加列配置对话框
function open_column_config_dialog(frm) {
    // 创建对话框
    const dialog = new frappe.ui.Dialog({
        title: __("配置报表列"),
        fields: [
            {
                fieldname: "columns",
                fieldtype: "Table",
                label: __("报表列"),
                fields: [
                    {
                        fieldname: "column_label",
                        fieldtype: "Data",
                        label: __("列标题")
                    },
                    {
                        fieldname: "column_type",
                        fieldtype: "Select",
                        label: __("列类型"),
                        options: [
                            "金额", "百分比", "增长率", "占比", "累计值", "分期值"
                        ]
                    },
                    {
                        fieldname: "comparison_type",
                        fieldtype: "Select",
                        label: __("对比类型"),
                        options: [
                            "", "同比", "环比", "预算对比"
                        ]
                    },
                    {
                        fieldname: "formula",
                        fieldtype: "Text",
                        label: __("计算公式")
                    }
                ]
            }
        ],
        primary_action: function(values) {
            // 保存配置
            frm.set_value("report_columns", JSON.stringify(values.columns));
            frm.save();
            dialog.hide();
            refresh_report();
        }
    });

    // 加载现有配置
    if (frm.doc.report_columns) {
        dialog.set_value("columns", JSON.parse(frm.doc.report_columns));
    }

    dialog.show();
}

// 3. 修改格式化函数,支持新的列类型
erpnext.financial_statements.formatter = function(value, row, column, data) {
    // 处理增长率列
    if (column.fieldtype === "增长率") {
        return value > 0 
            ? `<span class="text-green">+${value.toFixed(2)}%</span>` 
            : `<span class="text-red">${value.toFixed(2)}%</span>`;
    }

    // 处理占比列
    if (column.fieldtype === "占比") {
        return `${value.toFixed(2)}%`;
    }

    // 原有格式化逻辑...
    return value;
}

三、文件操作指南

1. 自定义应用开发模式(推荐)

# 1. 创建自定义应用
bench new-app custom_financial

# 2. 创建补丁文件,修改核心功能
bench generate-patch

# 3. 在custom_financial应用中创建覆盖文件
custom_financial/
├── accounts/
│   └── doctype/
│       └── financial_statements/
│           ├── financial_statements.py  # 覆盖核心文件
│           └── financial_statements.js  # 覆盖核心文件

# 4. 安装应用
bench --site your-site install-app custom_financial

# 5. 构建并部署
bench build
bench migrate

2. 直接修改核心文件(不推荐,升级时需重新修改)

# 1. 定位到ERPNext源代码目录
cd ~/frappe-bench/apps/erpnext/erpnext/accounts/doctype

# 2. 备份原始文件
cp financial_statements/financial_statements.py financial_statements/financial_statements.py.bak
cp financial_statements/financial_statements.js financial_statements/financial_statements.js.bak

# 3. 使用文本编辑器修改文件
nano financial_statements/financial_statements.py
nano financial_statements/financial_statements.js

# 4. 重启bench
bench restart

四、验证与测试

1. 验证Doctype创建

  • 登录ERPNext系统
  • 导航到"设置 > 文档类型"
  • 搜索"Report Column",确保成功创建

2. 测试报表功能

  • 创建新的报表格式配置
  • 选择"同比分析"或"预算对比"等列类型
  • 运行报表,检查数据是否正确计算和显示
  • 验证公式计算列是否按预期工作

3. 检查日志文件

# 查看应用日志,确保没有错误
tail -f ~/frappe-bench/logs/application.log

五、注意事项

  1. 系统升级:如果采用直接修改核心文件的方式,系统升级后可能需要重新应用这些更改
  2. 性能影响:复杂的公式计算和多期间对比可能影响报表生成速度,建议进行性能测试
  3. 数据安全:确保公式解析引擎的安全性,避免潜在的代码注入风险
  4. 备份:在进行任何代码修改前,务必备份整个bench环境

通过以上步骤,您可以成功实现报表格式的自定义管理和灵活取数功能。建议采用自定义应用开发模式,这样可以更好地管理代码变更并避免升级冲突。

Discard
Save
Review Changes ← Back to Content
Message Status Space Raised By Last update on