From b3a26a04e3b321ae542e7660345273707a72ecc0 Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Tue, 30 Jun 2026 11:41:37 +0300 Subject: [PATCH 01/12] refactor: remove redundant target document fields and link weighbridge ticket to target child items via custom fields --- av_tools/weigh_bridge/api.py | 37 ++----------------- av_tools/weigh_bridge/custom_fields.py | 21 +++++++++++ .../weighbridge_ticket/weighbridge_ticket.js | 4 -- .../weighbridge_ticket.json | 16 -------- 4 files changed, 25 insertions(+), 53 deletions(-) diff --git a/av_tools/weigh_bridge/api.py b/av_tools/weigh_bridge/api.py index 172a8a7..502e890 100644 --- a/av_tools/weigh_bridge/api.py +++ b/av_tools/weigh_bridge/api.py @@ -90,6 +90,8 @@ def _apply_ticket_items_to_target(target_doc, ticket_doc): if ticket_row.get("so_detail") and hasattr(row, "so_detail"): row.so_detail = ticket_row.get("so_detail") + row.weighbridge_ticket = ticket_doc.name + kept_rows.append(row) # Add remaining ticket items that didn't exist on mapped target. @@ -114,6 +116,8 @@ def _apply_ticket_items_to_target(target_doc, ticket_doc): if ticket_row.get("so_detail") and hasattr(child, "so_detail"): child.so_detail = ticket_row.get("so_detail") + child.weighbridge_ticket = ticket_doc.name + from erpnext.stock.get_item_details import get_item_details args = frappe._dict({ @@ -185,10 +189,6 @@ def make_target_from_ticket(source_name): if ticket.supplier and target.meta.has_field("supplier"): target.supplier = ticket.supplier - # Link ticket so existing validation + UI continue to work. - if target.meta.has_field("weighbridge_ticket"): - target.weighbridge_ticket = ticket.name - # Prefer ticket posting date/time when target supports them. if ticket.get("posting_date") and target.meta.has_field("posting_date"): target.posting_date = ticket.posting_date @@ -297,21 +297,6 @@ def get_ticket_items(ticket, doctype=None, document_name=None): f"Cannot use Weighbridge Ticket {doc.name} from {doc.document_type} as target {doctype}." ) - if ( - doctype - and doc.target_document_type - and doc.target_document_type != doctype - and not is_source_request - ): - frappe.throw("Weighbridge Ticket target document type does not match.") - if ( - document_name - and doc.target_document_reference - and doc.target_document_reference != document_name - and not is_source_request - ): - frappe.throw("Weighbridge Ticket belongs to another document.") - if ( document_name and doctype @@ -325,18 +310,6 @@ def get_ticket_items(ticket, doctype=None, document_name=None): f"Weighbridge source {doc.document_type} can only create: {', '.join(sorted(allowed_targets)) or 'None'}." ) - frappe.db.set_value( - "Weighbridge Ticket", - doc.name, - { - "target_document_type": doctype, - "target_document_reference": document_name, - }, - update_modified=True, - ) - doc.target_document_type = doctype - doc.target_document_reference = document_name - so_name = None so_details_by_item_code = {} if doc.document_type == "Sales Order" and doc.document_reference: @@ -373,8 +346,6 @@ def get_ticket_items(ticket, doctype=None, document_name=None): "items": items, "document_type": doc.document_type, "document_reference": doc.document_reference, - "target_document_type": doc.target_document_type, - "target_document_reference": doc.target_document_reference, "company": doc.company, "customer": doc.customer, "supplier": doc.supplier, diff --git a/av_tools/weigh_bridge/custom_fields.py b/av_tools/weigh_bridge/custom_fields.py index c2b3052..990468a 100644 --- a/av_tools/weigh_bridge/custom_fields.py +++ b/av_tools/weigh_bridge/custom_fields.py @@ -14,4 +14,25 @@ def setup_custom_fields(): ], } + item_doctypes = [ + "Sales Invoice Item", + "Delivery Note Item", + "Sales Order Item", + "Purchase Order Item", + "Purchase Invoice Item", + "Purchase Receipt Item", + ] + + for dt in item_doctypes: + custom_fields[dt] = [ + { + "fieldname": "weighbridge_ticket", + "label": "Weighbridge Ticket", + "fieldtype": "Link", + "options": "Weighbridge Ticket", + "insert_after": "item_code", + "read_only": 1, + } + ] + create_custom_fields(custom_fields, update=True) diff --git a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js index 5da4bd2..530d5d0 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js +++ b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js @@ -139,10 +139,6 @@ const add_create_buttons = (frm) => { return; } - if (frm.doc.target_document_reference) { - return; - } - const targets = CREATE_TARGETS_BY_SOURCE[frm.doc.document_type] || CREATE_TARGET_DOCTYPES; diff --git a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.json b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.json index eca3225..389d750 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.json +++ b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.json @@ -14,8 +14,6 @@ "column_break_reference", "document_type", "document_reference", - "target_document_type", - "target_document_reference", "column_break_tmkr", "posting_date", "posting_time", @@ -77,20 +75,6 @@ "label": "Document Reference", "options": "document_type" }, - { - "fieldname": "target_document_type", - "fieldtype": "Select", - "label": "Target Document Type", - "options": "\nSales Invoice\nDelivery Note\nSales Order\nPurchase Order\nPurchase Invoice\nPurchase Receipt", - "read_only": 1 - }, - { - "fieldname": "target_document_reference", - "fieldtype": "Dynamic Link", - "label": "Target Document Reference", - "options": "target_document_type", - "read_only": 1 - }, { "depends_on": "eval: ['Sales Invoice','Delivery Note','Sales Order'].includes(doc.document_type)", "fieldname": "customer", From ea7566bbdc0ea8e41a3435b3d54d7a20bed8946b Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Tue, 30 Jun 2026 13:31:09 +0300 Subject: [PATCH 02/12] refactor: remove target document type validation logic from WeighbridgeTicket --- .../weighbridge_ticket/weighbridge_ticket.js | 14 ++++--------- .../weighbridge_ticket/weighbridge_ticket.py | 21 +------------------ 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js index 530d5d0..ee27786 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js +++ b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js @@ -1,14 +1,6 @@ // Copyright (c) 2026, Aakvatech and contributors // For license information, please see license.txt -const CREATE_TARGET_DOCTYPES = [ - "Sales Invoice", - "Delivery Note", - "Sales Order", - "Purchase Order", - "Purchase Invoice", - "Purchase Receipt", -]; const CREATE_TARGETS_BY_SOURCE = { "Sales Order": ["Sales Invoice"], "Delivery Note": ["Sales Invoice"], @@ -139,8 +131,10 @@ const add_create_buttons = (frm) => { return; } - const targets = - CREATE_TARGETS_BY_SOURCE[frm.doc.document_type] || CREATE_TARGET_DOCTYPES; + const targets = CREATE_TARGETS_BY_SOURCE[frm.doc.document_type]; + if (!targets || !targets.length) { + return; + } targets.forEach((targetDoctype) => { frm.add_custom_button( diff --git a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.py b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.py index ed70533..d148d6e 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.py +++ b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.py @@ -15,12 +15,7 @@ "Purchase Receipt", } -ALLOWED_TARGETS_BY_SOURCE = { - "Sales Order": {"Sales Invoice"}, - "Delivery Note": {"Sales Invoice"}, - "Purchase Order": {"Purchase Invoice"}, - "Purchase Receipt": {"Purchase Invoice"}, -} + def _build_qty_map(rows): @@ -35,7 +30,6 @@ def _build_qty_map(rows): class WeighbridgeTicket(Document): def validate(self): - self.validate_target_mapping() self.validate_items_against_reference() def on_submit(self): @@ -53,19 +47,6 @@ def update_vehicle_default_tare(self): def on_cancel(self): self.clear_reference_document_link() - def validate_target_mapping(self): - source_type = self.document_type - target_type = self.target_document_type - - if source_type and target_type and source_type == target_type: - frappe.throw("Source and Target document types cannot be the same.") - - if source_type and target_type: - allowed_targets = ALLOWED_TARGETS_BY_SOURCE.get(source_type, set()) - if target_type not in allowed_targets: - frappe.throw( - f"Source {source_type} can only create: {', '.join(sorted(allowed_targets)) or 'None'}." - ) def validate_items_against_reference(self): # Reference is optional. Only validate against source document when provided. From 9e68cc0a097786b3dc39a0ef9bd42eff8bd25899 Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Wed, 1 Jul 2026 07:36:11 +0300 Subject: [PATCH 03/12] feat: add fallback to default target doctypes when source-specific targets are undefined --- .../weighbridge_ticket/weighbridge_ticket.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js index ee27786..530d5d0 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js +++ b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js @@ -1,6 +1,14 @@ // Copyright (c) 2026, Aakvatech and contributors // For license information, please see license.txt +const CREATE_TARGET_DOCTYPES = [ + "Sales Invoice", + "Delivery Note", + "Sales Order", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", +]; const CREATE_TARGETS_BY_SOURCE = { "Sales Order": ["Sales Invoice"], "Delivery Note": ["Sales Invoice"], @@ -131,10 +139,8 @@ const add_create_buttons = (frm) => { return; } - const targets = CREATE_TARGETS_BY_SOURCE[frm.doc.document_type]; - if (!targets || !targets.length) { - return; - } + const targets = + CREATE_TARGETS_BY_SOURCE[frm.doc.document_type] || CREATE_TARGET_DOCTYPES; targets.forEach((targetDoctype) => { frm.add_custom_button( From 3250188732d9be699e8b2f2fc6308bda58d24ab3 Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Wed, 1 Jul 2026 08:39:32 +0300 Subject: [PATCH 04/12] fix: add fallback to Sales Order and validation for empty targets in weighbridge ticket creation logic --- .../doctype/weighbridge_ticket/weighbridge_ticket.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js index 530d5d0..93fec96 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js +++ b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js @@ -139,8 +139,10 @@ const add_create_buttons = (frm) => { return; } - const targets = - CREATE_TARGETS_BY_SOURCE[frm.doc.document_type] || CREATE_TARGET_DOCTYPES; + const targets = CREATE_TARGETS_BY_SOURCE[frm.doc.document_type || "Sales Order"]; + if (!targets || !targets.length) { + return; + } targets.forEach((targetDoctype) => { frm.add_custom_button( From bccad036f0a53af76e0066ef54ddbbe9b0305efa Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Wed, 1 Jul 2026 21:49:51 +0300 Subject: [PATCH 05/12] feat: extend account-to-item sync to support Indirect Income in addition to Indirect Expenses --- av_tools/av_tools/account.js | 4 +- av_tools/av_tools_hooks/account.py | 68 ++++++++++++++++++------------ 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/av_tools/av_tools/account.js b/av_tools/av_tools/account.js index dfdec8e..87efb2f 100644 --- a/av_tools/av_tools/account.js +++ b/av_tools/av_tools/account.js @@ -5,7 +5,7 @@ frappe.ui.form.on("Account", { frm.set_query("item", function() { return { "filters": { - "item_group": "Indirect Expenses" + "item_group": ["in", ["Indirect Expenses", "Indirect Income"]] } }; }); @@ -16,7 +16,7 @@ frappe.ui.form.on("Account", { 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() { + frm.add_custom_button(__("Create Expense/Income Item"), function() { frappe.call({ method: 'av_tools.av_tools_hooks.account.add_indirect_expense_item', args: { diff --git a/av_tools/av_tools_hooks/account.py b/av_tools/av_tools_hooks/account.py index e48282f..30f5ab6 100644 --- a/av_tools/av_tools_hooks/account.py +++ b/av_tools/av_tools_hooks/account.py @@ -15,64 +15,78 @@ def create_indirect_expense_item(doc, method=None): 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 - ): + is_income = doc.root_type == "Income" + is_expense = doc.root_type == "Expense" + + if not doc.company or doc.is_group or not (is_income or is_expense): return - if ( - not doc.parent_account - and not check_expenses_in_parent_accounts(doc.account_name) - and doc.item - ): - doc.item = "" + + if not check_expenses_in_parent_accounts(doc.name): + # Unlink the item if it was moved out of Indirect Expenses/Income + if 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( + + item_group_name = "Indirect Income" if is_income else "Indirect Expenses" + if not frappe.db.exists("Item Group", item_group_name): + ig = frappe.get_doc( dict( doctype="Item Group", - item_group_name="Indirect Expenses", + item_group_name=item_group_name, ) ) - indirect_expenses_group.flags.ignore_permissions = True + ig.flags.ignore_permissions = True frappe.flags.ignore_account_permission = True - indirect_expenses_group.save() + ig.save() + item = frappe.db.exists("Item", doc.account_name) if item: item = frappe.get_doc("Item", doc.account_name) doc.item = item.name + + if is_income: + item.is_sales_item = 1 + elif is_expense: + item.is_purchase_item = 1 + 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: + if is_expense and i.expense_account != doc.name: i.expense_account = doc.name item.save() + elif is_income and i.default_income_account != doc.name: + i.default_income_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 + if is_expense: + row.expense_account = doc.name + elif is_income: + row.default_income_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", + item_group=item_group_name, is_stock_item=0, - is_sales_item=0, + is_sales_item=1 if is_income else 0, + is_purchase_item=1 if is_expense else 0, stock_uom="Nos", include_item_in_manufacturing=0, item_defaults=[ { "company": doc.company, - "expense_account": doc.name, + "expense_account": doc.name if is_expense else "", + "default_income_account": doc.name if is_income else "", "default_warehouse": "", } ], @@ -94,13 +108,13 @@ def create_indirect_expense_item(doc, method=None): 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): + if "Indirect Expenses" in str(parent_account_1) or "Indirect Income" 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): + if "Indirect Expenses" in str(parent_account_2) or "Indirect Income" 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): + if "Indirect Expenses" in str(parent_account_3) or "Indirect Income" in str(parent_account_3): return True return False @@ -109,7 +123,7 @@ def check_expenses_in_parent_accounts(account_name): 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.") + _("Indirect Expense/Income Item auto-creation is disabled. Enable it in AV Tools Settings.") ) account = frappe.get_doc("Account", account_name) return create_indirect_expense_item(account) From e4ae1fa02024a53ebb864979af72aeb9148dbec5 Mon Sep 17 00:00:00 2001 From: MariamMabele Date: Wed, 1 Jul 2026 17:29:18 +0300 Subject: [PATCH 06/12] fix(av_tools): remove duplicate amended_from field from inter company material request (cherry picked from commit 6b7b3c8413a0fccb101d22382799147e291857a3) --- .../inter_company_material_request.json | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/av_tools/av_tools/doctype/inter_company_material_request/inter_company_material_request.json b/av_tools/av_tools/doctype/inter_company_material_request/inter_company_material_request.json index 3980cba..2b59b19 100644 --- a/av_tools/av_tools/doctype/inter_company_material_request/inter_company_material_request.json +++ b/av_tools/av_tools/doctype/inter_company_material_request/inter_company_material_request.json @@ -92,21 +92,13 @@ "fieldname": "section_break_byxrx", "fieldtype": "Section Break" }, + { "fieldname": "amended_from", "fieldtype": "Link", "label": "Amended From", "no_copy": 1, - "options": "Inter Company Stock Transfer", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Inter Company Stock Transfer", + "options": "Inter Company Material Request", "print_hide": 1, "read_only": 1 }, From 2276c082f9a932f1c53e23ca6fa95de226679e5e Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Mon, 13 Jul 2026 16:45:27 +0300 Subject: [PATCH 07/12] feat: allow non-stock items to be included in weigh bridge processing --- av_tools/weigh_bridge/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/av_tools/weigh_bridge/api.py b/av_tools/weigh_bridge/api.py index 502e890..5a5bd97 100644 --- a/av_tools/weigh_bridge/api.py +++ b/av_tools/weigh_bridge/api.py @@ -67,6 +67,10 @@ def _apply_ticket_items_to_target(target_doc, ticket_doc): item_code = (row.get("item_code") or "").strip() matches = ticket_by_item_code.get(item_code) if item_code else None if not matches: + if item_code: + is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") + if not is_stock_item: + kept_rows.append(row) continue ticket_row = matches.pop(0) From 8ccd138866361dc6d60106286dd318989d5ee29b Mon Sep 17 00:00:00 2001 From: Emanuel Kagombora Date: Sat, 6 Jun 2026 21:50:00 +0300 Subject: [PATCH 08/12] chore(pyproject.toml): add Frappe version range (cherry picked from commit ab6685c844e593bcadfb46eda8d039e30b7ce834) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 97e07ed..54cf22f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,9 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" +[tool.bench.frappe-dependencies] +frappe = ">=15.0.0,<16.0.0" + # These dependencies are only installed when developer mode is enabled [tool.bench.dev-dependencies] # package_name = "~=1.1.0" From 4bcf8f3fa393dd2c47084e226bccd67e897e5251 Mon Sep 17 00:00:00 2001 From: MariamMabele Date: Sat, 18 Jul 2026 17:19:34 +0300 Subject: [PATCH 09/12] fix: grant Desk User read access to AV Tools Settings (cherry picked from commit 1bff3a79248ca8f0a08a8eb95b758fbde97ac0db) --- .../doctype/av_tools_settings/av_tools_settings.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 5901ac7..7b85f4d 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 @@ -211,6 +211,10 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "read": 1, + "role": "Desk User" } ], "row_format": "Dynamic", @@ -218,4 +222,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From f5d0d354c0d20a120164b58017df732de4ff697c Mon Sep 17 00:00:00 2001 From: Emanuel Kagombora Date: Wed, 15 Jul 2026 12:11:23 +0300 Subject: [PATCH 10/12] fix: use dict.get for barcode row fields to avoid KeyError. (cherry picked from commit d35a782df40597444fe926782e0a07a74c4acae0) --- .../item_barcode_update_tool/item_barcode_update_tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/av_tools/av_tools/doctype/item_barcode_update_tool/item_barcode_update_tool.py b/av_tools/av_tools/doctype/item_barcode_update_tool/item_barcode_update_tool.py index f1d9940..298f246 100644 --- a/av_tools/av_tools/doctype/item_barcode_update_tool/item_barcode_update_tool.py +++ b/av_tools/av_tools/doctype/item_barcode_update_tool/item_barcode_update_tool.py @@ -28,9 +28,9 @@ def update_barcodes(doc): item.append( "barcodes", { - "barcode": barcode["barcode"], - "barcode_type": barcode["barcode_type"], - "uom": barcode["uom"], + "barcode": barcode.get("barcode"), + "barcode_type": barcode.get("barcode_type"), + "uom": barcode.get("uom"), }, ) item.save(ignore_permissions=True) From f8ccc9bca1a33ba1c34029eb69f2e3cb41e8be49 Mon Sep 17 00:00:00 2001 From: Emanuel Kagombora Date: Thu, 6 Aug 2026 09:03:32 +0300 Subject: [PATCH 11/12] feat: route accounts receivable summary customers to general ledger (cherry picked from commit 2dff0af364c6cfb1379592f461f2af2ce3b6b152) --- .../js/financial_statements_override.js | 205 +++++++++++++----- 1 file changed, 155 insertions(+), 50 deletions(-) diff --git a/av_tools/public/js/financial_statements_override.js b/av_tools/public/js/financial_statements_override.js index 7039533..6325eeb 100644 --- a/av_tools/public/js/financial_statements_override.js +++ b/av_tools/public/js/financial_statements_override.js @@ -1,63 +1,168 @@ -function install_financial_statements_override() { - if (typeof erpnext === "undefined" || !erpnext.financial_statements) { - return false; +(function () { + frappe.provide("av_tools.financial_statements"); + + const ACCOUNTS_RECEIVABLE_SUMMARY = "Accounts Receivable Summary"; + const GENERAL_LEDGER = "General Ledger"; + const VOUCHER_CONSOLIDATED = "Categorize by Voucher (Consolidated)"; + + function get_report_filter_value(fieldname) { + return frappe.query_report ? frappe.query_report.get_filter_value(fieldname, false) : null; } - if (erpnext.financial_statements.__av_tools_override_installed) { - return true; + + function get_year_start(date_value) { + const date = date_value || frappe.datetime.get_today(); + return `${date.slice(0, 4)}-01-01`; } - const original_open_general_ledger = erpnext.financial_statements.open_general_ledger; - - erpnext.financial_statements.open_general_ledger = function (data) { - if (!data.account && !data.accounts) return; - - function navigate_based_on_type(account_type) { - if (account_type === "Receivable") { - frappe.route_options = { - company: frappe.query_report.get_filter_value("company"), - report_date: data.to_date || data.year_end_date, - ageing_based_on: "Posting Date", - }; - frappe.set_route("query-report", "Accounts Receivable Summary"); - } else if (account_type === "Payable") { - frappe.route_options = { - company: frappe.query_report.get_filter_value("company"), - report_date: data.to_date || data.year_end_date, - ageing_based_on: "Posting Date", - }; - frappe.set_route("query-report", "Accounts Payable Summary"); - } else { - original_open_general_ledger(data); + function retry_installer(installer, interval_key) { + if (installer() || av_tools.financial_statements[interval_key]) return; + + let attempts = 0; + av_tools.financial_statements[interval_key] = setInterval(function () { + attempts++; + + if (installer() || attempts > 50) { + clearInterval(av_tools.financial_statements[interval_key]); + av_tools.financial_statements[interval_key] = null; + } + }, 200); + } + + av_tools.financial_statements.open_customer_general_ledger = function (data) { + if (!data || data.party_type !== "Customer" || !data.party) return; + + const to_date = get_report_filter_value("report_date") || frappe.datetime.get_today(); + + frappe.route_options = { + company: get_report_filter_value("company"), + from_date: get_year_start(to_date), + to_date: to_date, + party_type: "Customer", + party: data.party, + categorize_by: VOUCHER_CONSOLIDATED, + }; + + ["finance_book", "cost_center", "project"].forEach(function (fieldname) { + const value = get_report_filter_value(fieldname); + if (value) { + frappe.route_options[fieldname] = value; } + }); + + frappe.set_route("query-report", GENERAL_LEDGER); + }; + + function install_accounts_receivable_summary_override() { + const report = frappe.query_reports && frappe.query_reports[ACCOUNTS_RECEIVABLE_SUMMARY]; + + if (!report || report.__av_tools_customer_gl_override_installed) { + return Boolean(report); } - if (data.account_type) { - navigate_based_on_type(data.account_type); - } else { - let account_name = data.account || data.accounts; - frappe.db.get_value("Account", account_name, "account_type", function (r) { - if (r && r.account_type) { - navigate_based_on_type(r.account_type); + const original_formatter = report.formatter; + + report.formatter = function (value, row, column, data, default_formatter, filter) { + if (column.fieldname === "party") { + if (data && data.party_type === "Customer" && data.party) { + column.link_onclick = + "av_tools.financial_statements.open_customer_general_ledger(" + + JSON.stringify({ party_type: data.party_type, party: data.party }) + + ")"; } else { - original_open_general_ledger(data); + delete column.link_onclick; } - }); + } + + if (original_formatter) { + return original_formatter.call(this, value, row, column, data, default_formatter, filter); + } + + return default_formatter(value, row, column, data); + }; + + report.__av_tools_customer_gl_override_installed = true; + return true; + } + + function install_financial_statements_override() { + if (typeof erpnext === "undefined" || !erpnext.financial_statements) { + return false; } - }; + if (erpnext.financial_statements.__av_tools_override_installed) { + return true; + } + + const original_open_general_ledger = erpnext.financial_statements.open_general_ledger; + + erpnext.financial_statements.open_general_ledger = function (data) { + if (!data.account && !data.accounts) return; + + function navigate_based_on_type(account_type) { + if (account_type === "Receivable") { + frappe.route_options = { + company: frappe.query_report.get_filter_value("company"), + report_date: data.to_date || data.year_end_date, + ageing_based_on: "Posting Date", + }; + frappe.set_route("query-report", ACCOUNTS_RECEIVABLE_SUMMARY); + } else if (account_type === "Payable") { + frappe.route_options = { + company: frappe.query_report.get_filter_value("company"), + report_date: data.to_date || data.year_end_date, + ageing_based_on: "Posting Date", + }; + frappe.set_route("query-report", "Accounts Payable Summary"); + } else { + original_open_general_ledger(data); + } + } + + if (data.account_type) { + navigate_based_on_type(data.account_type); + } else { + const account_name = data.account || data.accounts; + frappe.db.get_value("Account", account_name, "account_type", function (r) { + if (r && r.account_type) { + navigate_based_on_type(r.account_type); + } else { + original_open_general_ledger(data); + } + }); + } + }; + + erpnext.financial_statements.__av_tools_override_installed = true; + return true; + } + + function retry_financial_statement_override() { + retry_installer(install_financial_statements_override, "financial_statements_retry_interval"); + } + + function retry_accounts_receivable_summary_override() { + retry_installer( + install_accounts_receivable_summary_override, + "accounts_receivable_summary_retry_interval" + ); + } - erpnext.financial_statements.__av_tools_override_installed = true; - return true; -} + function is_accounts_receivable_summary_route() { + const route = frappe.get_route(); + return route && route[0] === "query-report" && route[1] === ACCOUNTS_RECEIVABLE_SUMMARY; + } + + $(document).on("app_ready startup", retry_financial_statement_override); -$(document).on("app_ready startup", install_financial_statements_override); + frappe.router.on("change", function () { + if (is_accounts_receivable_summary_route()) { + retry_accounts_receivable_summary_override(); + } + }); -$(function () { - if (install_financial_statements_override()) return; - let attempts = 0; - const interval = setInterval(function () { - attempts++; - if (install_financial_statements_override() || attempts > 50) { - clearInterval(interval); + $(function () { + retry_financial_statement_override(); + if (is_accounts_receivable_summary_route()) { + retry_accounts_receivable_summary_override(); } - }, 200); -}); + }); +})(); From 6baadecfde9cbef4659603048e52209e71c40fff Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Thu, 13 Aug 2026 08:14:48 +0300 Subject: [PATCH 12/12] feat(weigh_bridge): add configurable transport items setting for target sales invoice generation --- av_tools/av_tools_hooks/parallel_approval.py | 159 +++++++++++++----- av_tools/weigh_bridge/api.py | 9 +- .../weighbridge_settings.json | 15 +- .../weighbridge_transport_item/__init__.py | 1 + .../weighbridge_transport_item.json | 53 ++++++ .../weighbridge_transport_item.py | 8 + 6 files changed, 199 insertions(+), 46 deletions(-) create mode 100644 av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py create mode 100644 av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json create mode 100644 av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py diff --git a/av_tools/av_tools_hooks/parallel_approval.py b/av_tools/av_tools_hooks/parallel_approval.py index cf3f61a..5892438 100644 --- a/av_tools/av_tools_hooks/parallel_approval.py +++ b/av_tools/av_tools_hooks/parallel_approval.py @@ -2,7 +2,6 @@ from frappe import _ from frappe.custom.doctype.custom_field.custom_field import ( create_custom_fields, - delete_custom_fields, ) from frappe import share as frappe_share @@ -17,7 +16,9 @@ def _get_approval_doctypes() -> set: if getattr(frappe.flags, "in_migrate", False): return set() - if not frappe.db.get_single_value("AV Tools Settings", "enable_multi_approval_document"): + if not frappe.db.get_single_value( + "AV Tools Settings", "enable_multi_approval_document" + ): return set() if not frappe.db.table_exists("Approval Doctypes"): @@ -31,7 +32,9 @@ def _get_approval_doctypes() -> set: return set(rows) except Exception: - frappe.log_error(frappe.get_traceback(), "Parallel Approval: _get_approval_doctypes failed") + frappe.log_error( + frappe.get_traceback(), "Parallel Approval: _get_approval_doctypes failed" + ) return set() @@ -61,12 +64,21 @@ def sync_approver_shares(doc, method=None): for row in approver_rows: if row.approver: desired.add(row.approver) - user_info[row.approver] = {"name": row.approver_name or row.approver, "position": row.position or ""} + user_info[row.approver] = { + "name": row.approver_name or row.approver, + "position": row.position or "", + } if row.delegate_to: desired.add(row.delegate_to) if row.delegate_to not in user_info: - delegate_name = frappe.get_cached_value("User", row.delegate_to, "full_name") or row.delegate_to - user_info[row.delegate_to] = {"name": delegate_name, "position": row.position or ""} + delegate_name = ( + frappe.get_cached_value("User", row.delegate_to, "full_name") + or row.delegate_to + ) + user_info[row.delegate_to] = { + "name": delegate_name, + "position": row.position or "", + } # Capture existing DocShare users BEFORE adding new ones to detect new additions existing = frappe.get_all( @@ -78,7 +90,9 @@ def sync_approver_shares(doc, method=None): flags = {"ignore_share_permission": True} for user in desired: - frappe_share.add_docshare(doc.doctype, doc.name, user, read=1, write=0, flags=flags) + frappe_share.add_docshare( + doc.doctype, doc.name, user, read=1, write=0, flags=flags + ) if user not in existing_users: _notify_approver_added(user, user_info.get(user, {}), doc) @@ -97,11 +111,20 @@ def _notify_approver_added(user, info, doc): route = frappe.scrub(doc.doctype).replace("_", "-") doc_url = f"{site_url}/app/{route}/{doc.name}" - added_by = frappe.get_cached_value("User", frappe.session.user, "full_name") or frappe.session.user + added_by = ( + frappe.get_cached_value("User", frappe.session.user, "full_name") + or frappe.session.user + ) - subject = _("You have been added as an approver — {0} {1}").format(doc.doctype, doc.name) + subject = _("You have been added as an approver — {0} {1}").format( + doc.doctype, doc.name + ) - position_line = f"

