- 后端
- 企业应用
【免费下载链接】erpnext
Free and Open Source Enterprise Resource Planning (ERP)
导读
本文基于 ERPNext 开源仓库的 v6.3.0 版本变更日志,系统讲解该版本引入的Tax Rule(税务规则)自动匹配机制:如何根据客户、供应商、账单地址、送货地址等条件自动挑选合适的税模板;同时剖析本次针对Shopping Cart(购物车)的重大重构——单一价目表定价、按国家的运费规则、以及基于 Tax Rule 的税费计算。读完本文,你将掌握 Tax Rule 的字段语义、匹配优先级算法与冲突规则,能够独立完成升级后的税务与购物车配置核对。
一、v6.3.0 升级内容总览
该变更日志虽然简短,但涵盖了三块彼此关联的能力建设:
- 新增 Tax Rule 文档类型:以规则化方式自动选择税模板,替代此前基于 Territory(地域)的粗粒度税务匹配;
- Shopping Cart 定价、运费与税费体系重构:单一价目表 + 按国家运费 + 规则化税费;
- 体验类增强:Customer Portal(客户门户)界面优化,以及 Sales Order、Sales Invoice、Delivery Note 提交后销售团队信息可再次编辑。
其中 Tax Rule 与 Shopping Cart 重构是本次升级的技术核心,下文分别展开。
二、Tax Rule:自动选择税模板的规则引擎
2.1 设计目标
在 v6.3.0 之前,ERPNext 的税费模板通常依赖 Territory 等全局维度进行分配,无法满足"不同客户、不同地址适用不同税率"的精细业务场景。Tax Rule 的出现,把"什么样的单据适用哪套税模板"抽象为一组可叠加的过滤条件,让税费选择变为一条条显式可维护的规则。
该 DocType 定义于 tax_rule.json,位于erpnext/accounts/doctype/tax_rule/目录,核心逻辑实现在 tax_rule.py。
2.2 规则字段全景
Tax Rule 表单由「Tax Type + Tax Template」「Filters 过滤条件」「Validity 有效期」「Priority 优先级」几大区块构成,字段语义如下表:
| 区块 | 字段 | 类型 | 说明 |
|---|---|---|---|
| 基础 | tax_type | Select(Sales / Purchase) | 该规则作用于销售还是采购,默认 Sales |
| 基础 | sales_tax_template | Link | 匹配成功时套用的销售税模板(仅 Sales 显示) |
| 基础 | purchase_tax_template | Link | 匹配成功时套用的采购税模板(仅 Purchase 显示) |
| 基础 | use_for_shopping_cart | Check(默认勾选) | 是否将该规则纳入购物车税费计算 |
| 过滤 | customer/supplier | Link | 指定具体客户或供应商 |
| 过滤 | customer_group/supplier_group | Link | 指定客户组 / 供应商组,支持层级祖先匹配 |
| 过滤 | item/item_group | Link | 指定商品或商品组 |
| 过滤 | billing_city/county/state/zipcode/country | Data / Link | 账单地址维度 |
| 过滤 | shipping_city/county/state/zipcode/country | Data / Link | 送货地址维度 |
| 过滤 | tax_category | Link | 税务分类,常用于同客户不同产品类别的税率区分 |
| 过滤 | company | Link | 限定公司 |
| 有效期 | from_date/to_date | Date | 规则生效起止日期,可留空表示不限 |
| 优先级 | priority | Int(默认 1) | 同条件下多条规则命中时的决胜依据 |
从源码中的validate()方法(tax_rule.py)可以看到一个关键约束:当tax_type == "Sales"时,采购侧字段(purchase_tax_template、supplier、supplier_group)会被强制清空;反之亦然,且必须至少指定一个税模板,否则抛出 "Tax Template is mandatory."。
2.3 匹配算法:先比精确度,再比优先级
get_tax_template(posting_date, args)(tax_rule.py)是整条规则链路的执行入口,其匹配流程可分四步:
第一步:日期过滤。传入单据的posting_date落在规则的from_date~to_date区间内才参与候选;若未指定日期,则只匹配不设有效期的规则。
第二步:条件过滤。将单据上下文(客户、地址字段、税类等)逐一与规则比对,其中有两类特殊处理:
customer_group/supplier_group支持祖先链匹配——调用get_parent_customer_groups/get_parent_supplier_groups展开所选组的全部父级,若规则设置的是父组(例如 "All Customer Groups"),子组单据同样命中;- 未设置的过滤字段按空值等价处理,实现"未指定即通配"。
第三步:统计命中键数。对每条候选规则统计其与单据上下文中"显式设置且匹配成功"的条件个数(no_of_keys_matched)。
第四步:排序决胜。按no_of_keys_matched降序、再按priority降序取第一条作为最终规则,代码如下:
rule = sorted( tax_rule, key=functools.cmp_to_key( lambda b, a: cmp(a.no_of_keys_matched, b.no_of_keys_matched) or cmp(a.priority, b.priority) ), )[0]这套算法保证了"条件更具体(命中键更多)的规则优先于宽松规则,同为具体规则时优先级数字更大者胜出"。测试 test_tax_rule.py 中test_select_tax_rule_based_on_better_match、test_select_tax_rule_based_on_better_priority、test_select_tax_rule_based_cross_partially_keys等用例正是围绕该排序规则设计的:例如同为客户 + 城市命中时,命中两键的规则胜过命中一键的规则;键数相同时 priority=2 胜过 priority=1。
2.4 冲突检测与保存校验
为防止规则库失控,保存时validate_filters()(tax_rule.py)会做两项检查:
- 同型冲突:若存在一条过滤条件完全相同、且优先级相同的既有规则,则抛出
ConflictingTaxRule; - 日期区间重叠:新旧规则的生效区间存在交叠时同样视为冲突,避免同一时间窗内出现歧义。
测试用例test_conflict、test_conflict_with_overlapping_dates与test_conflict_with_non_overlapping_dates(test_tax_rule.py)分别验证了这三种场景:完全相同规则被拦截、日期交叠被拦截、而日期完全不交叠的同条件规则可以并存。
2.5 单据侧的调用链
Tax Rule 在真实单据流程中由两条链路驱动:
- 服务端自动应用:party.py 中通过
get_party_details拉取客户/供应商的默认账单、送货地址并填入地址维度,随后调用get_tax_template(posting_date, args)返回税模板名称,由控制器套用到单据的taxes_and_charges字段; - 前端联查:交易单据客户端在 transaction.js 通过
erpnext.controllers.queries.get_tax_template(定义于 queries.py)发起联查,实时预览将套用的税模板。
端到端验证可参考test_taxes_fetch_via_tax_rule(test_tax_rule.py):先为客户建立 Opportunity,再生成 Quotation,断言单据的taxes_and_charges自动等于规则指定的模板,且税行(quotation.taxes)已同步拉取——这证明 Tax Rule 不仅能选模板,还会连带拉取税率明细。
三、Shopping Cart 重构:单一价目表、按国家运费与规则化税费
v6.3.0 对购物车模块做了三处结构性调整,变更日志原文明确了三点:
3.1 单一价目表,单一货币
"The prices will be based on only a single Price List defined in Shopping Cart Settings. Essentially, it means that your Shopping Cart will be available only in a single currency."
购物车价格只依据Shopping Cart Settings中指定的唯一一个价目表(Price List)计算,购物车因此只支持单一货币。这大幅简化了购物车定价逻辑——不再需要跨价目表、跨币种合并报价,但也意味着如果你的价目表体系是按多币种组织的,需要先确认购物车目标市场对应的价目表,再做切换。
3.2 运费规则按国家定义,替代 Territory
"Shipping Rule will be defined per Country, instead of Territory."
运费规则的适用范围由 Territory 改为Country。从源码可印证这一点:shipping_rule.py 中的validate_countries()会读取单据送货地址的country字段,与规则的countries子表(Shipping Rule Country)逐项比对:
- 若送货地址缺少国家,直接报错 "Shipping Address does not have country, which is required for this Shipping Rule";
- 若国家不在规则列表内,报错 "Shipping rule not applicable for country {0} in Shipping Address"。
而运费金额本身仍由calculate_based_on(Net Total / Net Weight / Fixed)+ 条件区间表决定(get_shipping_amount_from_rules按 From/To 值区间取运费),最终经add_shipping_rule_to_tax_table以 "Actual" 税行形式写入单据税费表。因此升级时需要把每个 Territory 维度的旧运费规则,逐一改造成国家维度,并确认所有 Address 都维护了country字段。
3.3 税费基于 Tax Rule,替代 Territory
"Taxes will be applied based on the new Tax Rule system, instead of Territory."
这正是本文第二部分 Tax Rule 引擎的落地场景:购物车结算时,通过get_tax_template按客户、账单/送货地址、税类等条件动态确定税模板。use_for_shopping_cart字段(tax_rule.json 中默认值 1)专门用于将规则纳入购物车计算;从get_tax_template源码可见,当请求上下文携带use_for_shopping_cart=1时,查询会强制过滤出该标志位为真的规则,避免购物车误用为后台销售设计的规则。
测试 test_tax_rule.py 的test_use_for_shopping_cart_filter与test_use_for_shopping_cart_default验证了这一行为:购物车请求(带use_for_shopping_cart=1)只会命中购物车规则;普通请求不带该键则不施加此过滤。
3.4 升级必读:Shopping Cart Settings 已被禁用
Important Note:Your Shopping Cart Settings have been disabled. The new changes require you to review your Price List, Tax Rules and Shipping Rule, update the settings, and then enable Shopping Cart again.
这是本次升级最关键的运维动作:升级后购物车不会自动恢复工作。由于定价、运费、税费的底层维度全部变更,旧配置已不兼容,系统会默认禁用 Shopping Cart Settings。你必须按以下顺序完成核对后才能重新启用购物车:
- 价目表:确认 Shopping Cart Settings 指向的单一 Price List 及其币种正确;
- 税务规则:为购物车适用场景建立带
Use for Shopping Cart勾选的 Tax Rule,并检查规则冲突(保存时系统会自动校验); - 运费规则:将旧 Territory 规则迁移为按国家定义的 Shipping Rule,并核实送货地址的国家字段完整性;
- 以上确认无误后,重新启用 Shopping Cart Settings。
四、Customer Portal 界面增强
变更日志同步提到 "Enhancements in Customer Portal user interface"。客户门户是面向终端客户的自助界面,本次升级对其用户交互做了整体打磨。由于变更日志未给出具体条目,实际视觉与交互细节建议以升级后的实际界面为准;该模块相关实现可结合仓库中erpnext/portal/目录(portal/utils.py 及 portal/doctype)进一步追踪。
五、Sales Team 提交后可编辑
"Sales Team is now editable after submission of Sales Order, Sales Invoice and Delivery Note"
在此之前,单据提交(Submitted)后销售团队信息通常被冻结;v6.3.0 起,Sales Order、Sales Invoice、Delivery Note 提交后仍允许调整销售团队分配。从当前源码仍可看到该设计延续至今:例如 sales_invoice.py 中Sales Invoice文档定义了sales_team: DF.Table[SalesTeam]子表并支持后续编辑。这一改动对销售提成核算、事后纠正团队归属等场景非常实用,且不要求解锁或取消提交即可完成修正。
六、升级核对清单速查
综合全文,从 v6.3.0 升级或复现该版本能力时,建议按此清单逐项核对:
- Tax Rule:建立销售/采购规则,明确过滤条件(客户/地址/税类/商品)、有效期与优先级;
- 规则冲突:利用保存校验(同条件同优先级、日期交叠)确保规则库无歧义;
- 购物车税规则:为购物车专用规则勾选
Use for Shopping Cart; - 购物车价目表:确认 Shopping Cart Settings 的单一 Price List 与币种;
- 运费规则:将 Territory 维度迁移为 Country 维度,补全 Address 的
country; - 重新启用购物车:完成以上核对后再打开 Shopping Cart Settings;
- 销售团队:在提交后的销售单据上验证销售团队字段可编辑。
参考源码路径
- 变更日志原文:erpnext/change_log/v6/v6_3_0.md
- Tax Rule 实现:erpnext/accounts/doctype/tax_rule/tax_rule.py
- Tax Rule 表单定义:erpnext/accounts/doctype/tax_rule/tax_rule.json
- Tax Rule 测试用例:erpnext/accounts/doctype/tax_rule/test_tax_rule.py
- 单据侧税务应用:erpnext/accounts/party.py
- 前端税模板联查:erpnext/public/js/controllers/transaction.js、erpnext/controllers/queries.py
- 按国家运费规则:erpnext/accounts/doctype/shipping_rule/shipping_rule.py、erpnext/accounts/doctype/shipping_rule_country
- 销售团队子表:erpnext/accounts/doctype/sales_invoice/sales_invoice.py
- 后端
- 企业应用
【免费下载链接】erpnext
Free and Open Source Enterprise Resource Planning (ERP)
相关推荐
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考