diff --git a/default_thai_company/assets.py b/default_thai_company/assets.py new file mode 100644 index 0000000..501ec90 --- /dev/null +++ b/default_thai_company/assets.py @@ -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") diff --git a/default_thai_company/fixtures/asset_category.json b/default_thai_company/fixtures/asset_category.json new file mode 100644 index 0000000..abc5db1 --- /dev/null +++ b/default_thai_company/fixtures/asset_category.json @@ -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 + } +] diff --git a/default_thai_company/fixtures/location.json b/default_thai_company/fixtures/location.json new file mode 100644 index 0000000..67bd2c0 --- /dev/null +++ b/default_thai_company/fixtures/location.json @@ -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" + } +] diff --git a/default_thai_company/hooks.py b/default_thai_company/hooks.py index 0898f2e..fb3b862 100644 --- a/default_thai_company/hooks.py +++ b/default_thai_company/hooks.py @@ -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": { diff --git a/default_thai_company/tax_withholding.py b/default_thai_company/tax_withholding.py index f0ac22c..da8f831 100644 --- a/default_thai_company/tax_withholding.py +++ b/default_thai_company/tax_withholding.py @@ -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},