diff --git a/sf_trading/hooks.py b/sf_trading/hooks.py
index 61b8be1..6e9ccf3 100644
--- a/sf_trading/hooks.py
+++ b/sf_trading/hooks.py
@@ -33,6 +33,8 @@
"/assets/sf_trading/js/sales_invoice_barcode.js",
"/assets/sf_trading/js/sales_invoice_inter_company.js",
"/assets/sf_trading/js/sales_invoice_pos_total_popup.js",
+ "/assets/sf_trading/js/work_flow_rejection.js",
+ "/assets/sf_trading/js/workflow_approval_shortcut.js",
]
# include js, css files in header of web template
diff --git a/sf_trading/public/js/workflow_approval_shortcut.js b/sf_trading/public/js/workflow_approval_shortcut.js
new file mode 100644
index 0000000..08dd4da
--- /dev/null
+++ b/sf_trading/public/js/workflow_approval_shortcut.js
@@ -0,0 +1,62 @@
+// /sf_trading/sf_trading/public/js/workflow_approval_shortcut.js
+// Adds a pending count badge next to the "Work Flow Approval" shortcut
+// on any workspace, matching the native ERPNext shortcut count style.
+
+frappe.provide("sf_trading.wf_shortcut");
+
+sf_trading.wf_shortcut.update_count = function() {
+ let $links = $('.shortcut-widget-box a, .widget.shortcut-widget-box .widget-title')
+ .filter(function() {
+ return $(this).text().trim().toLowerCase().includes("work flow approval")
+ || $(this).text().trim().toLowerCase().includes("workflow approval");
+ });
+
+ if (!$links.length) return;
+
+ frappe.call({
+ method: "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.get_pending_approval_count",
+ callback: function(r) {
+ let count = r.message || 0;
+
+ $('.shortcut-widget-box').each(function() {
+ let $box = $(this);
+ let txt = $box.find('.widget-title, a').first().text().trim().toLowerCase();
+ if (!txt.includes("work flow approval") && !txt.includes("workflow approval")) return;
+
+ // Remove any previous injected badge
+ $box.find('.wf-pending-count').remove();
+
+ // Build badge in the same style as ERPNext's native shortcut count
+ let $badge = $(`${count}`);
+
+ // Append to the title/link
+ let $target = $box.find('.widget-title').first();
+ if (!$target.length) $target = $box.find('a').first();
+ $target.append($badge);
+ });
+ }
+ });
+};
+
+// Run after route changes (workspace navigation) and on initial load
+$(document).on("page-change app_ready", function() {
+ setTimeout(sf_trading.wf_shortcut.update_count, 600);
+ setTimeout(sf_trading.wf_shortcut.update_count, 1500); // retry for slow loads
+});
+
+frappe.after_ajax(function() {
+ setTimeout(sf_trading.wf_shortcut.update_count, 1000);
+});
+
+// Refresh every 60 seconds while the page is open
+setInterval(sf_trading.wf_shortcut.update_count, 60000);
\ No newline at end of file
diff --git a/sf_trading/sf_trading/doctype/branch_configuration/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js
new file mode 100644
index 0000000..3ad9aa2
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js
@@ -0,0 +1,47 @@
+// Copyright (c) 2026, Enfono and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Branch Configuration", {
+ refresh(frm) {
+ set_child_filters(frm);
+ },
+ company(frm) {
+ set_child_filters(frm);
+
+ // Clear warehouse + cost center + account fields when company changes
+ // (they may belong to the old company)
+ if (frm.doc.warehouse && frm.doc.warehouse.length) {
+ frm.clear_table("warehouse");
+ frm.refresh_field("warehouse");
+ }
+ if (frm.doc.cost_center && frm.doc.cost_center.length) {
+ frm.clear_table("cost_center");
+ frm.refresh_field("cost_center");
+ }
+ // MoPs are global, not company-bound — no need to clear on company change.
+ }
+});
+
+function set_child_filters(frm) {
+ frm.set_query("warehouse", "warehouse", function () {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query("cost_center", "cost_center", function () {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query("mode_of_payment", "mode_of_payment", function () {
+ return { filters: { type: ["in", ["Cash", "Bank"]] } };
+ });
+}
diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.json b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.json
new file mode 100644
index 0000000..29cb565
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.json
@@ -0,0 +1,94 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2026-05-04 18:33:59.085305",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "branch",
+ "column_break_jvyk",
+ "company",
+ "mode_of_payment_section",
+ "mode_of_payment",
+ "warehouse",
+ "cost_center",
+ "user"
+ ],
+ "fields": [
+ {
+ "fieldname": "branch",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Branch",
+ "options": "Branch",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_jvyk",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "mode_of_payment_section",
+ "fieldtype": "Section Break",
+ "label": "Mode of Payment"
+ },
+ {
+ "description": "Cash + Bank Modes of Payment allowed for this branch. On Sales Invoice for Branch Users,payment rows whose MoP type is Cash or Bank are constrained to this list (or swapped to the first matching row); BNPL is left untouched.",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Table",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment Account"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Table",
+ "label": "Warehouse",
+ "options": "Branch Configuration Warehouse"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Table",
+ "label": "Cost Center",
+ "options": "Branch Configuration Cost Center"
+ },
+ {
+ "fieldname": "user",
+ "fieldtype": "Table",
+ "label": "User",
+ "options": "Branch Configuration User"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2026-05-04 18:42:48.031006",
+ "modified_by": "Administrator",
+ "module": "Sf Trading",
+ "name": "Branch Configuration",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py
new file mode 100644
index 0000000..b405479
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py
@@ -0,0 +1,231 @@
+# User permissions are created for company, branch, warehouse and cost center.
+# Company and first warehouse are set as is_default=1 so Frappe uses them as
+# the user's Session Defaults instead of falling back to global defaults.
+# The "Branch User" role is auto-assigned to users added here.
+
+import frappe
+from frappe.model.document import Document
+
+BRANCH_USER_ROLE = "Branch User"
+
+
+class BranchConfiguration(Document):
+
+ def validate(self):
+ # Ensure warehouse and cost center belong to the selected company
+ if self.company:
+ for w in self.warehouse:
+ if w.warehouse:
+ wh_company = frappe.db.get_value("Warehouse", w.warehouse, "company")
+ if wh_company and wh_company != self.company:
+ frappe.throw(
+ f"Warehouse {w.warehouse} belongs to company {wh_company}, "
+ f"not {self.company}. Please select a warehouse from the correct company."
+ )
+
+ for c in self.cost_center:
+ if c.cost_center:
+ cc_company = frappe.db.get_value("Cost Center", c.cost_center, "company")
+ if cc_company and cc_company != self.company:
+ frappe.throw(
+ f"Cost Center {c.cost_center} belongs to company {cc_company}, "
+ f"not {self.company}. Please select a cost center from the correct company."
+ )
+
+ def before_save(self):
+ if self.is_new():
+ return
+
+ old_doc = self.get_doc_before_save()
+
+ old_users = {d.user for d in old_doc.user}
+ new_users = {d.user for d in self.user}
+
+ removed_users = old_users - new_users
+
+ for user in removed_users:
+ # Delete company permission
+ if old_doc.get("company"):
+ delete_permission(user, "Company", old_doc.company)
+
+ delete_permission(user, "Branch", old_doc.branch)
+
+ for w in old_doc.warehouse:
+ delete_permission(user, "Warehouse", w.warehouse)
+
+ for c in old_doc.cost_center:
+ delete_permission(user, "Cost Center", c.cost_center)
+
+ # Remove role if user is not in any other Branch Configuration
+ # Find what role they had in this branch
+ old_role = None
+ for old_u in old_doc.user:
+ if old_u.user == user:
+ old_role = old_u.get("role") or BRANCH_USER_ROLE
+ break
+ _maybe_remove_role(user, old_role or BRANCH_USER_ROLE, exclude_branch=self.name)
+
+ # Handle company change — remove old company permission for remaining users
+ old_company = old_doc.get("company")
+ new_company = self.get("company")
+ if old_company and old_company != new_company:
+ for u in self.user:
+ delete_permission(u.user, "Company", old_company)
+
+ def on_update(self):
+ self.create_permissions()
+
+ def create_permissions(self):
+ for u in self.user:
+ # Create company permission (marked as default so Session Defaults picks it)
+ if self.company:
+ create_permission(u.user, "Company", self.company, is_default=1)
+
+ create_permission(u.user, "Branch", self.branch)
+
+ for idx, w in enumerate(self.warehouse):
+ # First warehouse is the default
+ create_permission(u.user, "Warehouse", w.warehouse, is_default=1 if idx == 0 else 0)
+
+ for idx, c in enumerate(self.cost_center):
+ # First cost center is the default
+ create_permission(u.user, "Cost Center", c.cost_center, is_default=1 if idx == 0 else 0)
+
+ # Also grant access to the company's default cost center (used in tax templates)
+ # without marking it as default — the branch cost center stays as the user's default
+ if self.company:
+ company_default_cc = frappe.db.get_value("Company", self.company, "cost_center")
+ if company_default_cc:
+ create_permission(u.user, "Cost Center", company_default_cc, is_default=0)
+
+ # Auto-assign the selected role (Branch User, Warehouse User, or Stock User)
+ selected_role = u.get("role") or BRANCH_USER_ROLE
+ _assign_role(u.user, selected_role)
+
+ # Auto-set Module Profile to restrict sidebar modules
+ if selected_role == BRANCH_USER_ROLE:
+ _set_module_profile(u.user, "Branch User")
+ elif selected_role == "Damage User":
+ _set_module_profile(u.user, "Damage User")
+
+
+def create_permission(user, allow, value, is_default=0):
+ if not value:
+ return
+
+ existing = frappe.db.exists("User Permission", {
+ "user": user,
+ "allow": allow,
+ "for_value": value
+ })
+
+ if existing:
+ # Update is_default if needed — but only if no other default exists
+ if is_default and not _has_existing_default(user, allow, exclude_value=value):
+ frappe.db.set_value("User Permission", existing, "is_default", 1)
+ else:
+ # If we want to set default but one already exists, don't override it
+ if is_default and _has_existing_default(user, allow):
+ is_default = 0
+
+ doc = frappe.new_doc("User Permission")
+ doc.user = user
+ doc.allow = allow
+ doc.for_value = value
+ doc.is_default = is_default
+ doc.apply_to_all_doctypes = 1
+ doc.insert(ignore_permissions=True)
+
+
+def _has_existing_default(user, allow, exclude_value=None):
+ """Check if user already has a default User Permission for this allow type."""
+ filters = {
+ "user": user,
+ "allow": allow,
+ "is_default": 1,
+ }
+ if exclude_value:
+ filters["for_value"] = ["!=", exclude_value]
+
+ return frappe.db.exists("User Permission", filters)
+
+
+def delete_permission(user, allow, value):
+ if not value:
+ return
+
+ perms = frappe.get_all(
+ "User Permission",
+ filters={
+ "user": user,
+ "allow": allow,
+ "for_value": value
+ },
+ pluck="name"
+ )
+
+ for p in perms:
+ frappe.delete_doc("User Permission", p, ignore_permissions=True)
+
+
+def _ensure_system_user(user):
+ """Website Users cannot hold desk roles. Upgrade to System User so role takes effect."""
+ current_type = frappe.db.get_value("User", user, "user_type")
+ if current_type and current_type != "System User":
+ frappe.db.set_value("User", user, "user_type", "System User")
+
+
+def _assign_role(user, role):
+ """Assign the specified role if not already assigned. Uses direct DB for reliability."""
+ if not role or not frappe.db.exists("Role", role):
+ return
+
+ # Desk roles (Branch User / Stock User / Damage User / Stock Manager) require
+ # System User user_type; Website Users silently lose role assignments.
+ _ensure_system_user(user)
+
+ # Check if role already assigned
+ if frappe.db.exists("Has Role", {"parent": user, "role": role}):
+ return
+
+ # Direct insert — more reliable than user_doc.add_roles() which can fail
+ # during Branch Configuration save context
+ frappe.get_doc({
+ "doctype": "Has Role",
+ "parent": user,
+ "parenttype": "User",
+ "parentfield": "roles",
+ "role": role,
+ }).insert(ignore_permissions=True)
+
+
+def _set_module_profile(user, profile_name):
+ """Set the Module Profile on a user if not already set."""
+ if not frappe.db.exists("Module Profile", profile_name):
+ return
+ current = frappe.db.get_value("User", user, "module_profile")
+ if current != profile_name:
+ frappe.db.set_value("User", user, "module_profile", profile_name)
+
+
+def _maybe_remove_role(user, role, exclude_branch=None):
+ """Remove role if user is not in any other Branch Configuration with the same role."""
+ if not role:
+ return
+
+ other_configs = frappe.get_all(
+ "Branch Configuration User",
+ filters={"user": user},
+ fields=["parent", "role"],
+ )
+
+ # Check if user has this same role in any other Branch Configuration
+ has_role_elsewhere = any(
+ c.parent != exclude_branch and (c.get("role") or BRANCH_USER_ROLE) == role
+ for c in other_configs
+ )
+
+ if not has_role_elsewhere:
+ user_doc = frappe.get_doc("User", user)
+ if role in [r.role for r in user_doc.roles]:
+ user_doc.remove_roles(role)
diff --git a/sf_trading/sf_trading/doctype/branch_configuration/test_branch_configuration.py b/sf_trading/sf_trading/doctype/branch_configuration/test_branch_configuration.py
new file mode 100644
index 0000000..39c1427
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration/test_branch_configuration.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBranchConfiguration(FrappeTestCase):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.js b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.js
new file mode 100644
index 0000000..abffdac
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2026, enfono and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Branch Configuration Cost Center", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.json b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.json
new file mode 100644
index 0000000..50aef62
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.json
@@ -0,0 +1,34 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2026-05-04 18:31:01.031387",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "cost_center"
+ ],
+ "fields": [
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Cost Center",
+ "options": "Cost Center"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2026-05-04 18:41:12.932631",
+ "modified_by": "Administrator",
+ "module": "Sf Trading",
+ "name": "Branch Configuration Cost Center",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.py b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.py
new file mode 100644
index 0000000..e1f24cc
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/branch_configuration_cost_center.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BranchConfigurationCostCenter(Document):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_cost_center/test_branch_configuration_cost_center.py b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/test_branch_configuration_cost_center.py
new file mode 100644
index 0000000..41f1e69
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_cost_center/test_branch_configuration_cost_center.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBranchConfigurationCostCenter(FrappeTestCase):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.js b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.js
new file mode 100644
index 0000000..442a179
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2026, enfono and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Branch Configuration Mode of Payment", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.json b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.json
new file mode 100644
index 0000000..f85d738
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2026-05-04 18:24:35.941149",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "mode_of_payment",
+ "type"
+ ],
+ "fields": [
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "mode_of_payment.type",
+ "fetch_if_empty": 1,
+ "fieldname": "type",
+ "fieldtype": "Data",
+ "label": "Type"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2026-05-04 18:27:04.017025",
+ "modified_by": "Administrator",
+ "module": "Sf Trading",
+ "name": "Branch Configuration Mode of Payment",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.py b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.py
new file mode 100644
index 0000000..29d8807
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/branch_configuration_mode_of_payment.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BranchConfigurationModeofPayment(Document):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/test_branch_configuration_mode_of_payment.py b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/test_branch_configuration_mode_of_payment.py
new file mode 100644
index 0000000..4780c28
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_mode_of_payment/test_branch_configuration_mode_of_payment.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBranchConfigurationModeofPayment(FrappeTestCase):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_user/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.js b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.js
new file mode 100644
index 0000000..a98d5a1
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2026, enfono and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Branch Configuration User", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.json b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.json
new file mode 100644
index 0000000..8d264c3
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.json
@@ -0,0 +1,43 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2026-05-04 18:27:33.473806",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "role"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User",
+ "reqd": 1
+ },
+ {
+ "fieldname": "role",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Role",
+ "options": "Branch User\nStock User\nStock Manager\nDamage User"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2026-05-04 18:40:57.963773",
+ "modified_by": "Administrator",
+ "module": "Sf Trading",
+ "name": "Branch Configuration User",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.py b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.py
new file mode 100644
index 0000000..0172efe
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_user/branch_configuration_user.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BranchConfigurationUser(Document):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_user/test_branch_configuration_user.py b/sf_trading/sf_trading/doctype/branch_configuration_user/test_branch_configuration_user.py
new file mode 100644
index 0000000..396838d
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_user/test_branch_configuration_user.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBranchConfigurationUser(FrappeTestCase):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/__init__.py b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.js b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.js
new file mode 100644
index 0000000..d292758
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2026, enfono and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Branch Configuration Warehouse", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.json b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.json
new file mode 100644
index 0000000..e15b5f5
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.json
@@ -0,0 +1,34 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2026-05-04 18:29:57.062305",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "warehouse"
+ ],
+ "fields": [
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Warehouse",
+ "options": "Warehouse"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2026-05-04 18:40:44.055040",
+ "modified_by": "Administrator",
+ "module": "Sf Trading",
+ "name": "Branch Configuration Warehouse",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.py b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.py
new file mode 100644
index 0000000..2caaf64
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/branch_configuration_warehouse.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BranchConfigurationWarehouse(Document):
+ pass
diff --git a/sf_trading/sf_trading/doctype/branch_configuration_warehouse/test_branch_configuration_warehouse.py b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/test_branch_configuration_warehouse.py
new file mode 100644
index 0000000..a5bd8a6
--- /dev/null
+++ b/sf_trading/sf_trading/doctype/branch_configuration_warehouse/test_branch_configuration_warehouse.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2026, enfono and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBranchConfigurationWarehouse(FrappeTestCase):
+ pass
diff --git a/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.js b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.js
index 5176949..b466cc3 100644
--- a/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.js
+++ b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.js
@@ -1,13 +1,14 @@
+
let wf_action = "";
frappe.query_reports["Work Flow Approval"] = {
filters: [
{ fieldname: "user", label: "User", fieldtype: "Link", options: "User", default: frappe.session.user },
{ fieldname: "company", label: "Company", fieldtype: "Link", options: "Company" },
-
+
{
- fieldname: "doctype", label: "Document Type", fieldtype: "Link", options: "DocType", reqd: 1,
+ fieldname: "doctype", label: "Document Type", fieldtype: "Link", options: "DocType",
get_query: function() {
return {
query: "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.get_workflow_doctypes"
@@ -95,13 +96,17 @@ frappe.query_reports["Work Flow Approval"] = {
function load_actions() {
let doctype = frappe.query_report.get_filter_value("doctype");
- if (!doctype) return;
+ let method = doctype
+ ? "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.get_workflow_actions"
+ : "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.get_all_workflow_actions";
+ let args = doctype ? { doctype } : {};
+
frappe.call({
- method: "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.get_workflow_actions",
- args: { doctype },
+ method: method,
+ args: args,
callback: r => {
let $s = $("#wf-action-select").empty().append('');
(r.message || []).forEach(a => $s.append(``));
}
});
-}
+}
\ No newline at end of file
diff --git a/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.py b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.py
index 16ee980..8ab8dc3 100644
--- a/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.py
+++ b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.py
@@ -2,6 +2,8 @@
# # For license information, please see license.txt
import frappe
+
+
def execute(filters=None):
return get_columns(), get_data(filters)
@@ -18,11 +20,33 @@ def get_columns():
def get_data(filters):
- if not filters.get("doctype"):
- return []
+ filters = filters or {}
+
+ if filters.get("doctype"):
+ return get_doctype_data(filters.get("doctype"), filters)
+
+ workflows = frappe.db.get_all("Workflow",
+ filters={"is_active": 1},
+ fields=["document_type"]
+ )
+
+ all_data = []
+ seen = set()
+ for w in workflows:
+ dt = w.document_type
+ if not dt or dt in seen:
+ continue
+ seen.add(dt)
+ try:
+ all_data.extend(get_doctype_data(dt, filters))
+ except Exception:
+ continue
+
+ all_data.sort(key=lambda x: x.get("creation") or "", reverse=True)
+ return all_data
- doctype = filters.get("doctype")
+def get_doctype_data(doctype, filters):
if not frappe.db.has_column(doctype, "workflow_state"):
return []
@@ -41,7 +65,7 @@ def get_data(filters):
if filters.get("to_date"):
conditions += " AND creation <= %(to_date)s"
values["to_date"] = filters["to_date"]
-
+
fields = ("company, " if has_company else "") + "name, workflow_state, owner, creation"
data = frappe.db.sql(f"""
SELECT {fields} FROM `tab{doctype}`
@@ -72,6 +96,27 @@ def get_workflow_actions(doctype):
actions.append(t.action)
return actions
+
+@frappe.whitelist()
+def get_all_workflow_actions():
+ """Return the union of actions across all active workflows."""
+ workflows = frappe.db.get_all("Workflow",
+ filters={"is_active": 1},
+ fields=["name"]
+ )
+
+ seen, actions = set(), []
+ for w in workflows:
+ try:
+ for t in frappe.get_doc("Workflow", w.name).transitions:
+ if t.action and t.action not in seen:
+ seen.add(t.action)
+ actions.append(t.action)
+ except Exception:
+ continue
+ return actions
+
+
@frappe.whitelist()
def apply_bulk_workflow(docs, action):
import json
@@ -101,4 +146,81 @@ def get_workflow_doctypes(doctype, txt, searchfield, start, page_len, filters):
doctypes = [[w.document_type, w.document_type] for w in workflows
if w.document_type and txt.lower() in w.document_type.lower()]
- return doctypes
\ No newline at end of file
+ return doctypes
+
+
+@frappe.whitelist()
+def get_pending_approval_count(user=None):
+ """
+ Returns the count of pending workflow documents that the given user
+ (default: logged-in user) is permitted to approve.
+
+ A user is considered an approver if their role matches the
+ `allow_edit` role defined on the 'Pending' workflow state.
+ Administrator sees all pending documents.
+ """
+ user = user or frappe.session.user
+
+ if user == "Administrator":
+ return _count_all_pending()
+
+ user_roles = set(frappe.get_roles(user))
+
+ workflows = frappe.db.get_all("Workflow",
+ filters={"is_active": 1},
+ fields=["name", "document_type"]
+ )
+
+ total = 0
+ seen = set()
+ for w in workflows:
+ dt = w.document_type
+ if not dt or dt in seen:
+ continue
+ seen.add(dt)
+
+ if not frappe.db.has_column(dt, "workflow_state"):
+ continue
+
+ try:
+ wf_doc = frappe.get_cached_doc("Workflow", w.name)
+ except Exception:
+ continue
+
+ # Collect roles allowed to act on the Pending state
+ approver_roles = set()
+ for state in wf_doc.states:
+ if state.state == "Pending" and state.allow_edit:
+ approver_roles.add(state.allow_edit)
+
+ # Skip this doctype if the user has none of the approver roles
+ if approver_roles and not (user_roles & approver_roles):
+ continue
+
+ try:
+ total += frappe.db.count(dt, filters={"workflow_state": "Pending"}) or 0
+ except Exception:
+ continue
+
+ return total
+
+
+def _count_all_pending():
+ workflows = frappe.db.get_all("Workflow",
+ filters={"is_active": 1},
+ fields=["document_type"]
+ )
+ total = 0
+ seen = set()
+ for w in workflows:
+ dt = w.document_type
+ if not dt or dt in seen:
+ continue
+ seen.add(dt)
+ if not frappe.db.has_column(dt, "workflow_state"):
+ continue
+ try:
+ total += frappe.db.count(dt, filters={"workflow_state": "Pending"}) or 0
+ except Exception:
+ continue
+ return total
\ No newline at end of file