You likely could write a validation rule to satisfy your conditions, but you might not be able to save that validation rule (because it'd likely go beyond the compiled character limit).
Validation rules aren't really meant to do what you're looking to do. Preventing certain fields from being edited is easy. The inverse of that, allowing certain fields to be edited, is hard.
A better declarative solution is to create new record types and page layouts, and mark the fields that you want to prevent from being edited as "read only" on those new layouts. You could use a workflow rule or process builder to automatically move your records into/out-of these special recordTypes.
Other than that, this would be a relatively simple trigger.
- Gather a list of all the field api names on your object
- Create a set of field api names you want to allow editing on (probably a
Map<String, Set<String>>
since you want different fields on different recordTypes)
- Iterate over
trigger.new
, and check the record against its companion in trigger.old
- If a field is changed and not in your "allowed" set, add an error
some example code to get you going
Trigger myTrigger on MyObject(before update){
// We can gather the field API names through SObject describe information
List<String> fieldAPINames = new List<String>();
// This is one way to initialize a map
Map<String, Set<String>> editableFieldsByRecordType = new Map<String, Set<String>>{
'RecType1' => new Set<String>{'field1__c', 'field2__c', 'field3__c'}
};
for(Schema.FieldDiscribeResult fdr :Schema.SObject.MyObject.fields.getMap().values()){
// The getName() method of the FieldDescribeResult class gives you the API name of the field
fieldAPINames.add(fdr.getName());
}
for(MyObject newRec :Trigger.new){
MyObject oldRec = Trigger.oldMap.get(newRec.Id);
for(String currentField :fieldAPINames){
if(oldRec.get(currentField) != newRec.get(currentField) && !editableFieldsByRecordType.get(newRec.recordTypeName__c).contains(currentField)){
newRec.addError('Field is not editable for this stage');
// After you find the first error, you may be able to move on to the next
// record.
// Comment the next line out if you want to catch _all_ attempted edits
break;
}
}
}
}
The big caveat here (besides me not testing for a particular stage) is that I assume you have a recordTypeName__c
field (a formula field) to pull the name of your recordType into the object you're working on.
Trigger context variables don't contain any "related data" (data on objects other than the one you're working on). If you need to use more than one period/dot/full-stop to access a field, like myRec.RecordType.Name
, that information is not available in trigger context variables.
To get that information, you either need to explicitly query for it, or have a formula field. There are other ways around that, but those are the easiest and most common approaches.