Skip to content

Assignment Rule Patterns

Problem: ScoopJoy needs to auto-assign quality inspection tasks, customer complaints, and franchise audit visits to the right people — without a manager hand-picking an owner for every record.

Solution: Use Frappe’s Assignment Rule DocType. Each rule watches a target DocType, evaluates an assign_condition on every save, and assigns matching documents to a user using one of four strategies: Round Robin, Load Balancing, Based on Field, or any combination expressed as a complex condition. The four patterns below all live in one setup script, run once with bench execute scoopjoy.scoopjoy.setup.assignment_rules.create_assignment_rules.

scoopjoy/scoopjoy/setup/assignment_rules.py
import frappe
def create_assignment_rules():
"""Create assignment rules for ScoopJoy operations.
Run: bench execute scoopjoy.scoopjoy.setup.assignment_rules.create_assignment_rules
"""
_create_qa_round_robin()
_create_complaint_load_balance()
_create_audit_field_based()
_create_complex_condition_rule()
frappe.db.commit()

Round Robin distributes inspections evenly: Frappe tracks last_user on the rule and cycles through the users list. The assignment_days list restricts assignment to the days the QA team works.

scoopjoy/scoopjoy/setup/assignment_rules.py
def _create_qa_round_robin():
"""Round Robin: Distribute QA inspections evenly across the QA team."""
if frappe.db.exists("Assignment Rule", "QA Inspection Round Robin"):
return
frappe.get_doc({
"doctype": "Assignment Rule",
"name": "QA Inspection Round Robin",
"document_type": "Quality Inspection",
"assign_condition": "doc.docstatus == 0 and doc.status == 'Draft'",
"priority": 0,
"disabled": 0,
# Rule type: Round Robin
"rule": "Round Robin",
# Days assignment is allowed
"assignment_days": [
{"day": "Monday"},
{"day": "Tuesday"},
{"day": "Wednesday"},
{"day": "Thursday"},
{"day": "Friday"},
{"day": "Saturday"},
],
# Users to rotate through
"users": [
{"user": "qa.inspector1@scoopjoy.com"},
{"user": "qa.inspector2@scoopjoy.com"},
{"user": "qa.inspector3@scoopjoy.com"},
],
# Due date: 2 days from creation
"due_date_based_on": "Creation Date",
"due_date_days": 2,
# Close rule: close assignment when inspection is submitted
"close_condition": "doc.docstatus == 1",
"description": "Assigns QA inspections to inspectors in round-robin order",
}).insert(ignore_permissions=True)

Pattern 2: Load Balancing — Customer Complaints

Section titled “Pattern 2: Load Balancing — Customer Complaints”

Load Balancing counts each user’s open ToDos and assigns to whoever has the fewest — ideal for support, where ticket volume per agent varies. The unassign_condition releases the assignment automatically if the issue type changes away from Customer Complaint.

scoopjoy/scoopjoy/setup/assignment_rules.py
def _create_complaint_load_balance():
"""Load Balancing: Assign complaints to the least-busy support agent."""
if frappe.db.exists("Assignment Rule", "Complaint Load Balancer"):
return
frappe.get_doc({
"doctype": "Assignment Rule",
"name": "Complaint Load Balancer",
"document_type": "Issue",
"assign_condition": (
"doc.status == 'Open' and "
"doc.issue_type == 'Customer Complaint'"
),
"priority": 0,
"disabled": 0,
# Rule type: Load Balancing (assigns to user with fewest open assignments)
"rule": "Load Balancing",
"users": [
{"user": "support.agent1@scoopjoy.com"},
{"user": "support.agent2@scoopjoy.com"},
{"user": "support.agent3@scoopjoy.com"},
{"user": "support.agent4@scoopjoy.com"},
],
# Due date: 1 day for complaints
"due_date_based_on": "Creation Date",
"due_date_days": 1,
# Close when resolved
"close_condition": "doc.status in ('Closed', 'Resolved')",
# Unassign when condition no longer matches (e.g., type changed)
"unassign_condition": "doc.issue_type != 'Customer Complaint'",
"description": "Assigns customer complaints to the support agent with fewest open tickets",
}).insert(ignore_permissions=True)

