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.
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()Pattern 1: Round Robin — QA Inspections
Section titled “Pattern 1: Round Robin — QA Inspections”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.
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.
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.
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.
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)