- 后端
- 企业应用
【免费下载链接】erpnext
Free and Open Source Enterprise Resource Planning (ERP)
Mode of Payment(付款方式)是 ERPNext 中用于统管"客户/供应商以何种方式付款"的基础主数据(Master),Cash、Credit Card、Bank Transfer、Check 等常见付款方式都以它为单位进行维护。本文以 erpnext/accounts/doctype/mode_of_payment/README.md 为骨架,结合仓库内该 DocType 的字段定义、校验逻辑、测试用例及 POS、付款单、日记账等下游集成代码,完整讲解付款方式的配置方法、默认账户映射机制与底层校验原理,帮助你正确搭建一套可复用的收款/付款主数据体系。
一、什么是 Mode of Payment:一份"付款方式主数据"
在 ERPNext 的会计模块(Accounts)中,付款方式不是一个散落在各单据里的下拉选项,而是一张独立的主数据表。官方 README 的一句话定义是:
Master for modes of payment.(付款方式主数据)
并给出了四个典型示例:
- Cash(现金)
- Credit Card(信用卡)
- Bank Transfer(银行转账)
- Check(支票)
从源码角度看,它是一个标准的 Frappe DocType,定义于 mode_of_payment.json:
"doctype": "DocType"、"document_type": "Setup":属于设置(Setup)类文档,而不是会提交(Submit)的业务单据;"autoname": "field:mode_of_payment":文档名称直接取自"Mode of Payment"字段本身,因此名称天然唯一;"icon": "wallet"、"quick_entry": 1:支持快速新建入口;"index_web_pages_for_search": 1、"show_name_in_global_search": 1、"translated_doctype": 1:支持全局搜索、网页索引与翻译。
可以这样理解它的定位:付款方式是"付款手段"的字典表,它本身不做账,但为后续所有涉及收付款的单据(POS 发票、付款单、日记账、银行对账等)提供"用哪种方式收/付"以及"这笔钱进哪个账户"的标准答案。
二、字段详解:Mode of Payment 的四个核心字段
根据 mode_of_payment.json 的field_order(mode_of_payment→enabled→type→accounts),主文档共四个字段:
| 字段名 | 字段类型 | 必填/唯一 | 取值与说明 |
|---|---|---|---|
mode_of_payment | Data(单行文本) | reqd: 1、unique: 1 | 付款方式名称,如 "Cash";同时作为文档自动命名来源,全局唯一 |
type | Select(下拉) | 否 | 取值固定为Cash、Bank、General、Phone四类,用于对付款方式进行归类(如现金类、银行类、通用类、手机支付类) |
enabled | Check(勾选) | 否(默认"1") | 是否启用;置为未勾选即停用该付款方式 |
accounts | Table(子表) | 否 | 指向子表 "Mode of Payment Account",用于按公司配置默认入账账户 |
其中type字段在 JSON 中以"options": "Cash\nBank\nGeneral\nPhone"定义,in_standard_filter: 1表明它可直接作为列表页的标准筛选条件。字段mode_of_payment同时设置了in_list_view: 1,保证列表视图直接展示名称。
Python 侧的类型定义见 mode_of_payment.py:
accounts: DF.Table[ModeofPaymentAccount] enabled: DF.Check mode_of_payment: DF.Data type: DF.Literal["Cash", "Bank", "General", "Phone"]三、账户映射:Mode of Payment Account 子表与默认账户
付款方式真正的"记账落点"在子表Mode of Payment Account。该子表定义于 mode_of_payment_account.json,仅含两个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
company | Link → Company | 该映射所属公司 |
default_account | Link → Account | 该公司下的默认账户(现金/银行/应收类) |
子表字段描述明确写道:"Default account will be automatically updated in POS Invoice when this mode is selected."(选择该付款方式时,默认账户会自动更新到 POS 发票中)。这正是整个映射机制的核心用途:一个付款方式可以在不同公司下对应不同账户。
为保证default_account选得合法,前端 mode_of_payment.js 为子表账户设置了查询过滤器:
frm.set_query("default_account", "accounts", function (doc, cdt, cdn) { let d = locals[cdt][cdn]; return { filters: [ ["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]], ["Account", "is_group", "=", 0], ["Account", "company", "=", d.company], ], }; });即只能从银行(Bank)、现金(Cash)、应收(Receivable)三类非分组账户中挑选,且账户所属公司必须与子表行中的公司一致,从源头避免"选了个别的公司账户"这类低级错误。
账户映射的消费方:get_bank_cash_account
下游单据拿到mode_of_payment后,如何反查默认账户?核心实现在 erpnext/accounts/doctype/sales_invoice/services/pos.py:
def get_bank_cash_account(mode_of_payment: str, company: str) -> dict: account = frappe.db.get_value( "Mode of Payment Account", {"parent": mode_of_payment, "company": company}, "default_account", ) if not account: frappe.throw( _("Please set default Cash or Bank account in Mode of Payment {0}").format( get_link_to_form("Mode of Payment", mode_of_payment) ), title=_("Missing Account"), ) return {"account": account}逻辑要点:
- 以
(mode_of_payment, company)为键精确查子表default_account; - 查不到即抛错:"Please set default Cash or Bank account in Mode of Payment ...",强制用户把映射补齐,防止单据带着空账户过账。
同样的查询在 journal_entry.py 中也被复用:日记账通过get_bank_cash_account(mode_of_payment, company)获取账户后写入分录。
四、保存时的三层校验逻辑
付款方式保存(validate)时,mode_of_payment.py 依次执行三个校验:
def validate(self): self.validate_accounts() self.validate_repeating_companies() self.validate_pos_mode_of_payment()1. 账户与公司一致性校验(validate_accounts)
逐行检查子表default_account所属公司是否与行内company一致(mode_of_payment.py):
if frappe.get_cached_value("Account", entry.default_account, "company") != entry.company: frappe.throw( _("Account {0} does not match with Company {1} in Mode of Account: {2}").format(...) )即使前端过滤器被绕过,后端保存时仍会二次校验,防止脏数据入库。
2. 公司重复校验(validate_repeating_companies)
同一付款方式下,同一公司只允许出现一次(mode_of_payment.py):
if len(accounts_list) != len(set(accounts_list)): frappe.throw(_("Same Company is entered more than once"))3. POS 引用保护(validate_pos_mode_of_payment)
当勾掉enabled停用某付款方式时,会先检查它是否仍被 POS Profile 引用(mode_of_payment.py):
pos_profiles = frappe.get_all( "Sales Invoice Payment", filters={"parenttype": "POS Profile", "mode_of_payment": self.name}, pluck="parent", ) if pos_profiles: frappe.throw(message, title=_("Not Allowed"))若仍被引用,则拒绝停用并提示"POS Profile {0} contains Mode of Payment {1}...Please remove them to disable this mode."
测试用例对校验逻辑的佐证
test_mode_of_payment.py 通过ERPNextTestSuite覆盖了上述行为:
test_valid_mode_of_payment_saves:为_Test Company配置 Cash 账户后可正常保存;test_account_of_wrong_company_throws:把另一公司的账户挂进来会抛出frappe.ValidationError;test_repeating_company_throws:同一公司出现两行会触发校验异常;test_disabling_unreferenced_mode_succeeds:未被引用的付款方式可正常停用。
值得留意的是test_disabling_mode_referenced_by_pos_profile_is_not_blocked,测试注释明确指出一个疑似缺陷:validate_pos_mode_of_payment查询的是parenttype == "POS Profile"的Sales Invoice Payment行,但 POS Profile 的付款方式实际存放在POS Payment Method子表中,过滤器永远不会命中,导致"引用保护"实际失效——测试刻意锁定了这一当前行为,以便将来修复该守卫时能被测试捕获。如果你在实施时依赖"POS 引用中的付款方式不可停用"这一保护,请先核实该缺陷在目标版本中的修复状态。
五、与业务单据的集成:POS、付款单与日记账
5.1 POS 发票 / 销售发票
付款方式与 POS 的联系最为紧密。销售发票通过POSService把付款方式映射成支付行(sales_invoice.py):
set_account_for_mode_of_payment:将支付行中mode_of_payment对应的get_bank_cash_account(...)账户写入payment.account(见 services/pos.py);update_multi_mode_option:按 POS Profile 中的支付方式(POS Payment Method行)重建发票的payments子表,逐行填充default、mode_of_payment、account、type;若某个付款方式未配置默认账户,会报错 "Please set default Cash or Bank account in Mode of Payment ..."(services/pos.py);clear_unallocated_mode_of_payments:清除金额为 0 的支付行,避免空行干扰过账(services/pos.py)。
5.2 付款单(Payment Entry)
付款单上直接带有mode_of_payment链接字段(见 payment_entry.json)。前端在切换付款方式时调用erpnext.accounts.pos.get_payment_mode_account自动带出账户(payment_entry.js);后端创建付款单时也会把来源单据的mode_of_payment透传过去(payment_entry.py)。
5.3 银行对账与收银结算
- 银行对账工具(Bank Reconciliation Tool):创建付款单时支持直接指定
mode_of_payment(bank_reconciliation_tool.py),银行交易按付款方式归类; - 收银结算(Cashier Closing):其
payments子表同样引用付款方式(cashier_closing_payments.py),测试中也以{"mode_of_payment": "Cash", "amount": ...}形式构造数据。
六、权限模型与角色可见性
mode_of_payment.json 定义了清晰的分层权限:
| 角色 | 权限 |
|---|---|
| Accounts Manager | 全部:create / read / write / share / print / report / email |
| Accounts User | 只读 + report |
| HR User / HR Manager / Maintenance Manager / Maintenance User / Purchase Manager / Purchase User / Sales Manager / Sales User | 仅select(可用于下拉引用,不能编辑) |
也就是说,付款方式主数据默认只有会计管理员能增删改,其余业务角色仅能引用,这符合主数据"集中维护、全局引用"的管理惯例。
七、编程式维护:set_default_account_for_mode_of_payment
在集成或测试场景下,可通过测试模块导出的辅助函数以代码方式维护默认账户(test_mode_of_payment.py):
def set_default_account_for_mode_of_payment(mode_of_payment, company, account): mode_of_payment.reload() if frappe.db.exists( "Mode of Payment Account", {"parent": mode_of_payment.mode_of_payment, "company": company} ): frappe.db.set_value( "Mode of Payment Account", {"parent": mode_of_payment.mode_of_payment, "company": company}, "default_account", account, ) return mode_of_payment.append("accounts", {"company": company, "default_account": account}) mode_of_payment.save()该函数遵循"已存在则更新、不存在则追加并保存"的幂等语义,被 bank_clearance/test_bank_clearance.py 与 bank_transaction/test_bank_transaction.py 等测试直接复用,可作为二次开发时维护付款方式映射的参考范本。
八、实操建议与配置清单
基于以上源码事实,配置一套可用的付款方式体系建议按以下步骤进行:
- 创建主数据:新建 Mode of Payment,命名建议保持简短唯一(Cash、Bank Transfer、Check、UPI 等),
autoname会直接取字段值作为文档名; - 归类:按
type选择 Cash / Bank / General / Phone,便于列表筛选与报表归类; - 按公司映射账户:在 Accounts 子表逐公司添加
default_account(限 Bank / Cash / Receivable 类非分组账户,且公司必须匹配); - 启用状态:默认
enabled = 1;停用前先确认没有 POS Profile / 收银结算仍在引用,并留意上文提到的 POS 引用保护可能存在的已知缺陷; - 权限控制:仅授予 Accounts Manager 写权限,其他业务角色保持只读/引用权限。
完成以上配置后,POS 发票、付款单、日记账、银行对账工具即可统一通过mode_of_payment自动落账到正确的现金/银行账户,实现"付款方式一处维护、全链路复用"。
- 后端
- 企业应用
【免费下载链接】erpnext
Free and Open Source Enterprise Resource Planning (ERP)
相关推荐
RegClient完全指南:一站式掌握Docker与OCI Registry客户端开发
RegClient完全指南:一站式掌握Docker与OCI Registry客户端开发 RegClient是一个用Go语言开发的Docker和OCI Regis
云原生镜像仓库开发工具CLINocoBase 主数据源接入 PostgreSQL:安装配置、版本要求与字段类型映射完全指南
NocoBase 主数据源接入 PostgreSQL:安装配置、版本要求与字段类型映射完全指南 导读 本文讲解如何将 PostgreSQL 配置为 NocoBa
低代码后端前端人工智能AI 应用工作流自动化Keystone 6 Timestamp 字段完全指南:配置选项、GraphQL 行为与数据库映射
Keystone 6 Timestamp 字段完全指南:配置选项、GraphQL 行为与数据库映射 导读 timestamp 是 Keystone 6 内置的标
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考