Bunchee
Back to Blog

วิธี Override Chart of Accounts ใน Frappe สำหรับภาษาไทย

เรียนรู้วิธีสร้าง Custom Chart of Accounts สำหรับบริษัทไทย พร้อมรองรับชื่อบัญชีภาษาไทย การ Import จาก CSV และการติดตั้งอัตโนมัติใน Frappe Framework

วิธี Override Chart of Accounts ใน Frappe สำหรับภาษาไทย

ระบบ Chart of Accounts (ผังบัญชี) ใน Frappe/ERPNext มาพร้อมกับ Template มาตรฐานหลายประเทศ แต่สำหรับบริษัทไทยที่ต้องการใช้ผังบัญชีตามมาตรฐานไทย พร้อมชื่อบัญชีภาษาไทย บทความนี้จะแนะนำวิธี Override ระบบเพื่อรองรับความต้องการเหล่านี้

ภาพรวมของระบบ

การ Override Chart of Accounts ประกอบด้วย 3 ส่วนหลัก:

  1. COA Template Handler - โหลด Template ผังบัญชี Custom
  2. Extended CSV Importer - Import ผังบัญชีจาก CSV พร้อมคอลัมน์เพิ่มเติม
  3. Auto-Installer - ติดตั้งผังบัญชีอัตโนมัติเมื่อสร้างบริษัท

โครงสร้างไฟล์

your_app/
├── your_app/
│   ├── accounts/
│   │   ├── chart_of_accounts/
│   │   │   ├── thai_standard.json      # JSON Template
│   │   │   └── thai_standard.csv       # CSV Template
│   │   └── chart_of_accounts.py        # COA Handler
│   ├── overrides/
│   │   └── chart_of_accounts_importer.py  # CSV Importer
│   └── setup/
│       └── coa_installer.py            # Auto-Installer
└── hooks.py

1. สร้าง COA Template Handler

สร้างไฟล์ chart_of_accounts.py เพื่อ Override การโหลด Template:

import frappe
import os
import json

def get_charts_for_country(country: str, with_standard: bool = False):
    """Override เพื่อเพิ่ม Chart Templates สำหรับประเทศไทย"""
    charts = []

    # โหลดจาก Template Directory ของ App
    template_dir = frappe.get_app_path("your_app", "accounts", "chart_of_accounts")

    if os.path.exists(template_dir):
        for filename in os.listdir(template_dir):
            if filename.endswith('.json'):
                chart_name = filename.replace('.json', '').replace('_', ' ').title()
                charts.append(chart_name)

    return charts

def get_chart(chart_name: str, country: str = None):
    """โหลด Chart Template ตามชื่อ"""
    template_dir = frappe.get_app_path("your_app", "accounts", "chart_of_accounts")
    filename = chart_name.lower().replace(' ', '_') + '.json'
    filepath = os.path.join(template_dir, filename)

    if os.path.exists(filepath):
        with open(filepath, 'r', encoding='utf-8') as f:
            return json.load(f)

    return None

2. สร้าง Extended CSV Importer

CSV Importer มาตรฐานของ Frappe รองรับ 8 คอลัมน์ แต่เราต้องการเพิ่มคอลัมน์สำหรับชื่อบัญชีภาษาไทย:

import frappe
from frappe import _
import csv