{_('Position')}: {frappe.utils.escape_html(position)}

" if position else "" + position_line = ( + f"

{_('Position')}: {frappe.utils.escape_html(position)}

" + if position + else "" + ) message = f"""

{_("Dear")} {frappe.utils.escape_html(display_name)},

@@ -126,7 +149,9 @@ def _notify_approver_added(user, info, doc): now=True, ) except Exception: - frappe.log_error(frappe.get_traceback(), "Parallel Approval: approver email failed") + frappe.log_error( + frappe.get_traceback(), "Parallel Approval: approver email failed" + ) def block_submit_if_not_approved(doc, method=None): @@ -196,7 +221,9 @@ def create_approval_fields(doctype: str) -> None: def delete_approval_fields(doctype: str) -> None: - delete_custom_fields({doctype: [_APPROVER_TAB, _APPROVER_TABLE]}) + for fieldname in [_APPROVER_TAB, _APPROVER_TABLE]: + frappe.db.delete("Custom Field", {"dt": doctype, "fieldname": fieldname}) + frappe.clear_cache(doctype=doctype) def clear_approval_cache() -> None: @@ -285,13 +312,15 @@ def create_approver_qr_print_format(doctype: str) -> None: continue if any(f.get("fieldname") == _QR_FIELD_NAME for f in fd): continue - fd.append({ - "fieldname": _QR_FIELD_NAME, - "fieldtype": "HTML", - "label": "Approver QR Codes", - "print_hide": 0, - "options": _QR_PRINT_FORMAT_HTML, - }) + fd.append( + { + "fieldname": _QR_FIELD_NAME, + "fieldtype": "HTML", + "label": "Approver QR Codes", + "print_hide": 0, + "options": _QR_PRINT_FORMAT_HTML, + } + ) pf.format_data = json.dumps(fd) pf.save(ignore_permissions=True) @@ -334,7 +363,9 @@ def boot_session(bootinfo): try: bootinfo.parallel_approval_doctypes = list(_get_approval_doctypes()) except Exception: - frappe.log_error(frappe.get_traceback(), "Parallel Approval: boot_session failed") + frappe.log_error( + frappe.get_traceback(), "Parallel Approval: boot_session failed" + ) bootinfo.parallel_approval_doctypes = [] @@ -352,7 +383,7 @@ def submit_approval_action(doctype, docname, action, reason=None): current_user = frappe.session.user approver_row = None - for row in (getattr(doc, _APPROVER_TABLE, None) or []): + for row in getattr(doc, _APPROVER_TABLE, None) or []: if row.approver == current_user or row.delegate_to == current_user: approver_row = row break @@ -360,50 +391,90 @@ def submit_approval_action(doctype, docname, action, reason=None): if not approver_row: frappe.throw(_("You are not listed as an approver for this document.")) - is_expired = approver_row.expiry_date and frappe.utils.getdate(approver_row.expiry_date) < frappe.utils.getdate(frappe.utils.nowdate()) - if is_expired and current_user == approver_row.approver and approver_row.delegate_to: - frappe.throw(_( - "Your approval authority for this document expired on {0}. " - "This has been escalated to {1}." - ).format(frappe.utils.formatdate(approver_row.expiry_date), approver_row.delegate_to)) + is_expired = approver_row.expiry_date and frappe.utils.getdate( + approver_row.expiry_date + ) < frappe.utils.getdate(frappe.utils.nowdate()) + if ( + is_expired + and current_user == approver_row.approver + and approver_row.delegate_to + ): + frappe.throw( + _( + "Your approval authority for this document expired on {0}. " + "This has been escalated to {1}." + ).format( + frappe.utils.formatdate(approver_row.expiry_date), + approver_row.delegate_to, + ) + ) - acting_name = frappe.get_cached_value("User", current_user, "full_name") or current_user + acting_name = ( + frappe.get_cached_value("User", current_user, "full_name") or current_user + ) - acting_as_delegate = current_user == approver_row.delegate_to and current_user != approver_row.approver + acting_as_delegate = ( + current_user == approver_row.delegate_to + and current_user != approver_row.approver + ) if acting_as_delegate: - approver_name = frappe.get_cached_value("User", approver_row.approver, "full_name") or approver_row.approver + approver_name = ( + frappe.get_cached_value("User", approver_row.approver, "full_name") + or approver_row.approver + ) delegate_suffix = _(" (as delegate for {0})").format(approver_name) else: delegate_suffix = "" if action == "approve": - updates = {"approved": 1, "rejected": 0, "rejection_reason": "", "date": frappe.utils.now()} - comment_html = _("{0} reviewed and approved this document{1}.").format(acting_name, delegate_suffix) + updates = { + "approved": 1, + "rejected": 0, + "rejection_reason": "", + "date": frappe.utils.now(), + } + comment_html = _("{0} reviewed and approved this document{1}.").format( + acting_name, delegate_suffix + ) elif action == "reject": if not reason: frappe.throw(_("A rejection reason is required.")) safe_reason = frappe.utils.escape_html(reason) - updates = {"approved": 0, "rejected": 1, "rejection_reason": reason, "date": frappe.utils.now()} + updates = { + "approved": 0, + "rejected": 1, + "rejection_reason": reason, + "date": frappe.utils.now(), + } comment_html = _( "{0} rejected this document{1}.
Reason: {2}" ).format(acting_name, delegate_suffix, safe_reason) else: # clear_rejection - updates = {"approved": 0, "rejected": 0, "rejection_reason": "", "date": frappe.utils.now()} - comment_html = _("{0} cleared their rejection{1}.").format(acting_name, delegate_suffix) + updates = { + "approved": 0, + "rejected": 0, + "rejection_reason": "", + "date": frappe.utils.now(), + } + comment_html = _("{0} cleared their rejection{1}.").format( + acting_name, delegate_suffix + ) approver_row.update(updates) doc.save(ignore_permissions=True) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Comment", - "reference_doctype": doctype, - "reference_name": docname, - "comment_email": current_user, - "comment_by": acting_name, - "content": comment_html, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": doctype, + "reference_name": docname, + "comment_email": current_user, + "comment_by": acting_name, + "content": comment_html, + } + ).insert(ignore_permissions=True) return True diff --git a/av_tools/weigh_bridge/api.py b/av_tools/weigh_bridge/api.py index 5a5bd97..af13ca4 100644 --- a/av_tools/weigh_bridge/api.py +++ b/av_tools/weigh_bridge/api.py @@ -62,6 +62,13 @@ def _apply_ticket_items_to_target(target_doc, ticket_doc): continue ticket_by_item_code.setdefault(item_code, []).append(row) + settings = frappe.get_single("Weighbridge Settings") + allowed_transport_items = { + (row.item_code or "").strip() + for row in (settings.get("transport_items") or []) + if row.get("item_code") + } + kept_rows = [] for row in (target_doc.get("items") or []): item_code = (row.get("item_code") or "").strip() @@ -69,7 +76,7 @@ def _apply_ticket_items_to_target(target_doc, ticket_doc): if not matches: if item_code: is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") - if not is_stock_item: + if not is_stock_item and item_code in allowed_transport_items: kept_rows.append(row) continue diff --git a/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json b/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json index 42a3944..fa0bebf 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json +++ b/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json @@ -10,6 +10,8 @@ "timeout_seconds", "gateway_url", "read_weight_url", + "section_break_transport", + "transport_items", "section_break_device", "device_ip", "device_port", @@ -51,6 +53,17 @@ "fieldtype": "Data", "label": "Read Weight URL" }, + { + "fieldname": "section_break_transport", + "fieldtype": "Section Break", + "label": "Transport Items Configuration" + }, + { + "fieldname": "transport_items", + "fieldtype": "Table", + "label": "Allowed Transport / Service Items", + "options": "Weighbridge Transport Item" + }, { "fieldname": "section_break_device", "fieldtype": "Section Break", @@ -81,7 +94,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-02-09 00:00:00.000000", + "modified": "2026-08-13 08:00:00.000000", "modified_by": "Administrator", "module": "Weigh Bridge", "name": "Weighbridge Settings", diff --git a/av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py new file mode 100644 index 0000000..03d47fc --- /dev/null +++ b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py @@ -0,0 +1 @@ +# Package init diff --git a/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json new file mode 100644 index 0000000..9454493 --- /dev/null +++ b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json @@ -0,0 +1,53 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2026-08-13 08:00:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "description" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "read_only": 1 + }, + { + "fetch_from": "item_code.description", + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 0, + "label": "Description", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2026-08-13 08:00:00.000000", + "modified_by": "Administrator", + "module": "Weigh Bridge", + "name": "Weighbridge Transport Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py new file mode 100644 index 0000000..79fa7b7 --- /dev/null +++ b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2026, Av Tools and contributors +# For license information, please see license.txt + +from frappe.model.document import Document + + +class WeighbridgeTransportItem(Document): + pass