diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js
index 6c31cfe3fc..00f2e66e08 100644
--- a/frappe/website/doctype/web_form/web_form.js
+++ b/frappe/website/doctype/web_form/web_form.js
@@ -46,6 +46,7 @@ frappe.ui.form.on("Web Form", {
frm.trigger("add_get_fields_button");
frm.trigger("add_publish_button");
frm.trigger("render_condition_table");
+ frm.trigger("render_dynamic_filters_table");
},
login_required: function (frm) {
@@ -203,9 +204,15 @@ frappe.ui.form.on("Web Form", {
},
before_save: function (frm) {
+ let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || "null");
let static_filters = JSON.parse(frm.doc.condition_json || "[]");
+ static_filters = frappe.dashboard_utils.remove_common_static_filter_values(
+ static_filters,
+ dynamic_filters
+ );
frm.set_value("condition_json", JSON.stringify(static_filters));
frm.trigger("render_condition_table");
+ frm.trigger("render_dynamic_filters_table");
},
render_condition_table: function (frm) {
@@ -308,6 +315,106 @@ frappe.ui.form.on("Web Form", {
dialog.set_values(filters);
});
},
+ render_dynamic_filters_table(frm) {
+ let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty();
+
+ frm.dynamic_filter_table = $(`
+
+
+ | ${__("Filter")} |
+ ${__("Condition")} |
+ ${__("Value")} |
+
+
+
+
`).appendTo(wrapper);
+
+ frm.dynamic_filters =
+ frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
+
+ frm.trigger("set_dynamic_filters_in_table");
+
+ let filters = JSON.parse(frm.doc.condition_json || "[]");
+
+ let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
+ true,
+ filters,
+ frm.dynamic_filters
+ );
+
+ // Override description to show Python expressions (evaluated server-side)
+ let desc_field = fields.find((f) => f.fieldname === "description");
+ if (desc_field) {
+ desc_field.options = `
+
${__("Set dynamic filter values as Python expressions.")}
+
${__("For example:")}
+ frappe.session.user ${__("or")}
+ frappe.utils.now()
+
+
`;
+ }
+
+ frm.dynamic_filter_table.on("click", () => {
+ if (!frm.has_perm("write")) {
+ return;
+ }
+
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frappe.throw(__("Cannot edit filters for standard Web Forms"));
+ }
+ let dialog = new frappe.ui.Dialog({
+ title: __("Set Dynamic Filters"),
+ fields: fields,
+ primary_action: () => {
+ let values = dialog.get_values();
+ dialog.hide();
+ let dynamic_filters = [];
+ for (let key of Object.keys(values)) {
+ let [doctype, fieldname] = key.split(":");
+ dynamic_filters.push([doctype, fieldname, "=", values[key]]);
+ }
+ frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters));
+ frm.trigger("set_dynamic_filters_in_table");
+ },
+ primary_action_label: __("Set"),
+ });
+
+ dialog.show();
+ if (frm.dynamic_filters) {
+ let filter_values = {};
+ frm.dynamic_filters.forEach((f) => {
+ filter_values[f[0] + ":" + f[1]] = f[3];
+ });
+ dialog.set_values(filter_values);
+ }
+ });
+ },
+ set_dynamic_filters_in_table: function (frm) {
+ frm.dynamic_filters =
+ frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
+
+ if (!frm.dynamic_filters) {
+ const filter_row = $(`|
+ ${__("Click to Set Dynamic Filters")} |
`);
+ frm.dynamic_filter_table.find("tbody").html(filter_row);
+ } else {
+ let filter_rows = "";
+ frm.dynamic_filters.forEach((filter) => {
+ filter_rows += `
+ | ${filter[1]} |
+ ${filter[2] || ""} |
+ ${filter[3]} |
+
`;
+ });
+ frm.dynamic_filter_table.find("tbody").html(filter_rows);
+ }
+ },
});
frappe.ui.form.on("Web Form List Column", {
diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json
index 6b03a6b988..c2e291bdfc 100644
--- a/frappe/website/doctype/web_form/web_form.json
+++ b/frappe/website/doctype/web_form/web_form.json
@@ -39,6 +39,8 @@
"condition_section",
"condition_description",
"condition_json",
+ "dynamic_filters_section",
+ "dynamic_filters_json",
"section_break_3",
"list_setting_message",
"show_list",
@@ -424,12 +426,22 @@
{
"fieldname": "column_break_hhec",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "dynamic_filters_json",
+ "fieldtype": "JSON",
+ "label": "Dynamic Filters JSON"
+ },
+ {
+ "fieldname": "dynamic_filters_section",
+ "fieldtype": "Section Break",
+ "label": "Dynamic Filters"
}
],
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
- "modified": "2025-12-12 16:29:35.806107",
+ "modified": "2026-02-13 19:59:19.807594",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index a42934d795..f42c39322d 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -46,6 +46,7 @@ class WebForm(WebsiteGenerator):
condition_json: DF.JSON | None
custom_css: DF.Code | None
doc_type: DF.Link
+ dynamic_filters_json: DF.JSON | None
hide_footer: DF.Check
hide_navbar: DF.Check
introduction_text: DF.TextEditor | None
diff --git a/frappe/www/list.py b/frappe/www/list.py
index d912efe9fe..b407349f54 100644
--- a/frappe/www/list.py
+++ b/frappe/www/list.py
@@ -45,6 +45,11 @@ def get_list_data(
if list_context.filters:
filters.update(list_context.filters)
+ if web_form_name:
+ dynamic_filters = get_dynamic_filters(web_form_name)
+ if dynamic_filters:
+ filters.update(dynamic_filters)
+
_get_list = list_context.get_list or get_list
kwargs = dict(
@@ -194,3 +199,37 @@ def get_list(
order_by=order_by,
distinct=distinct,
)
+
+
+def get_dynamic_filters(web_form_name):
+ """Evaluate dynamic filter expressions from Web Form.
+ Uses same safe_eval + get_workflow_safe_globals pattern as Workflow."""
+ from frappe.model.workflow import get_workflow_safe_globals
+
+ web_form = frappe.get_cached_doc("Web Form", web_form_name)
+
+ if not web_form.dynamic_filters_json:
+ return None
+
+ dynamic_filters = json.loads(web_form.dynamic_filters_json)
+ if not dynamic_filters:
+ return None
+
+ safe_globals = get_workflow_safe_globals()
+ safe_globals["frappe"]["defaults"] = frappe._dict(
+ get_user_default=frappe.defaults.get_user_default,
+ get_global_default=frappe.defaults.get_global_default,
+ )
+
+ evaluated = {}
+ for f in dynamic_filters:
+ try:
+ value = frappe.safe_eval(f[3], safe_globals)
+ evaluated[f[1]] = [f[2], value]
+ except Exception as e:
+ frappe.throw(
+ _("Invalid expression in Web Form Dynamic Filter for {0}: {1}").format(f[1], e),
+ title=_("Dynamic Filter Error"),
+ )
+
+ return evaluated