class ChartOfAccountsImporter:
    """Extended COA Importer รองรับ 10 คอลัมน์"""

    # คอลัมน์มาตรฐาน: 8
    # คอลัมน์เพิ่มเติม: 2 (account_name_th, description_th)
    EXPECTED_COLUMNS = 10

    COLUMN_MAPPING = {
        0: "account_name",
        1: "account_name_th",      # ชื่อบัญชีภาษาไทย
        2: "parent_account",
        3: "account_number",
        4: "is_group",
        5: "account_type",
        6: "root_type",
        7: "report_type",
        8: "account_currency",
        9: "description_th"        # คำอธิบายภาษาไทย
    }

    def validate_columns(self, row):
        """ตรวจสอบจำนวนคอลัมน์"""
        if len(row) < self.EXPECTED_COLUMNS:
            frappe.throw(_(f"ต้องมี {self.EXPECTED_COLUMNS} คอลัมน์, พบ {len(row)}"))

    def generate_data_from_csv(self, file_path):
        """Parse CSV พร้อมคอลัมน์เพิ่มเติม"""
        accounts = []

        with open(file_path, 'r', encoding='utf-8-sig') as f:
            reader = csv.reader(f)
            headers = next(reader)  # ข้าม Header Row

            for row in reader:
                if not row or not row[0].strip():
                    continue

                self.validate_columns(row)

                account = {
                    "account_name": row[0].strip(),
                    "account_name_th": row[1].strip() if len(row) > 1 else "",
                    "parent_account": row[2].strip() if len(row) > 2 else "",
                    "account_number": row[3].strip() if len(row) > 3 else "",
                    "is_group": int(row[4]) if len(row) > 4 and row[4] else 0,
                    "account_type": row[5].strip() if len(row) > 5 else "",
                    "root_type": row[6].strip() if len(row) > 6 else "",
                    "report_type": row[7].strip() if len(row) > 7 else "",
                    "account_currency": row[8].strip() if len(row) > 8 else "",
                    "description_th": row[9].strip() if len(row) > 9 else ""
                }
                accounts.append(account)

        return accounts

    def build_forest(self, accounts):
        """สร้าง Tree Structure จากข้อมูล Flat"""
        by_parent = {}
        for acc in accounts:
            parent = acc.get("parent_account", "")
            if parent not in by_parent:
                by_parent[parent] = []
            by_parent[parent].append(acc)

        def build_tree(parent_name=""):
            tree = {}
            for acc in by_parent.get(parent_name, []):
                name = acc["account_name"]
                tree[name] = {
                    "account_number": acc.get("account_number"),
                    "account_type": acc.get("account_type"),
                    "root_type": acc.get("root_type"),
                    "report_type": acc.get("report_type"),
                    "is_group": acc.get("is_group"),
                    "account_name_th": acc.get("account_name_th"),
                }
                # Recursive สำหรับ Child Accounts
                children = build_tree(name)
                if children:
                    tree[name].update(children)
            return tree

        return build_tree()

    def import_coa(self, company, chart_data):
        """Import ผังบัญชีเข้าบริษัท"""
        from frappe.utils.nestedset import rebuild_tree

        def create_accounts(tree, parent=None, root_type=None):
            for account_name, account_data in tree.items():
                # ข้าม Metadata Keys
                if account_name in ["account_number", "account_type", "root_type",
                                    "report_type", "is_group", "account_name_th"]:
                    continue

                current_root_type = account_data.get("root_type") or root_type

                account = frappe.new_doc("Account")
                account.account_name = account_name
                account.company = company
                account.parent_account = parent
                account.account_number = account_data.get("account_number")
                account.account_type = account_data.get("account_type")
                account.root_type = current_root_type
                account.report_type = account_data.get("report_type")
                account.is_group = account_data.get("is_group", 0)

                # เก็บชื่อภาษาไทยใน Custom Field
                if account_data.get("account_name_th"):
                    account.account_name_th = account_data["account_name_th"]

                account.flags.ignore_permissions = True
                account.insert()

                # Recursive สำหรับ Child Accounts
                create_accounts(account_data, account.name, current_root_type)

        create_accounts(chart_data)
        rebuild_tree("Account", "parent_account")

3. สร้าง Auto-Installer

สร้างไฟล์ coa_installer.py สำหรับติดตั้งอัตโนมัติ:

import frappe
import os