Pattern 3: Based on Field — Franchise Audit

Section titled “Pattern 3: Based on Field — Franchise Audit”

“Based on Field” reads a User link field from the document itself, so each outlet’s audit goes to its designated auditor. The field key names that link field (custom_designated_auditor), and priority 1 lets this rule out-rank the round-robin rule (priority 0) when both conditions match the same inspection.

scoopjoy/scoopjoy/setup/assignment_rules.py
def _create_audit_field_based():
"""Based on Field: Assign audit to the outlet's designated auditor."""
if frappe.db.exists("Assignment Rule", "Franchise Audit Auditor"):
return
frappe.get_doc({
"doctype": "Assignment Rule",
"name": "Franchise Audit Auditor",
"document_type": "Quality Inspection",
"assign_condition": (
"doc.inspection_type == 'Incoming' and "
"doc.custom_is_franchise_audit == 1"
),
"priority": 1, # Higher priority than the round-robin rule
"disabled": 0,
# Rule type: Based on Field
"rule": "Based on Field",
"field": "custom_designated_auditor", # Link field on Quality Inspection
# Due date: 7 days for audits
"due_date_based_on": "Creation Date",
"due_date_days": 7,
"close_condition": "doc.docstatus == 1",
"description": "Assigns franchise audits to the outlet's designated auditor",
}).insert(ignore_permissions=True)

Pattern 4: Complex Condition — Item Group AND Territory

Section titled “Pattern 4: Complex Condition — Item Group AND Territory”

When routing depends on several attributes, encode them in the assign_condition itself. Here each rule combines item group and warehouse zone so ice-cream inspections in the South zone go to the South QA team, and so on. These rules use the highest priority (2) because they are the most specific.

scoopjoy/scoopjoy/setup/assignment_rules.py
def _create_complex_condition_rule():
"""Complex condition: Assign based on item group AND territory.
Ice cream items in South zone -> south.qa@scoopjoy.com
Ice cream items in North zone -> north.qa@scoopjoy.com
"""
rules = [
{
"name": "QA South Zone Ice Cream",
"condition": (
"doc.item_group == 'Ice Cream' and "
"frappe.db.get_value('Warehouse', doc.warehouse, 'custom_zone') == 'South'"
),
"users": [{"user": "south.qa@scoopjoy.com"}],
},
{
"name": "QA North Zone Ice Cream",
"condition": (
"doc.item_group == 'Ice Cream' and "
"frappe.db.get_value('Warehouse', doc.warehouse, 'custom_zone') == 'North'"
),
"users": [{"user": "north.qa@scoopjoy.com"}],
},
{
"name": "QA South Zone Toppings",
"condition": (
"doc.item_group == 'Toppings' and "
"frappe.db.get_value('Warehouse', doc.warehouse, 'custom_zone') == 'South'"
),
"users": [{"user": "south.qa.toppings@scoopjoy.com"}],
},
{
"name": "QA North Zone Toppings",
"condition": (
"doc.item_group == 'Toppings' and "
"frappe.db.get_value('Warehouse', doc.warehouse, 'custom_zone') == 'North'"
),
"users": [{"user": "north.qa.toppings@scoopjoy.com"}],
},
]
for rule_def in rules:
if frappe.db.exists("Assignment Rule", rule_def["name"]):
continue
frappe.get_doc({
"doctype": "Assignment Rule",
"name": rule_def["name"],
"document_type": "Quality Inspection",
"assign_condition": rule_def["condition"],
"priority": 2, # Highest priority -- most specific
"disabled": 0,
"rule": "Round Robin",
"users": rule_def["users"],
"due_date_based_on": "Creation Date",
"due_date_days": 3,
"close_condition": "doc.docstatus == 1",
"description": f"Zone/item-specific QA assignment: {rule_def['name']}",
}).insert(ignore_permissions=True)