feat: default Location tree and Asset Categories
Location fixture: "All Locations" group with Head Office, Branch Office and Warehouse. Fixture import re-inserts tree nodes with fresh lft/rgt, which strands locations users add beneath them, so after_migrate rebuilds the Location tree. Asset Category fixture: Land (non-depreciable), Buildings (20 years), Plant and Machinery, Vehicles, Furniture and Fixtures, Office Equipment, Computers and Electronics, Software (5 years) and Intangible Assets (10 years), per Royal Decree 145 rates; monthly straight-line, daily pro-rata. The accounts table is company-specific and therefore not shipped: an Asset Category before_import hook links every Thai company (same contract as the WHT categories), reusing the Standard chart's fixed asset accounts and creating Land, Vehicles and Intangible Assets under Fixed Assets. Company.on_update links new companies. Depreciation accounts are left to the Company defaults. get_or_create_account now takes account_type and parent_groups.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import frappe
|
||||
from frappe.utils.nestedset import rebuild_tree
|
||||
|
||||
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
|
||||
|
||||
# Asset Category -> fixed asset account. Names follow ERPNext's Standard chart so
|
||||
# setup-wizard companies reuse their accounts; the rest are created under the
|
||||
# same "Fixed Assets" group. Depreciation accounts are left to the Company
|
||||
# defaults (Asset falls back to them), so one change there covers every category.
|
||||
FIXED_ASSET_ACCOUNTS = {
|
||||
"Land": "Land",
|
||||
"Buildings": "Buildings",
|
||||
"Plant and Machinery": "Plants and Machineries",
|
||||
"Vehicles": "Vehicles",
|
||||
"Furniture and Fixtures": "Furnitures and Fixtures",
|
||||
"Office Equipment": "Office Equipments",
|
||||
"Computers and Electronics": "Electronic Equipments",
|
||||
"Software": "Softwares",
|
||||
"Intangible Assets": "Intangible Assets",
|
||||
}
|
||||
|
||||
FIXED_ASSET_GROUPS = ("Fixed Assets",)
|
||||
|
||||
|
||||
def fixed_asset_account(company, category):
|
||||
return get_or_create_account(
|
||||
company, FIXED_ASSET_ACCOUNTS[category], "Asset", "Fixed Asset", FIXED_ASSET_GROUPS
|
||||
)
|
||||
|
||||
|
||||
def link_company(category, company):
|
||||
"""Append the company's fixed asset account row to the category if missing."""
|
||||
if any(row.company_name == company for row in category.accounts):
|
||||
return False
|
||||
category.append(
|
||||
"accounts",
|
||||
{"company_name": company, "fixed_asset_account": fixed_asset_account(company, category.name)},
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def prepare_fixture_accounts(doc, method=None):
|
||||
"""Asset Category fixtures ship without `accounts` (company-specific).
|
||||
|
||||
Same contract as the Tax Withholding Category fixtures: fixture import
|
||||
re-inserts the doc on every migrate, so carry over the rows already on this
|
||||
site, then link every Thai company. Mandatory is relaxed for sites without a
|
||||
Thai company yet; `setup_company` links them later.
|
||||
"""
|
||||
if doc.name not in FIXED_ASSET_ACCOUNTS:
|
||||
return
|
||||
|
||||
doc.flags.ignore_mandatory = True
|
||||
|
||||
if not doc.accounts and frappe.db.exists(doc.doctype, doc.name):
|
||||
for row in frappe.get_all(
|
||||
"Asset Category Account",
|
||||
filters={"parent": doc.name, "parenttype": doc.doctype},
|
||||
fields=[
|
||||
"company_name",
|
||||
"fixed_asset_account",
|
||||
"accumulated_depreciation_account",
|
||||
"depreciation_expense_account",
|
||||
"capital_work_in_progress_account",
|
||||
],
|
||||
order_by="idx",
|
||||
):
|
||||
doc.append("accounts", row)
|
||||
|
||||
for company in thai_companies():
|
||||
link_company(doc, company)
|
||||
|
||||
|
||||
def setup_company(doc, method=None):
|
||||
"""Company.on_update: link a Thai company on every default Asset Category."""
|
||||
if not company_ready(doc):
|
||||
return
|
||||
|
||||
for name in FIXED_ASSET_ACCOUNTS:
|
||||
if not frappe.db.exists("Asset Category", name):
|
||||
continue
|
||||
category = frappe.get_doc("Asset Category", name)
|
||||
if link_company(category, doc.name):
|
||||
category.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def rebuild_locations():
|
||||
"""after_migrate: fixture import re-inserts the Location tree nodes (fresh
|
||||
lft/rgt), which strands locations users added beneath them; rebuild."""
|
||||
if frappe.db.exists("Location", "All Locations"):
|
||||
rebuild_tree("Location")
|
||||
@@ -0,0 +1,237 @@
|
||||
[
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Land",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Land",
|
||||
"non_depreciable_category": 1
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Buildings",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Buildings",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 240
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Buildings",
|
||||
"non_depreciable_category": 0
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Plant and Machinery",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Plant and Machinery",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 60
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Plant and Machinery",
|
||||
"non_depreciable_category": 0
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Vehicles",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Vehicles",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 60
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Vehicles",
|
||||
"non_depreciable_category": 0
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Furniture and Fixtures",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Furniture and Fixtures",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 60
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Furniture and Fixtures",
|
||||
"non_depreciable_category": 0
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Office Equipment",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Office Equipment",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 60
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Office Equipment",
|
||||
"non_depreciable_category": 0
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Computers and Electronics",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Computers and Electronics",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 60
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Computers and Electronics",
|
||||
"non_depreciable_category": 0
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Software",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Software",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 60
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Software",
|
||||
"non_depreciable_category": 0
|
||||
},
|
||||
{
|
||||
"accounts": [],
|
||||
"asset_category_name": "Intangible Assets",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Category",
|
||||
"enable_cwip_accounting": 0,
|
||||
"finance_books": [
|
||||
{
|
||||
"daily_prorata_based": 1,
|
||||
"depreciation_method": "Straight Line",
|
||||
"docstatus": 0,
|
||||
"doctype": "Asset Finance Book",
|
||||
"finance_book": null,
|
||||
"frequency_of_depreciation": 1,
|
||||
"idx": 1,
|
||||
"parent": "Intangible Assets",
|
||||
"parentfield": "finance_books",
|
||||
"parenttype": "Asset Category",
|
||||
"rate_of_depreciation": 0.0,
|
||||
"salvage_value_percentage": 0.0,
|
||||
"shift_based": 0,
|
||||
"total_number_of_depreciations": 120
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Intangible Assets",
|
||||
"non_depreciable_category": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
[
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Location",
|
||||
"is_container": 0,
|
||||
"is_group": 1,
|
||||
"location_name": "All Locations",
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "All Locations",
|
||||
"parent_location": null
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Location",
|
||||
"is_container": 0,
|
||||
"is_group": 0,
|
||||
"location_name": "Head Office",
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Head Office",
|
||||
"parent_location": "All Locations"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Location",
|
||||
"is_container": 0,
|
||||
"is_group": 0,
|
||||
"location_name": "Branch Office",
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Branch Office",
|
||||
"parent_location": "All Locations"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Location",
|
||||
"is_container": 0,
|
||||
"is_group": 0,
|
||||
"location_name": "Warehouse",
|
||||
"modified": "2026-09-16 09:00:00.000000",
|
||||
"name": "Warehouse",
|
||||
"parent_location": "All Locations"
|
||||
}
|
||||
]
|
||||
@@ -105,6 +105,30 @@ fixtures = [
|
||||
],
|
||||
},
|
||||
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
|
||||
{
|
||||
"doctype": "Location",
|
||||
"filters": [["name", "in", ["All Locations", "Head Office", "Branch Office", "Warehouse"]]],
|
||||
},
|
||||
{
|
||||
"doctype": "Asset Category",
|
||||
"filters": [
|
||||
[
|
||||
"name",
|
||||
"in",
|
||||
[
|
||||
"Land",
|
||||
"Buildings",
|
||||
"Plant and Machinery",
|
||||
"Vehicles",
|
||||
"Furniture and Fixtures",
|
||||
"Office Equipment",
|
||||
"Computers and Electronics",
|
||||
"Software",
|
||||
"Intangible Assets",
|
||||
],
|
||||
]
|
||||
],
|
||||
},
|
||||
# HRMS doctypes; skipped by sync_fixtures on sites without hrms
|
||||
{"doctype": "Employee Tax Exemption Category"},
|
||||
{"doctype": "Employee Tax Exemption Sub Category"},
|
||||
@@ -117,6 +141,8 @@ fixtures = [
|
||||
# before_install = "default_thai_company.install.before_install"
|
||||
# Fixtures are synced before this runs; VAT schemes for companies that already exist.
|
||||
after_install = "default_thai_company.vat.setup_companies"
|
||||
# Fixture import re-inserts the Location tree; restore lft/rgt for user-added nodes.
|
||||
after_migrate = "default_thai_company.assets.rebuild_locations"
|
||||
|
||||
# Uninstallation
|
||||
# ------------
|
||||
@@ -179,10 +205,14 @@ doc_events = {
|
||||
"Tax Withholding Category": {
|
||||
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
|
||||
},
|
||||
"Asset Category": {
|
||||
"before_import": "default_thai_company.assets.prepare_fixture_accounts",
|
||||
},
|
||||
"Company": {
|
||||
"on_update": [
|
||||
"default_thai_company.tax_withholding.setup_company",
|
||||
"default_thai_company.vat.setup_company",
|
||||
"default_thai_company.assets.setup_company",
|
||||
],
|
||||
},
|
||||
"Payment Entry": {
|
||||
|
||||
@@ -43,7 +43,9 @@ def thai_companies():
|
||||
return frappe.get_all("Company", filters={"country": "Thailand"}, pluck="name")
|
||||
|
||||
|
||||
def get_or_create_account(company, account_name, root_type):
|
||||
def get_or_create_account(company, account_name, root_type, account_type="Tax", parent_groups=None):
|
||||
"""Leaf account `account_name` for `company`, created under the first existing
|
||||
group in `parent_groups` (default: PARENT_GROUPS[root_type]), else the root."""
|
||||
name = frappe.db.get_value(
|
||||
"Account", {"company": company, "account_name": account_name, "is_group": 0, "root_type": root_type}
|
||||
)
|
||||
@@ -55,17 +57,19 @@ def get_or_create_account(company, account_name, root_type):
|
||||
"doctype": "Account",
|
||||
"company": company,
|
||||
"account_name": account_name,
|
||||
"parent_account": find_parent_group(company, root_type),
|
||||
"parent_account": find_parent_group(
|
||||
company, root_type, parent_groups or PARENT_GROUPS[root_type]
|
||||
),
|
||||
"root_type": root_type,
|
||||
"account_type": "Tax",
|
||||
"account_type": account_type,
|
||||
"is_group": 0,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
return account.name
|
||||
|
||||
|
||||
def find_parent_group(company, root_type):
|
||||
for account_name in PARENT_GROUPS[root_type]:
|
||||
def find_parent_group(company, root_type, group_names):
|
||||
for account_name in group_names:
|
||||
parent = frappe.db.get_value(
|
||||
"Account",
|
||||
{"company": company, "account_name": account_name, "is_group": 1, "root_type": root_type},
|
||||
|
||||
Reference in New Issue
Block a user