人财事物信息化 - cost_center.py
- 一段话总结:该代码定义了
CostCenter
类,用于管理ERPNext系统中的成本中心。它继承自NestedSet
,具备自动命名、必填项和父成本中心验证等功能。支持成本中心在组和分类账之间转换,转换时会检查是否存在子节点、交易记录或分配记录。在重命名前后也有相应的处理逻辑,并在on_doctype_update
方法中为Cost Center
添加索引。 - 思维导图
## **CostCenter类定义**
- 继承NestedSet类
- 包含多个属性,如company、cost_center_name等
## **自动命名功能**
- 使用get_autoname_with_number函数
- 依据成本中心编号、名称和公司生成名称
## **验证方法**
- 验证必填项,检查父成本中心设置
- 验证父成本中心是否为组节点
## **转换功能**
- 组转分类账,检查子节点和交易记录
- 分类账转组,检查分配和交易记录
## **重命名处理**
- 重命名前添加公司缩写并验证
- 重命名后更新编号和名称
## **索引添加**
- 在on_doctype_update方法中执行
- 为Cost Center添加lft和rgt索引
- 详细总结
cost_center.py
文件定义了CostCenter
类,用于管理ERPNext系统中的成本中心,以下是详细总结:
- 类定义与属性:
CostCenter
类继承自NestedSet
,用于处理具有层级关系的成本中心数据。它包含多个属性,用于描述成本中心的相关信息,具体如下:属性名 类型 描述 company
DF.Link
关联的公司,通过链接指向公司文档 cost_center_name
DF.Data
成本中心名称 cost_center_number
DF.Data | None
成本中心编号,可为空 disabled
DF.Check
表示成本中心是否禁用,布尔类型 is_group
DF.Check
标记成本中心是否为组,布尔类型 lft
DF.Int
用于嵌套集模型,存储左值 old_parent
DF.Link | None
旧的父成本中心链接,可为空 parent_cost_center
DF.Link
父成本中心链接 rgt
DF.Int
用于嵌套集模型,存储右值 - 自动命名:
autoname
方法通过调用get_autoname_with_number
函数,依据cost_center_number
、cost_center_name
和company
生成成本中心的名称。 - 验证方法
validate_mandatory
:验证必填项,当cost_center_name
不等于company
且未设置parent_cost_center
时,或cost_center_name
等于company
却设置了parent_cost_center
时,抛出异常提示。validate_parent_cost_center
:验证所选的parent_cost_center
是否为组节点,若不是则抛出异常。
- 转换功能
convert_group_to_ledger
:将成本中心组转换为分类账,转换前会检查是否存在子节点(通过check_if_child_exists
)和交易记录(通过check_gle_exists
),若存在则抛出异常,否则将is_group
设为0并保存。convert_ledger_to_group
:将分类账转换为成本中心组,转换前会检查是否存在分配记录(通过if_allocation_exists_against_cost_center
和check_if_part_of_cost_center_allocation
)和交易记录(通过check_gle_exists
),若存在则抛出异常,否则将is_group
设为1并保存。
- 重命名处理
before_rename
:重命名前,添加公司缩写(通过get_name_with_abbr
),验证合并属性,若不合并则调用get_name_with_number
处理新名称。after_rename
:重命名后,更新成本中心编号和名称,确保编号和名称与新名称一致。
- 索引添加:
on_doctype_update
方法为Cost Center
添加["lft", "rgt"]
索引,以优化嵌套集相关操作的性能。
- 关键问题
- 问题1:成本中心组转换为分类账时,为什么要检查是否存在子节点?
- 答案:因为成本中心组若存在子节点,将其转换为分类账会破坏成本中心的层级结构和数据完整性,导致子节点失去正确的归属关系,所以在转换前需要检查并禁止这种操作。
- 问题2:
validate_mandatory
方法中两个条件判断的依据是什么?- 答案:当
cost_center_name
不等于company
时,意味着不是根成本中心,此时必须设置parent_cost_center
来确定其层级关系;而当cost_center_name
等于company
时,代表这是根成本中心,根成本中心不能有父成本中心,否则会破坏成本中心的根节点定义和层级逻辑。
- 答案:当
- 问题3:
after_rename
方法中更新成本中心编号和名称的意义是什么?- 答案:更新成本中心编号和名称是为了确保成本中心的编号和名称与重命名后的实际情况保持一致,保证系统中成本中心信息的准确性和一致性,避免因编号或名称不一致导致数据混乱,影响后续的成本核算、报表生成等相关业务操作。 ---
- 问题1:成本中心组转换为分类账时,为什么要检查是否存在子节点?
- 一段话总结:该Python文件定义了
CostCenter
类,继承自Document
,用于管理成本中心。其中validate
方法在保存成本中心信息时,会验证父成本中心是否存在循环引用,若有则抛出错误;同时会验证父成本中心是否已禁用,若禁用则抛出错误。还会调用update_nsm_model
更新嵌套集模型。 - 思维导图
## **CostCenter类**
- 继承自Document
- 用于管理成本中心
## **validate方法**
- 验证父成本中心循环引用
- 验证父成本中心是否禁用
- 调用update_nsm_model更新嵌套集模型
- 详细总结
此Python文件围绕
CostCenter
类展开,主要用于成本中心的管理。- 类定义:
CostCenter
类继承自Document
,用于管理成本中心的相关信息。 - 关键方法
方法名 功能 业务逻辑 validate
保存成本中心信息时的验证 1. 调用 validate_parent_cost_center
验证父成本中心是否存在循环引用,若有则抛出frappe.exceptions.CyclicReferenceError
错误。
2. 调用validate_disabled_parent_cost_center
验证父成本中心是否已禁用,若禁用则抛出错误提示。
3. 调用update_nsm_model
更新嵌套集模型。validate_parent_cost_center
验证父成本中心循环引用 使用 frappe.utils.validate_parent_field
函数验证父成本中心是否存在循环引用。validate_disabled_parent_cost_center
验证父成本中心是否禁用 若父成本中心存在且已禁用,通过 frappe.throw
抛出错误提示“Parent Cost Center {parentcostcenter} is disabled. Please select another Parent Cost Center”。on_update
更新成本中心信息时调用 调用 update_nsm_model
更新嵌套集模型。on_trash
删除成本中心信息时调用 调用 update_nsm_model
更新嵌套集模型。
- 类定义:
- 关键问题
- 问题1:
validate
方法在保存成本中心信息时主要做了哪些验证?- 答案:主要做了两方面验证。一是调用
validate_parent_cost_center
验证父成本中心是否存在循环引用,若存在则抛出frappe.exceptions.CyclicReferenceError
错误;二是调用validate_disabled_parent_cost_center
验证父成本中心是否已禁用,若禁用则抛出相应错误提示。
- 答案:主要做了两方面验证。一是调用
- 问题2:当父成本中心已禁用时,会有什么提示?
- 答案:当父成本中心已禁用时,会通过
frappe.throw
抛出错误提示“Parent Cost Center {parentcostcenter} is disabled. Please select another Parent Cost Center”。
- 答案:当父成本中心已禁用时,会通过
- 问题3:在更新和删除成本中心信息时,都会调用哪个方法?
- 答案:在更新和删除成本中心信息时,都会调用
update_nsm_model
方法来更新嵌套集模型。
- 答案:在更新和删除成本中心信息时,都会调用
- 问题1:
要在现有代码基础上增加允许跨公司对成本中心设立虚拟组的属性,需要对CostCenter
类进行以下几方面的修改:
1. 在类属性中添加新属性。
2. 在验证方法中考虑新属性的逻辑。
3. 根据需求调整其他相关方法。
以下是修改后的代码示例:
import frappe
from frappe import _
from frappe.utils.nestedset import NestedSet
from erpnext.accounts.utils import validate_field_number
class CostCenter(NestedSet):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link
cost_center_name: DF.Data
cost_center_number: DF.Data | None
disabled: DF.Check
is_group: DF.Check
lft: DF.Int
old_parent: DF.Link | None
parent_cost_center: DF.Link
rgt: DF.Int
# 新增属性:允许跨公司设立虚拟组
allow_cross_company_virtual_group: DF.Check
# end: auto-generated types
nsm_parent_field = "parent_cost_center"
def autoname(self):
from erpnext.accounts.utils import get_autoname_with_number
self.name = get_autoname_with_number(self.cost_center_number, self.cost_center_name, self.company)
def validate(self):
self.validate_mandatory()
self.validate_parent_cost_center()
# 新增对新属性的验证逻辑
self.validate_cross_company_virtual_group()
def validate_mandatory(self):
if self.cost_center_name != self.company and not self.parent_cost_center:
frappe.throw(_("Please enter parent cost center"))
elif self.cost_center_name == self.company and self.parent_cost_center:
frappe.throw(_("Root cannot have a parent cost center"))
def validate_parent_cost_center(self):
if self.parent_cost_center:
if not frappe.db.get_value("Cost Center", self.parent_cost_center, "is_group"):
frappe.throw(
_("{0} is not a group node. Please select a group node as parent cost center").format(
frappe.bold(self.parent_cost_center)
)
)
# 新增验证方法:检查跨公司虚拟组的设置是否合理
def validate_cross_company_virtual_group(self):
if self.allow_cross_company_virtual_group:
# 这里可以添加更具体的验证逻辑,例如检查相关权限等
if not self.is_group:
frappe.throw(_("Only group cost centers can be set as cross-company virtual groups."))
@frappe.whitelist()
def convert_group_to_ledger(self):
if self.check_if_child_exists():
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
elif self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to ledger"))
else:
self.is_group = 0
self.save()
return 1
@frappe.whitelist()
def convert_ledger_to_group(self):
if self.if_allocation_exists_against_cost_center():
frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
if self.check_if_part_of_cost_center_allocation():
frappe.throw(
_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group")
)
if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1
self.save()
return 1
def check_gle_exists(self):
return frappe.db.get_value("GL Entry", {"cost_center": self.name})
def check_if_child_exists(self):
return frappe.db.sql(
"select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2",
self.name,
)
def if_allocation_exists_against_cost_center(self):
return frappe.db.get_value(
"Cost Center Allocation", filters={"main_cost_center": self.name, "docstatus": 1}
)
def check_if_part_of_cost_center_allocation(self):
return frappe.db.get_value(
"Cost Center Allocation Percentage", filters={"cost_center": self.name, "docstatus": 1}
)
def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr
new_cost_center = get_name_with_abbr(newdn, self.company)
# Validate properties before merging
super().before_rename(olddn, new_cost_center, merge, "is_group")
if not merge:
new_cost_center = get_name_with_number(new_cost_center, self.cost_center_number)
return new_cost_center
def after_rename(self, olddn, newdn, merge=False):
super().after_rename(olddn, newdn, merge)
if not merge:
new_cost_center = frappe.db.get_value(
"Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1
)
# exclude company abbr
new_parts = newdn.split(" - ")[:-1]
# update cost center number and remove from parts
if new_parts[0][0].isdigit():
if len(new_parts) == 1:
new_parts = newdn.split(" ")
if new_cost_center.cost_center_number != new_parts[0]:
validate_field_number(
"Cost Center", self.name, new_parts[0], self.company, "cost_center_number"
)
self.cost_center_number = new_parts[0]
self.db_set("cost_center_number", new_parts[0])
new_parts = new_parts[1:]
# update cost center name
cost_center_name = " - ".join(new_parts)
if new_cost_center.cost_center_name != cost_center_name:
self.cost_center_name = cost_center_name
self.db_set("cost_center_name", cost_center_name)
def on_doctype_update():
frappe.db.add_index("Cost Center", ["lft", "rgt"])
def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account
return new_account
修改说明:
- 属性添加:在
CostCenter
类的类型提示部分添加了allow_cross_company_virtual_group: DF.Check
属性,用于表示是否允许该成本中心作为跨公司的虚拟组。 - 验证方法添加:新增了
validate_cross_company_virtual_group
方法,在validate
方法中调用。该方法目前检查当allow_cross_company_virtual_group
为True
时,成本中心必须是组类型,否则抛出错误。你可以根据实际业务需求进一步扩展此方法,例如检查用户权限、确保跨公司操作的合规性等。 - 其他方法调整:如果后续有涉及到跨公司虚拟组的操作,如转换组和分类账、重命名等,可能需要在相应方法中添加对新属性的处理逻辑。当前代码在已有方法基础上,主要确保新属性在保存时能得到合理验证。 4.
为了实现“允许跨公司对成本中心设立虚拟组”这一功能,需要从多个方面进行考虑,包括数据结构的修改、验证逻辑的添加、相关业务操作的调整等。下面详细阐述具体的实现逻辑:
1. 数据结构修改
在CostCenter
类中添加新的属性allow_cross_company_virtual_group
,用于标记该成本中心是否允许作为跨公司的虚拟组。在类型提示部分添加该属性的定义,使其在代码编写时能得到类型检查支持。
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
# 新增属性:允许跨公司设立虚拟组
allow_cross_company_virtual_group: DF.Check
2. 验证逻辑添加
在保存成本中心信息时,需要对allow_cross_company_virtual_group
属性进行验证,确保其设置符合业务规则。
2.1 基本验证:只有组类型的成本中心才能设置为跨公司虚拟组
在validate
方法中添加对新属性的验证逻辑,调用validate_cross_company_virtual_group
方法进行具体验证。
def validate(self):
self.validate_mandatory()
self.validate_parent_cost_center()
# 新增对新属性的验证逻辑
self.validate_cross_company_virtual_group()
def validate_cross_company_virtual_group(self):
if self.allow_cross_company_virtual_group:
if not self.is_group:
frappe.throw(_("Only group cost centers can be set as cross-company virtual groups."))
2.2 权限验证(可选)
除了基本的类型验证,还可以添加权限验证逻辑,确保只有具有相应权限的用户才能设置成本中心为跨公司虚拟组。
def validate_cross_company_virtual_group(self):
if self.allow_cross_company_virtual_group:
if not self.is_group:
frappe.throw(_("Only group cost centers can be set as cross-company virtual groups."))
# 假设存在一个检查用户权限的函数
if not has_permission_to_set_cross_company_group():
frappe.throw(_("You do not have permission to set this cost center as a cross-company virtual group."))
3. 业务操作调整
在涉及成本中心的业务操作中,如转换组和分类账、重命名等,需要考虑新属性的影响。
3.1 转换组和分类账
当将成本中心从组转换为分类账或从分类账转换为组时,需要检查allow_cross_company_virtual_group
属性的状态。如果成本中心已经被设置为跨公司虚拟组,可能需要禁止某些转换操作,或者在转换时进行特殊处理。
@frappe.whitelist()
def convert_group_to_ledger(self):
if self.allow_cross_company_virtual_group:
frappe.throw(_("Cannot convert a cross-company virtual group cost center to a ledger."))
if self.check_if_child_exists():
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
elif self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to ledger"))
else:
self.is_group = 0
self.save()
return 1
@frappe.whitelist()
def convert_ledger_to_group(self):
if self.allow_cross_company_virtual_group:
frappe.throw(_("A cross-company virtual group cost center is already a group and cannot be converted again."))
if self.if_allocation_exists_against_cost_center():
frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
if self.check_if_part_of_cost_center_allocation():
frappe.throw(
_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group")
)
if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1
self.save()
return 1
3.2 重命名
在重命名成本中心时,需要确保新名称不会影响其作为跨公司虚拟组的设置。如果需要,可以在重命名前后添加相应的处理逻辑。
def before_rename(self, olddn, newdn, merge=False):
# 检查是否为跨公司虚拟组,可根据需要添加额外处理逻辑
if self.allow_cross_company_virtual_group:
pass
# Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr
new_cost_center = get_name_with_abbr(newdn, self.company)
# Validate properties before merging
super().before_rename(olddn, new_cost_center, merge, "is_group")
if not merge:
new_cost_center = get_name_with_number(new_cost_center, self.cost_center_number)
return new_cost_center
def after_rename(self, olddn, newdn, merge=False):
# 检查是否为跨公司虚拟组,可根据需要添加额外处理逻辑
if self.allow_cross_company_virtual_group:
pass
super().after_rename(olddn, newdn, merge)
if not merge:
new_cost_center = frappe.db.get_value(
"Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1
)
# exclude company abbr
new_parts = newdn.split(" - ")[:-1]
# update cost center number and remove from parts
if new_parts[0][0].isdigit():
if len(new_parts) == 1:
new_parts = newdn.split(" ")
if new_cost_center.cost_center_number != new_parts[0]:
validate_field_number(
"Cost Center", self.name, new_parts[0], self.company, "cost_center_number"
)
self.cost_center_number = new_parts[0]
self.db_set("cost_center_number", new_parts[0])
new_parts = new_parts[1:]
# update cost center name
cost_center_name = " - ".join(new_parts)
if new_cost_center.cost_center_name != cost_center_name:
self.cost_center_name = cost_center_name
self.db_set("cost_center_name", cost_center_name)
4. 数据关联与查询
在实际业务中,可能需要根据allow_cross_company_virtual_group
属性进行数据关联和查询。例如,查询所有允许跨公司的虚拟组成本中心,或者在进行成本核算时,考虑跨公司虚拟组的特殊处理。
# 查询所有允许跨公司的虚拟组成本中心
cross_company_virtual_groups = frappe.get_all("Cost Center", filters={"allow_cross_company_virtual_group": 1})
总结
通过以上步骤,实现了“允许跨公司对成本中心设立虚拟组”的功能。主要包括数据结构的修改、验证逻辑的添加、业务操作的调整以及数据关联与查询的支持。在实际应用中,可以根据具体业务需求进一步完善和扩展这些逻辑。