def install_chart_of_accounts(company_name: str, template_name: str = "thai_standard"):
    """ติดตั้งผังบัญชีอัตโนมัติ"""

    # ตรวจสอบว่าบริษัทมีบัญชีอยู่แล้วหรือไม่
    existing = frappe.db.count("Account", {"company": company_name})
    if existing > 0:
        frappe.msgprint(f"บริษัท {company_name} มี {existing} บัญชีอยู่แล้ว")
        return

    # โหลด Template
    template_path = frappe.get_app_path(
        "your_app",
        "accounts",
        "chart_of_accounts",
        f"{template_name}.csv"
    )

    if not os.path.exists(template_path):
        frappe.throw(f"ไม่พบ Template: {template_path}")

    from your_app.overrides.chart_of_accounts_importer import ChartOfAccountsImporter

    importer = ChartOfAccountsImporter()
    accounts = importer.generate_data_from_csv(template_path)
    chart_data = importer.build_forest(accounts)
    importer.import_coa(company_name, chart_data)

    frappe.db.commit()
    frappe.msgprint(f"ติดตั้ง {len(accounts)} บัญชี สำหรับ {company_name} เรียบร้อย")

4. ตั้งค่า hooks.py

# Override COA Functions
override_whitelisted_methods = {
    "erpnext.accounts.doctype.account.chart_of_accounts.importer.get_charts_for_country":
        "your_app.accounts.chart_of_accounts.get_charts_for_country",
    "erpnext.accounts.doctype.account.chart_of_accounts.importer.get_chart":
        "your_app.accounts.chart_of_accounts.get_chart",
}

# ติดตั้งอัตโนมัติเมื่อสร้างบริษัท (Optional)
doc_events = {
    "Company": {
        "after_insert": "your_app.setup.coa_installer.install_chart_of_accounts"
    }
}

5. รูปแบบ CSV Template

สร้างไฟล์ CSV 10 คอลัมน์:

account_name,account_name_th,parent_account,account_number,is_group,account_type,root_type,report_type,account_currency,description_th
Assets,สินทรัพย์,,1,1,,Asset,Balance Sheet,THB,หมวดสินทรัพย์
Current Assets,สินทรัพย์หมุนเวียน,Assets,11,1,,Asset,Balance Sheet,THB,
Cash,เงินสด,Current Assets,1101,0,Cash,Asset,Balance Sheet,THB,บัญชีเงินสด
Bank Accounts,เงินฝากธนาคาร,Current Assets,1102,1,Bank,Asset,Balance Sheet,THB,
Liabilities,หนี้สิน,,2,1,,Liability,Balance Sheet,THB,หมวดหนี้สิน
Equity,ส่วนของผู้ถือหุ้น,,3,1,,Equity,Balance Sheet,THB,หมวดทุน
Income,รายได้,,4,1,,Income,Profit and Loss,THB,หมวดรายได้
Expenses,ค่าใช้จ่าย,,5,1,,Expense,Profit and Loss,THB,หมวดค่าใช้จ่าย

สร้าง Custom Field สำหรับชื่อภาษาไทย

ใน App ของคุณ ต้องสร้าง Custom Field account_name_th บน Account DocType:

{
  "doctype": "Custom Field",
  "dt": "Account",
  "fieldname": "account_name_th",
  "fieldtype": "Data",
  "label": "Account Name (Thai)",
  "insert_after": "account_name"
}

การแก้ไขปัญหา

บัญชีไม่ถูก Import

  • ตรวจสอบ Encoding ของไฟล์ CSV (ต้องเป็น UTF-8)
  • ตรวจสอบจำนวนคอลัมน์
  • Parent Account ต้องถูกสร้างก่อน Child

Tree Structure พัง

bench --site your-site.local console
from frappe.utils.nestedset import rebuild_tree
rebuild_tree("Account", "parent_account")

Custom Field ไม่ถูกบันทึก

  • ตรวจสอบว่า Custom Field account_name_th มีอยู่บน Account DocType
  • ตรวจสอบ Permissions

สรุป

การ Override Chart of Accounts ช่วยให้เราสามารถ:

  1. ใช้ผังบัญชีมาตรฐานไทย
  2. มีชื่อบัญชีทั้งภาษาอังกฤษและภาษาไทย
  3. Import จาก CSV ได้สะดวก
  4. ติดตั้งอัตโนมัติเมื่อสร้างบริษัท

เอกสารอ้างอิง