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/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
+}
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)
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)
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/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);
-});
+ });
+})();
diff --git a/av_tools/weigh_bridge/api.py b/av_tools/weigh_bridge/api.py
index 172a8a7..af13ca4 100644
--- a/av_tools/weigh_bridge/api.py
+++ b/av_tools/weigh_bridge/api.py
@@ -62,11 +62,22 @@ 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()
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 and item_code in allowed_transport_items:
+ kept_rows.append(row)
continue
ticket_row = matches.pop(0)
@@ -90,6 +101,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 +127,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 +200,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 +308,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 +321,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 +357,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_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_ticket/weighbridge_ticket.js b/av_tools/weigh_bridge/doctype/weighbridge_ticket/weighbridge_ticket.js
index 5da4bd2..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,13 +139,11 @@ const add_create_buttons = (frm) => {
return;
}
- if (frm.doc.target_document_reference) {
+ const targets = CREATE_TARGETS_BY_SOURCE[frm.doc.document_type || "Sales Order"];
+ 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(
__(targetDoctype),
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",
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.
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
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"