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",
|
"rule",
|
||||||
"field",
|
"field",
|
||||||
"users",
|
"users",
|
||||||
"last_user"
|
"weighted_users",
|
||||||
|
"column_break_mkgo",
|
||||||
|
"last_user",
|
||||||
|
"current_index"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
|
@ -96,15 +99,15 @@
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rule",
|
"label": "Rule",
|
||||||
"options": "Round Robin\nLoad Balancing\nBased on Field",
|
"options": "Round Robin\nLoad Balancing\nBased on Field\nWeighted Distribution",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.rule !== 'Based on Field'",
|
"depends_on": "eval:in_list(['Round Robin', \"Load Balancing\"], doc.rule)",
|
||||||
"fieldname": "users",
|
"fieldname": "users",
|
||||||
"fieldtype": "Table MultiSelect",
|
"fieldtype": "Table MultiSelect",
|
||||||
"label": "Users",
|
"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"
|
"options": "Assignment Rule User"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -150,12 +153,31 @@
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Field",
|
"label": "Field",
|
||||||
"mandatory_depends_on": "eval: doc.rule == 'Based on 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,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-25 17:09:11.644603",
|
"modified": "2026-03-03 13:08:12.561504",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Automation",
|
"module": "Automation",
|
||||||
"name": "Assignment Rule",
|
"name": "Assignment Rule",
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,13 @@ class AssignmentRule(Document):
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.automation.doctype.assignment_rule_day.assignment_rule_day import AssignmentRuleDay
|
from frappe.automation.doctype.assignment_rule_day.assignment_rule_day import AssignmentRuleDay
|
||||||
from frappe.automation.doctype.assignment_rule_user.assignment_rule_user import (
|
from frappe.automation.doctype.assignment_rule_user.assignment_rule_user import AssignmentRuleUser
|
||||||
AssignmentRuleUser,
|
|
||||||
)
|
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
assign_condition: DF.Code
|
assign_condition: DF.Code
|
||||||
assignment_days: DF.Table[AssignmentRuleDay]
|
assignment_days: DF.Table[AssignmentRuleDay]
|
||||||
close_condition: DF.Code | None
|
close_condition: DF.Code | None
|
||||||
|
current_index: DF.Int
|
||||||
description: DF.SmallText
|
description: DF.SmallText
|
||||||
disabled: DF.Check
|
disabled: DF.Check
|
||||||
document_type: DF.Link
|
document_type: DF.Link
|
||||||
|
|
@ -35,9 +34,10 @@ class AssignmentRule(Document):
|
||||||
field: DF.Literal[None]
|
field: DF.Literal[None]
|
||||||
last_user: DF.Link | None
|
last_user: DF.Link | None
|
||||||
priority: DF.Int
|
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
|
unassign_condition: DF.Code | None
|
||||||
users: DF.TableMultiSelect[AssignmentRuleUser]
|
users: DF.TableMultiSelect[AssignmentRuleUser]
|
||||||
|
weighted_users: DF.Table[AssignmentRuleUser]
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
|
@ -80,25 +80,25 @@ class AssignmentRule(Document):
|
||||||
|
|
||||||
user = self.get_user(doc)
|
user = self.get_user(doc)
|
||||||
|
|
||||||
if user:
|
if not user or not frappe.db.exists("User", user):
|
||||||
assign_to.add(
|
return False
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# set for reference in round robin
|
assign_to.add(
|
||||||
self.db_set("last_user", user)
|
dict(
|
||||||
return True
|
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):
|
def clear_assignment(self, doc):
|
||||||
"""Clear assignments"""
|
"""Clear assignments"""
|
||||||
|
|
@ -122,6 +122,8 @@ class AssignmentRule(Document):
|
||||||
return self.get_user_load_balancing()
|
return self.get_user_load_balancing()
|
||||||
elif self.rule == "Based on Field":
|
elif self.rule == "Based on Field":
|
||||||
return self.get_user_based_on_field(doc)
|
return self.get_user_based_on_field(doc)
|
||||||
|
elif self.rule == "Weighted Distribution":
|
||||||
|
return self.get_weighted_user()
|
||||||
|
|
||||||
def get_user_round_robin(self):
|
def get_user_round_robin(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -167,6 +169,33 @@ class AssignmentRule(Document):
|
||||||
if frappe.db.exists("User", val):
|
if frappe.db.exists("User", val):
|
||||||
return 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):
|
def safe_eval(self, fieldname, doc):
|
||||||
try:
|
try:
|
||||||
if self.get(fieldname):
|
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
|
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):
|
def test_assingment_on_guest_submissions(self):
|
||||||
"""Sometimes documents are inserted as guest, check if assignment rules run on them. Use case: Web Forms"""
|
"""Sometimes documents are inserted as guest, check if assignment rules run on them. Use case: Web Forms"""
|
||||||
with self.set_user("Guest"):
|
with self.set_user("Guest"):
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"user"
|
"user",
|
||||||
|
"weight"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
|
@ -15,20 +16,29 @@
|
||||||
"label": "User",
|
"label": "User",
|
||||||
"options": "User",
|
"options": "User",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "weight",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Weight"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-23 16:01:27.847608",
|
"modified": "2026-03-03 12:30:01.394107",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Automation",
|
"module": "Automation",
|
||||||
"name": "Assignment Rule User",
|
"name": "Assignment Rule User",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class AssignmentRuleUser(Document):
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
user: DF.Link
|
user: DF.Link
|
||||||
|
weight: DF.Int
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue