# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt import os import json import frappe from frappe import _ def setup_taxes_and_charges(company_name: str, country: str): if not frappe.db.exists("Company", company_name): frappe.throw(_("Company {} does not exist yet. Taxes setup aborted.").format(company_name)) file_path = os.path.join(os.path.dirname(__file__), "..", "data", "country_wise_tax.json") with open(file_path, "r") as json_file: tax_data = json.load(json_file) country_wise_tax = tax_data.get(country) if not country_wise_tax: return if "chart_of_accounts" not in country_wise_tax: country_wise_tax = simple_to_detailed(country_wise_tax) from_detailed_data(company_name, country_wise_tax) update_regional_tax_settings(country, company_name) def simple_to_detailed(templates): """ Convert a simple taxes object into a more detailed data structure. Example input: { "France VAT 20%": { "account_name": "VAT 20%", "tax_rate": 20, "default": 1 }, "France VAT 10%": { "account_name": "VAT 10%", "tax_rate": 10 } } """ return { "chart_of_accounts": { "*": { "item_tax_templates": [ { "title": title, "taxes": [ {"tax_type": {"account_name": data.get("account_name"), "tax_rate": data.get("tax_rate")}} ], } for title, data in templates.items() ], "*": [ { "title": title, "is_default": data.get("default", 0), "taxes": [ { "account_head": { "account_name": data.get("account_name"), "tax_rate": data.get("tax_rate"), } } ], } for title, data in templates.items() ], } } } def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" coa_name = frappe.db.get_value("Company", company_name, "chart_of_accounts") coa_data = data.get("chart_of_accounts", {}) tax_templates = coa_data.get(coa_name) or coa_data.get("*", {}) tax_categories = data.get("tax_categories") sales_tax_templates = tax_templates.get("sales_tax_templates") or tax_templates.get("*", {}) purchase_tax_templates = tax_templates.get("purchase_tax_templates") or tax_templates.get("*", {}) item_tax_templates = tax_templates.get("item_tax_templates") or tax_templates.get("*", {}) if tax_categories: for tax_category in tax_categories: make_tax_category(tax_category) if sales_tax_templates: for template in sales_tax_templates: make_taxes_and_charges_template(company_name, "Sales Taxes and Charges Template", template) if purchase_tax_templates: for template in purchase_tax_templates: make_taxes_and_charges_template(company_name, "Purchase Taxes and Charges Template", template) if item_tax_templates: for template in item_tax_templates: make_item_tax_template(company_name, template) def update_regional_tax_settings(country, company): path = frappe.get_app_path("erpnext", "regional", frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format( frappe.scrub(country) ) frappe.get_attr(module_name)(country, company) except Exception as e: # Log error and ignore if failed to setup regional tax settings frappe.log_error("Unable to setup regional tax settings") pass def make_taxes_and_charges_template(company_name, doctype, template): template["company"] = company_name template["doctype"] = doctype if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return for tax_row in template.get("taxes"): account_data = tax_row.get("account_head") tax_row_defaults = { "category": "Total", "charge_type": "On Net Total", "cost_center": frappe.db.get_value("Company", company_name, "cost_center"), } if doctype == "Purchase Taxes and Charges Template": tax_row_defaults["add_deduct_tax"] = "Add" # if account_head is a dict, search or create the account and get it's name if isinstance(account_data, dict): tax_row_defaults["description"] = "{0} @ {1}".format( account_data.get("account_name"), account_data.get("tax_rate") ) tax_row_defaults["rate"] = account_data.get("tax_rate") account = get_or_create_account(company_name, account_data) tax_row["account_head"] = account.name # use the default value if nothing other is specified for fieldname, default_value in tax_row_defaults.items(): if fieldname not in tax_row: tax_row[fieldname] = default_value doc = frappe.get_doc(template) # Data in country wise json is already pre validated, hence validations can be ignored # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) return doc def make_item_tax_template(company_name, template): """Create an Item Tax Template. This requires a separate method because Item Tax Template is structured differently from Sales and Purchase Tax Templates. """ doctype = "Item Tax Template" template["company"] = company_name template["doctype"] = doctype if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return for tax_row in template.get("taxes"): account_data = tax_row.get("tax_type") # if tax_type is a dict, search or create the account and get it's name if isinstance(account_data, dict): account = get_or_create_account(company_name, account_data) tax_row["tax_type"] = account.name if "tax_rate" not in tax_row: tax_row["tax_rate"] = account_data.get("tax_rate") doc = frappe.get_doc(template) # Data in country wise json is already pre validated, hence validations can be ignored # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) return doc def get_or_create_account(company_name, account): """ Check if account already exists. If not, create it. Return a tax account or None. """ default_root_type = "Liability" root_type = account.get("root_type", default_root_type) existing_accounts = frappe.get_all( "Account", filters={"company": company_name, "root_type": root_type}, or_filters={ "account_name": account.get("account_name"), "account_number": account.get("account_number"), }, ) if existing_accounts: return frappe.get_doc("Account", existing_accounts[0].name) tax_group = get_or_create_tax_group(company_name, root_type) account["doctype"] = "Account" account["company"] = company_name account["parent_account"] = tax_group account["report_type"] = "Balance Sheet" account["account_type"] = "Tax" account["root_type"] = root_type account["is_group"] = 0 doc = frappe.get_doc(account) doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True, ignore_mandatory=True) return doc def get_or_create_tax_group(company_name, root_type): # Look for a group account of type 'Tax' tax_group_name = frappe.db.get_value( "Account", {"is_group": 1, "root_type": root_type, "account_type": "Tax", "company": company_name}, ) if tax_group_name: return tax_group_name # Look for a group account named 'Duties and Taxes' or 'Tax Assets' account_name = _("Duties and Taxes") if root_type == "Liability" else _("Tax Assets") tax_group_name = frappe.db.get_value( "Account", {"is_group": 1, "root_type": root_type, "account_name": account_name, "company": company_name}, ) if tax_group_name: return tax_group_name # Create a new group account named 'Duties and Taxes' or 'Tax Assets' just # below the root account root_account = frappe.get_all( "Account", { "is_group": 1, "root_type": root_type, "company": company_name, "report_type": "Balance Sheet", "parent_account": ("is", "not set"), }, limit=1, )[0] tax_group_account = frappe.get_doc( { "doctype": "Account", "company": company_name, "is_group": 1, "report_type": "Balance Sheet", "root_type": root_type, "account_type": "Tax", "account_name": account_name, "parent_account": root_account.name, } ) tax_group_account.flags.ignore_links = True tax_group_account.flags.ignore_validate = True tax_group_account.insert(ignore_permissions=True) tax_group_name = tax_group_account.name return tax_group_name def make_tax_category(tax_category): doctype = "Tax Category" if isinstance(tax_category, str): tax_category = {"title": tax_category} tax_category["doctype"] = doctype if not frappe.db.exists(doctype, tax_category["title"]): doc = frappe.get_doc(tax_category) doc.insert(ignore_permissions=True)