diff --git a/sf_trading/sf_trading/report/work_flow_approval/__init__.py b/sf_trading/sf_trading/report/work_flow_approval/__init__.py
new file mode 100644
index 0000000..e69de29
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
new file mode 100644
index 0000000..5176949
--- /dev/null
+++ b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.js
@@ -0,0 +1,107 @@
+
+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,
+ get_query: function() {
+ return {
+ query: "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.get_workflow_doctypes"
+ };
+ },
+ on_change: function() {
+ wf_action = "";
+ $("#wf-action-select").val("");
+ load_actions();
+ frappe.query_report.refresh();
+ }
+ },
+ { fieldname: "from_date", label: "From Date", fieldtype: "Date" },
+ { fieldname: "to_date", label: "To Date", fieldtype: "Date" }
+ ],
+
+ after_datatable_render: function(dt) {
+ $(dt.wrapper).find(".dt-cell--col-0").each(function(i) {
+ if (i === 0) return;
+ let $c = $(this).css({ "text-align": "center", "cursor": "pointer" });
+ if (!$c.find('input[type="checkbox"]').length)
+ $c.html('');
+ $c.off("click").on("click", function(e) {
+ if (!$(e.target).is("input"))
+ $c.find('input[type="checkbox"]').prop("checked", v => !v);
+ });
+ });
+ },
+
+ onload: function(report) {
+ setTimeout(function() {
+ if (report.page.page_form.length && !$("#wf-action-select").length) {
+ report.page.page_form.append(`
+
+
+
`);
+ $("#wf-action-select").on("change", function() { wf_action = $(this).val(); });
+ }
+ load_actions();
+ }, 800);
+
+ report.page.add_inner_button("Apply Workflow Action", function() {
+ let docs = [];
+
+ $(report.datatable.wrapper).find("input.row-check:checked").each(function() {
+ let idx = parseInt($(this).closest(".dt-row").attr("data-row-index"));
+ if (!isNaN(idx) && report.data[idx])
+ docs.push({ doctype: report.data[idx].doctype, name: report.data[idx].name });
+ });
+
+ if (!docs.length) {
+ $(report.datatable.wrapper).find(".dt-row").each(function(i) {
+ if ($(this).find("input.row-check").prop("checked") && report.data[i])
+ docs.push({ doctype: report.data[i].doctype, name: report.data[i].name });
+ });
+ }
+
+ if (!docs.length) return frappe.msgprint(__("Please select at least one document."));
+
+ let action = $("#wf-action-select").val() || wf_action;
+ if (!action) return frappe.msgprint(__("Please select a Workflow Action."));
+
+ frappe.confirm(
+ __("Apply {0} to {1} document(s)?", [action, docs.length]),
+ () => frappe.call({
+ method: "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.apply_bulk_workflow",
+ args: { docs: JSON.stringify(docs), action },
+ freeze: true,
+ freeze_message: __("Applying..."),
+ callback: r => {
+ if (!r.exc) {
+ frappe.msgprint({ title: __("Result"), message: r.message, indicator: "green" });
+ frappe.query_report.refresh();
+ }
+ }
+ })
+ );
+ });
+ }
+};
+
+function load_actions() {
+ let doctype = frappe.query_report.get_filter_value("doctype");
+ if (!doctype) return;
+ frappe.call({
+ method: "sf_trading.sf_trading.report.work_flow_approval.work_flow_approval.get_workflow_actions",
+ args: { doctype },
+ callback: r => {
+ let $s = $("#wf-action-select").empty().append('');
+ (r.message || []).forEach(a => $s.append(``));
+ }
+ });
+}
diff --git a/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.json b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.json
new file mode 100644
index 0000000..27872ab
--- /dev/null
+++ b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.json
@@ -0,0 +1,28 @@
+{
+ "add_total_row": 0,
+ "add_translate_data": 0,
+ "columns": [],
+ "creation": "2026-03-03 11:04:33.355621",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": null,
+ "modified": "2026-03-03 20:21:39.835341",
+ "modified_by": "Administrator",
+ "module": "Sf Trading",
+ "name": "Work Flow Approval",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Workflow",
+ "report_name": "Work Flow Approval",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ }
+ ],
+ "timeout": 0
+}
\ 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
new file mode 100644
index 0000000..16ee980
--- /dev/null
+++ b/sf_trading/sf_trading/report/work_flow_approval/work_flow_approval.py
@@ -0,0 +1,104 @@
+# # Copyright (c) 2026, enfono and contributors
+# # For license information, please see license.txt
+
+import frappe
+def execute(filters=None):
+ return get_columns(), get_data(filters)
+
+
+def get_columns():
+ return [
+ { "label": "User", "fieldname": "owner", "fieldtype": "Link", "options": "User", "width": 180 },
+ { "label": "Company", "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 150 },
+ { "label": "DocType", "fieldname": "doctype", "fieldtype": "Data", "width": 150 },
+ { "label": "Document", "fieldname": "name", "fieldtype": "Dynamic Link", "options": "doctype", "width": 200 },
+ { "label": "Status", "fieldname": "workflow_state", "fieldtype": "Data", "width": 150 },
+ { "label": "Created Date", "fieldname": "creation", "fieldtype": "Datetime", "width": 180 }
+ ]
+
+
+def get_data(filters):
+ if not filters.get("doctype"):
+ return []
+
+ doctype = filters.get("doctype")
+
+ if not frappe.db.has_column(doctype, "workflow_state"):
+ return []
+
+ has_company = frappe.db.has_column(doctype, "company")
+ conditions = "workflow_state = 'Pending'"
+ values = {}
+
+ if filters.get("company") and has_company:
+ conditions += " AND company = %(company)s"
+ values["company"] = filters["company"]
+
+ if filters.get("from_date"):
+ conditions += " AND creation >= %(from_date)s"
+ values["from_date"] = filters["from_date"]
+
+ 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}`
+ WHERE {conditions} ORDER BY creation DESC
+ """, values, as_dict=True)
+
+ for d in data:
+ d["doctype"] = doctype
+ if not has_company:
+ d["company"] = ""
+
+ return data
+
+
+@frappe.whitelist()
+def get_workflow_actions(doctype):
+ wf = frappe.db.get_all("Workflow",
+ filters={"document_type": doctype, "is_active": 1},
+ fields=["name"], limit=1)
+
+ if not wf:
+ return []
+
+ seen, actions = set(), []
+ for t in frappe.get_doc("Workflow", wf[0].name).transitions:
+ if t.action and t.action not in seen:
+ seen.add(t.action)
+ actions.append(t.action)
+ return actions
+
+@frappe.whitelist()
+def apply_bulk_workflow(docs, action):
+ import json
+ from frappe.model.workflow import apply_workflow
+
+ results = []
+ for d in json.loads(docs):
+ try:
+ apply_workflow(frappe.get_doc(d["doctype"], d["name"]), action)
+ results.append(f" {d['name']} — Success")
+ except Exception as e:
+ results.append(f" {d['name']} — {str(e)}")
+
+ frappe.db.commit()
+ return "
".join(results)
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_workflow_doctypes(doctype, txt, searchfield, start, page_len, filters):
+ workflows = frappe.db.get_all("Workflow",
+ filters={"is_active": 1},
+ fields=["document_type"],
+ limit=100
+ )
+
+ 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