feat(assignment_rule): add weighted distribution strategy (#37741)
This commit is contained in:
parent
b6ee391bd2
commit
115d3cead0
5 changed files with 111 additions and 29 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class AssignmentRuleUser(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
user: DF.Link
|
||||
weight: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue