feat(assignment_rule): add weighted distribution strategy (#37741)

This commit is contained in:
Nabin Hait 2026-03-05 14:31:03 +05:30 committed by GitHub
parent b6ee391bd2
commit 115d3cead0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 111 additions and 29 deletions

View file

@ -26,7 +26,10 @@
"rule",
"field",
"users",
"last_user"
"weighted_users",
"column_break_mkgo",
"last_user",
"current_index"
],
"fields": [
{
@ -96,15 +99,15 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Rule",
"options": "Round Robin\nLoad Balancing\nBased on Field",
"options": "Round Robin\nLoad Balancing\nBased on Field\nWeighted Distribution",
"reqd": 1
},
{
"depends_on": "eval: doc.rule !== 'Based on Field'",
"depends_on": "eval:in_list(['Round Robin', \"Load Balancing\"], doc.rule)",
"fieldname": "users",
"fieldtype": "Table MultiSelect",
"label": "Users",
"mandatory_depends_on": "eval: doc.rule !== 'Based on Field'",
"mandatory_depends_on": "eval:in_list(['Round Robin', \"Load Balancing\"], doc.rule)",
"options": "Assignment Rule User"
},
{
@ -150,12 +153,31 @@
"fieldtype": "Select",
"label": "Field",
"mandatory_depends_on": "eval: doc.rule == 'Based on Field'"
},
{
"depends_on": "eval:doc.rule=='Weighted Distribution'",
"fieldname": "current_index",
"fieldtype": "Int",
"label": "Current Index",
"read_only": 1
},
{
"fieldname": "column_break_mkgo",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.rule=='Weighted Distribution'",
"fieldname": "weighted_users",
"fieldtype": "Table",
"label": "Users",
"mandatory_depends_on": "eval:doc.rule=='Weighted Distribution'",
"options": "Assignment Rule User"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-25 17:09:11.644603",
"modified": "2026-03-03 13:08:12.561504",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",

View file

@ -20,14 +20,13 @@ class AssignmentRule(Document):
if TYPE_CHECKING:
from frappe.automation.doctype.assignment_rule_day.assignment_rule_day import AssignmentRuleDay
from frappe.automation.doctype.assignment_rule_user.assignment_rule_user import (
AssignmentRuleUser,
)
from frappe.automation.doctype.assignment_rule_user.assignment_rule_user import AssignmentRuleUser
from frappe.types import DF
assign_condition: DF.Code
assignment_days: DF.Table[AssignmentRuleDay]
close_condition: DF.Code | None
current_index: DF.Int
description: DF.SmallText
disabled: DF.Check
document_type: DF.Link
@ -35,9 +34,10 @@ class AssignmentRule(Document):
field: DF.Literal[None]
last_user: DF.Link | None
priority: DF.Int
rule: DF.Literal["Round Robin", "Load Balancing", "Based on Field"]
rule: DF.Literal["Round Robin", "Load Balancing", "Based on Field", "Weighted Distribution"]
unassign_condition: DF.Code | None
users: DF.TableMultiSelect[AssignmentRuleUser]
weighted_users: DF.Table[AssignmentRuleUser]
# end: auto-generated types
def validate(self):
@ -80,25 +80,25 @@ class AssignmentRule(Document):
user = self.get_user(doc)
if user:
assign_to.add(
dict(
assign_to=[user],
doctype=doc.get("doctype"),
name=doc.get("name"),
description=frappe.render_template(self.description, doc),
assignment_rule=self.name,
notify=True,
date=doc.get(self.due_date_based_on) if self.due_date_based_on else None,
),
ignore_permissions=True,
)
if not user or not frappe.db.exists("User", user):
return False
# set for reference in round robin
self.db_set("last_user", user)
return True
assign_to.add(
dict(
assign_to=[user],
doctype=doc.get("doctype"),
name=doc.get("name"),
description=frappe.render_template(self.description, doc),
assignment_rule=self.name,
notify=True,
date=doc.get(self.due_date_based_on) if self.due_date_based_on else None,
),
ignore_permissions=True,
)
return False
# set for reference in round robin
self.db_set("last_user", user)
return True
def clear_assignment(self, doc):
"""Clear assignments"""
@ -122,6 +122,8 @@ class AssignmentRule(Document):
return self.get_user_load_balancing()
elif self.rule == "Based on Field":
return self.get_user_based_on_field(doc)
elif self.rule == "Weighted Distribution":
return self.get_weighted_user()
def get_user_round_robin(self):
"""
@ -167,6 +169,33 @@ class AssignmentRule(Document):
if frappe.db.exists("User", val):
return val
def get_weighted_user(self):
"""
Assign to the user based on weights assigned to users
Each rule maintains its own counter.
"""
users = [(d.user, d.weight or 1) for d in self.weighted_users if d.user]
if not users:
return None
total_weight = sum(weight for _, weight in users)
if total_weight <= 0:
return None
current_index = (
frappe.db.get_value("Assignment Rule", self.name, "current_index", for_update=True) or 0
)
slot = current_index % total_weight
cumulative_weight = 0
for user, weight in users:
cumulative_weight += weight
if slot < cumulative_weight:
frappe.db.set_value(
"Assignment Rule", self.name, "current_index", current_index + 1, update_modified=False
)
return user
def safe_eval(self, fieldname, doc):
try:
if self.get(fieldname):

View file

@ -113,6 +113,26 @@ class TestAutoAssign(IntegrationTestCase):
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type=TEST_DOCTYPE))), 10
)
def test_weighted_distribution(self):
self.assignment_rule.rule = "Weighted Distribution"
self.assignment_rule.weighted_users.clear()
self.assignment_rule.append("weighted_users", dict(user="test@example.com", weight=1))
self.assignment_rule.append("weighted_users", dict(user="test1@example.com", weight=2))
self.assignment_rule.save()
for _ in range(5):
_make_test_record(public=1)
# check if users are assigned based on weights (out of 5,
# test@example.com should have 2 assignments and test1@example.com should have 3 assignments )
self.assertEqual(
len(frappe.get_all("ToDo", dict(allocated_to="test@example.com", reference_type=TEST_DOCTYPE))), 2
)
self.assertEqual(
len(frappe.get_all("ToDo", dict(allocated_to="test1@example.com", reference_type=TEST_DOCTYPE))),
3,
)
def test_assingment_on_guest_submissions(self):
"""Sometimes documents are inserted as guest, check if assignment rules run on them. Use case: Web Forms"""
with self.set_user("Guest"):

View file

@ -5,7 +5,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user"
"user",
"weight"
],
"fields": [
{
@ -15,20 +16,29 @@
"label": "User",
"options": "User",
"reqd": 1
},
{
"default": "1",
"fieldname": "weight",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Weight"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-23 16:01:27.847608",
"modified": "2026-03-03 12:30:01.394107",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule User",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -18,6 +18,7 @@ class AssignmentRuleUser(Document):
parentfield: DF.Data
parenttype: DF.Data
user: DF.Link
weight: DF.Int
# end: auto-generated types
pass