人财事物信息化 - cost_center.py


  1. 一段话总结:该代码定义了CostCenter类,用于管理ERPNext系统中的成本中心。它继承自NestedSet,具备自动命名、必填项和父成本中心验证等功能。支持成本中心在组和分类账之间转换,转换时会检查是否存在子节点、交易记录或分配记录。在重命名前后也有相应的处理逻辑,并在on_doctype_update方法中为Cost Center添加索引
  2. 思维导图
## **CostCenter类定义**
- 继承NestedSet类
- 包含多个属性,如company、cost_center_name等
## **自动命名功能**
- 使用get_autoname_with_number函数
- 依据成本中心编号、名称和公司生成名称
## **验证方法**
- 验证必填项,检查父成本中心设置
- 验证父成本中心是否为组节点
## **转换功能**
- 组转分类账,检查子节点和交易记录
- 分类账转组,检查分配和交易记录
## **重命名处理**
- 重命名前添加公司缩写并验证
- 重命名后更新编号和名称
## **索引添加**
- 在on_doctype_update方法中执行
- 为Cost Center添加lft和rgt索引
  1. 详细总结 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_numbercost_center_namecompany生成成本中心的名称。
  • 验证方法
    • 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_centercheck_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. 关键问题
    • 问题1:成本中心组转换为分类账时,为什么要检查是否存在子节点?
      • 答案:因为成本中心组若存在子节点,将其转换为分类账会破坏成本中心的层级结构和数据完整性,导致子节点失去正确的归属关系,所以在转换前需要检查并禁止这种操作。
    • 问题2validate_mandatory方法中两个条件判断的依据是什么?
      • 答案:当cost_center_name不等于company时,意味着不是根成本中心,此时必须设置parent_cost_center来确定其层级关系;而当cost_center_name等于company时,代表这是根成本中心,根成本中心不能有父成本中心,否则会破坏成本中心的根节点定义和层级逻辑。
    • 问题3after_rename方法中更新成本中心编号和名称的意义是什么?
      • 答案:更新成本中心编号和名称是为了确保成本中心的编号和名称与重命名后的实际情况保持一致,保证系统中成本中心信息的准确性和一致性,避免因编号或名称不一致导致数据混乱,影响后续的成本核算、报表生成等相关业务操作。 ---
  2. 一段话总结:该Python文件定义了CostCenter类,继承自Document,用于管理成本中心。其中validate方法在保存成本中心信息时,会验证父成本中心是否存在循环引用,若有则抛出错误;同时会验证父成本中心是否已禁用,若禁用则抛出错误。还会调用update_nsm_model更新嵌套集模型。
  3. 思维导图
## **CostCenter类**
- 继承自Document
- 用于管理成本中心
## **validate方法**
- 验证父成本中心循环引用
- 验证父成本中心是否禁用
- 调用update_nsm_model更新嵌套集模型
  1. 详细总结 此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更新嵌套集模型。
  2. 关键问题
    • 问题1validate方法在保存成本中心信息时主要做了哪些验证?
      • 答案:主要做了两方面验证。一是调用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方法来更新嵌套集模型。

要在现有代码基础上增加允许跨公司对成本中心设立虚拟组的属性,需要对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

修改说明:

  1. 属性添加:在CostCenter类的类型提示部分添加了allow_cross_company_virtual_group: DF.Check属性,用于表示是否允许该成本中心作为跨公司的虚拟组。
  2. 验证方法添加:新增了validate_cross_company_virtual_group方法,在validate方法中调用。该方法目前检查当allow_cross_company_virtual_groupTrue时,成本中心必须是组类型,否则抛出错误。你可以根据实际业务需求进一步扩展此方法,例如检查用户权限、确保跨公司操作的合规性等。
  3. 其他方法调整:如果后续有涉及到跨公司虚拟组的操作,如转换组和分类账、重命名等,可能需要在相应方法中添加对新属性的处理逻辑。当前代码在已有方法基础上,主要确保新属性在保存时能得到合理验证。
  4. 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})

总结

通过以上步骤,实现了“允许跨公司对成本中心设立虚拟组”的功能。主要包括数据结构的修改、验证逻辑的添加、业务操作的调整以及数据关联与查询的支持。在实际应用中,可以根据具体业务需求进一步完善和扩展这些逻辑。

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