From 7d1a7dc8e4ff73d18b2563dfb5c49f95a3e1867f Mon Sep 17 00:00:00 2001 From: Emanuel Kagombora Date: Mon, 4 May 2026 21:19:22 +0300 Subject: [PATCH] feat(av_tools): add indirect expense item auto-creation --- av_tools/av_tools/account.js | 43 +++++++ .../av_tools_settings/av_tools_settings.json | 16 ++- av_tools/av_tools_hooks/account.py | 115 ++++++++++++++++++ av_tools/hooks.py | 5 + av_tools/patches.txt | 1 + .../account_item_custom_field.json | 43 +++++++ .../move_indirect_expense_item_feature.py | 10 ++ 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 av_tools/av_tools/account.js create mode 100644 av_tools/av_tools_hooks/account.py create mode 100644 av_tools/patches/custom_fields/custom_fields_json/account_item_custom_field.json create mode 100644 av_tools/patches/v1_0/move_indirect_expense_item_feature.py diff --git a/av_tools/av_tools/account.js b/av_tools/av_tools/account.js new file mode 100644 index 0000000..dfdec8e --- /dev/null +++ b/av_tools/av_tools/account.js @@ -0,0 +1,43 @@ +frappe.ui.form.on("Account", { + onload_post_render: function(frm) { + frm.trigger("parent_account"); + frm.trigger("create_expenses_item_btn"); + frm.set_query("item", function() { + return { + "filters": { + "item_group": "Indirect Expenses" + } + }; + }); + }, + refresh: function(frm) { + frm.trigger("onload_post_render"); + }, + create_expenses_item_btn: function (frm) { + frappe.db.get_single_value("AV Tools Settings", "enable_indirect_expense_item_creation").then(function (enabled) { + if (!enabled) return; + frm.add_custom_button(__("Create Expenses Item"), function() { + frappe.call({ + method: 'av_tools.av_tools_hooks.account.add_indirect_expense_item', + args: { + account_name: frm.doc.name, + }, + callback: function(r) { + if (r.message) { + frm.set_value("item", r.message); + frm.refresh_field("item"); + frm.save(); + } + } + }); + }); + }); + }, + parent_account: function(frm) { + frm.trigger("create_expenses_item_btn"); + frm.refresh_field("item"); + }, + item: function(frm) { + frm.trigger("create_expenses_item_btn"); + }, +}); diff --git a/av_tools/av_tools/doctype/av_tools_settings/av_tools_settings.json b/av_tools/av_tools/doctype/av_tools_settings/av_tools_settings.json index 1346fb5..a375c4a 100644 --- a/av_tools/av_tools/doctype/av_tools_settings/av_tools_settings.json +++ b/av_tools/av_tools/doctype/av_tools_settings/av_tools_settings.json @@ -20,7 +20,9 @@ "section_break_inter_company", "allow_inter_company_stock_transfer", "permissions_settings_section", - "enable_dependent_auto_permission" + "enable_dependent_auto_permission", + "indirect_expense_item_section", + "enable_indirect_expense_item_creation" ], "fields": [ { @@ -126,6 +128,18 @@ "fieldname": "enable_dependent_auto_permission", "fieldtype": "Check", "label": "Enable Dependent Auto Permission" + }, + { + "fieldname": "indirect_expense_item_section", + "fieldtype": "Section Break", + "label": "Indirect Expense Item" + }, + { + "default": "0", + "description": "When enabled, saving an Account whose parent contains \"Indirect Expenses\" will auto-create (or reuse) an Item with the same name and link it via Account.item.", + "fieldname": "enable_indirect_expense_item_creation", + "fieldtype": "Check", + "label": "Enable Indirect Expense Item Auto-Creation" } ], "index_web_pages_for_search": 1, diff --git a/av_tools/av_tools_hooks/account.py b/av_tools/av_tools_hooks/account.py new file mode 100644 index 0000000..e48282f --- /dev/null +++ b/av_tools/av_tools_hooks/account.py @@ -0,0 +1,115 @@ +import frappe +from frappe import _ + + +def _is_feature_enabled(): + return bool( + frappe.db.get_single_value("AV Tools Settings", "enable_indirect_expense_item_creation") + ) + + +def create_indirect_expense_item(doc, method=None): + if not _is_feature_enabled(): + return + + if frappe.local.flags.ignore_root_company_validation: + return + + if ( + not doc.parent_account + or doc.is_group + or not check_expenses_in_parent_accounts(doc.name) + or not doc.company + ): + return + if ( + not doc.parent_account + and not check_expenses_in_parent_accounts(doc.account_name) + and doc.item + ): + doc.item = "" + return + indirect_expenses_group = frappe.db.exists("Item Group", "Indirect Expenses") + if not indirect_expenses_group: + indirect_expenses_group = frappe.get_doc( + dict( + doctype="Item Group", + item_group_name="Indirect Expenses", + ) + ) + indirect_expenses_group.flags.ignore_permissions = True + frappe.flags.ignore_account_permission = True + indirect_expenses_group.save() + item = frappe.db.exists("Item", doc.account_name) + if item: + item = frappe.get_doc("Item", doc.account_name) + doc.item = item.name + company_list = [] + for i in item.item_defaults: + if doc.company not in company_list: + if i.company == doc.company: + company_list.append(doc.company) + if i.expense_account != doc.name: + i.expense_account = doc.name + item.save() + if doc.company not in company_list: + row = item.append("item_defaults", {}) + row.company = doc.company + row.expense_account = doc.name + item.save() + company_list.append(doc.company) + doc.db_update() + return item.name + new_item = frappe.get_doc( + dict( + doctype="Item", + item_code=doc.account_name, + item_group="Indirect Expenses", + is_stock_item=0, + is_sales_item=0, + stock_uom="Nos", + include_item_in_manufacturing=0, + item_defaults=[ + { + "company": doc.company, + "expense_account": doc.name, + "default_warehouse": "", + } + ], + ) + ) + new_item.flags.ignore_permissions = True + frappe.flags.ignore_account_permission = True + new_item.save() + if new_item.name: + url = frappe.utils.get_url_to_form(new_item.doctype, new_item.name) + msgprint = "New Item is Created {1}".format( + url, new_item.name + ) + frappe.msgprint(_(msgprint)) + doc.item = new_item.name + doc.db_update() + return new_item.name + + +def check_expenses_in_parent_accounts(account_name): + parent_account_1 = frappe.get_value("Account", account_name, "parent_account") + if "Indirect Expenses" in str(parent_account_1): + return True + parent_account_2 = frappe.get_value("Account", parent_account_1, "parent_account") + if "Indirect Expenses" in str(parent_account_2): + return True + parent_account_3 = frappe.get_value("Account", parent_account_2, "parent_account") + if "Indirect Expenses" in str(parent_account_3): + return True + return False + + +@frappe.whitelist() +def add_indirect_expense_item(account_name): + if not _is_feature_enabled(): + frappe.throw( + _("Indirect Expense Item auto-creation is disabled. Enable it in AV Tools Settings.") + ) + account = frappe.get_doc("Account", account_name) + return create_indirect_expense_item(account) diff --git a/av_tools/hooks.py b/av_tools/hooks.py index a8d51a9..65ff399 100644 --- a/av_tools/hooks.py +++ b/av_tools/hooks.py @@ -63,6 +63,7 @@ "Purchase Invoice": "weigh_bridge/doctype/purchase_invoice_weighbridge_ticket.js", "Purchase Receipt": "weigh_bridge/doctype/purchase_receipt_weighbridge_ticket.js", "Customer": "authotp/api/customer.js", + "Account": "av_tools/account.js", } # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} @@ -194,6 +195,10 @@ "Custom DocPerm": { "validate": "av_tools.av_tools_hooks.custom_docperm.grant_dependant_access", }, + "Account": { + "on_update": "av_tools.av_tools_hooks.account.create_indirect_expense_item", + "after_insert": "av_tools.av_tools_hooks.account.create_indirect_expense_item", + }, "*": { "validate": ["av_tools.av_tools.doctype.visibility.visibility.run_visibility"], "onload": ["av_tools.av_tools.doctype.visibility.visibility.run_visibility"], diff --git a/av_tools/patches.txt b/av_tools/patches.txt index c5579df..edc77b7 100644 --- a/av_tools/patches.txt +++ b/av_tools/patches.txt @@ -22,6 +22,7 @@ av_tools.patches.v1_0.move_attachment_and_maintenance_doctypes av_tools.patches.v1_0.move_root_cause_doctypes av_tools.patches.v1_0.move_misc_csf_tz_doctypes av_tools.patches.v1_0.move_repack_and_consignment_doctypes +av_tools.patches.v1_0.move_indirect_expense_item_feature av_tools.patches.v1_0.move_csf_tz_print_formats av_tools.patches.v1_0.move_salary_register_reports av_tools.patches.v1_0.move_financial_and_trade_reports diff --git a/av_tools/patches/custom_fields/custom_fields_json/account_item_custom_field.json b/av_tools/patches/custom_fields/custom_fields_json/account_item_custom_field.json new file mode 100644 index 0000000..b656ddd --- /dev/null +++ b/av_tools/patches/custom_fields/custom_fields_json/account_item_custom_field.json @@ -0,0 +1,43 @@ +[ + { + "name": "Account-item", + "module": "Av Tools", + "doctype": "Custom Field", + "dt": "Account", + "fieldname": "item", + "fieldtype": "Link", + "label": "Expense Item", + "options": "Item", + "insert_after": "include_in_gross", + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "is_system_generated": 0, + "is_virtual": 0, + "length": 0, + "no_copy": 0, + "non_negative": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0 + } +] diff --git a/av_tools/patches/v1_0/move_indirect_expense_item_feature.py b/av_tools/patches/v1_0/move_indirect_expense_item_feature.py new file mode 100644 index 0000000..2bfb940 --- /dev/null +++ b/av_tools/patches/v1_0/move_indirect_expense_item_feature.py @@ -0,0 +1,10 @@ +import frappe + + +CUSTOM_FIELDS = ("Account-item",) + + +def execute(): + for cf_name in CUSTOM_FIELDS: + if frappe.db.exists("Custom Field", cf_name): + frappe.db.set_value("Custom Field", cf_name, "module", "Av Tools")