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