function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Jess coreJess core 

Update checkbox field if a record has an attachment

We would like to see which records have an attachment attached from the list view.  To do this, we created a checkbox field HasAttachment, default unchecked, and the following trigger.  If the Expense custom object record has an attachment, then the HasAttachment field should update to TRUE.  We tested this trigger through 'Upload File' under the Notes and Attachments related list, however, the HasAttachment field remains unchecked.
 
trigger AttachmentonExpense on Attachment (after insert) 
{
Set setParentId = new Set();
List Expenselst = new List();

for(Attachment att: Trigger.new)
{
setParentId.add(att.ParentId);
}

Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];

For(Expense__c e : Expenselst)
{
e.HasAttachment__c = True;
}

update Expenselst;
}

 
Best Answer chosen by Jess core
Andrew GAndrew G
no worries.

To be more precise: 
The expense claim would already exist as a Content Document and would be related via the Content Document Link.  If it appears in your Files & Notes related list in the Expense record, then it exists as a Content Document Link.  What we are doing is is writing a trigger on the Contect Document Link record to detect if it is related to an Expense record and then updating the check box on that record.

In this way, if someone later "links" the expense change to a content document, it will also mark the checkbox in your expense record.
note: this is an after Insert trigger, so it won't update the check box on Expense records with existing Content Documents (links).

Code fix:
trigger AttachmentonCDL on ContentDocumentLink (after insert) 
{ 
	String tempParentId;
	Set<Id> setParentId = new Set<Id>();
	List<Expense__c> Expenselst = new List<Expense__c>();
	
 for (ContentDocumentLink cdl : trigger.new ) {
			tempParentId = cdl.LinkedEntityId;
	 
			if (tempParentId.left(3) =='a0X') {
				System.debug('Debug : found a0X');
				System.debug('Debug : content document id ' + cdl.ContentDocumentId );
				setParentId.add(cdl.LinkedEntityId);
			}
		}
	Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
	 
	 For(Expense__c e : Expenselst)
	 {
		e.HasAttachment__c = True;
	 }

	 update Expenselst;
}
1. Ok, we missed declaring the tempParentId.
2. We had declared the Set setParentId but we were trying to update the list expenseIds which we hadn't declare - so updated the code to use the Set we declared.
3. The contentDocumentLinks was a list of CDLs because I prefer to write TriggerHandlers rather than write code direct in Triggers. I have changed contentDocumentLinks  to Trigger.new which will work directly in the trigger.

Give that a whirl

Regards
Andrew

 

All Answers

Syed Insha Jawaid 2Syed Insha Jawaid 2

Hi Jess

Are you using Salesforce Lightning?
If you are, then the trigger needs to be written on different object.
Files have a diferent object structure in Salesforce.

Please refer the link : https://developer.salesforce.com/docs/atlas.en-us.sfFieldRef.meta/sfFieldRef/salesforce_field_reference_ContentDocument.htm

Cheers!!!

Jess coreJess core
Hi Syed,

Thanks, we are using Lightning.

I have updated the code so that the trigger is on the Content Document instead of Attachment but it is still not working.
trigger AttachmentonCD on ContentDocument (after insert) 
{
    Set<Id> setParentId = new Set<Id>();
    List<Expense__c> Expenselst = new List<Expense__c>();
    
     for(ContentDocument att: Trigger.new)
     {
           setParentId.add(att.ParentId);
     }

     Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
     
     For(Expense__c e : Expenselst)
     {
        e.HasAttachment__c = True;
     }

     update Expenselst

 
Andrew GAndrew G
To confuse you some more, the trigger is not on the Content Document object.

You will need to do it on the ContentDocumentLink Object.
 
for (ContentDocumentLink cdl : contentDocumentLinks ) {
			tempParentId = cdl.LinkedEntityId;
			//0WO prefix for Work Order Records
                        //***update prefix to match you object type
			if (tempParentId.left(3) =='0WO') {
				System.debug('Debug : found 0WO');
				System.debug('Debug : content document id ' + cdl.ContentDocumentId );
				expenseIds.add(cdl.LinkedEntityId);
			}
		}

ContentDocument is the file, but ContentDocumentLink defines it's attachment to a record. We can also talk about ContentVersions but they aren't relevant here as you are only wanting to the tage the parent record.

The test for a prefix is so that the trigger doesn't try to update records that aren't an Expense Object. (and therefore don't have the checkbox).


HTH
Regards
Andrew



 
Andrew GAndrew G
So to add some clarity to the above, since the Schema doesnt show the objects, have a play in Developer Console
Here is a query of the Content Document table.  As you see, Parent id is empty.
Result from query on Content Document object
Then using the first record Id, I query the Content Document Link object.
Sample result from Content Document Link Table
By the highlighing, we see that this Content Document is related to a person record (prefix 005) and a case (prefix 500).
We can then check that by opening the content document and checking the Share With settings
User interface shot of the content document record showing shared records

Hope the extra info helps

Regards
Andrew

 
Jess coreJess core
Hi Andrew,

Thanks for explaining what ContentDocumentLink is.   So by adding your code the expense record will become a linked entity to the content document?

I replace part of the code with yours so I now have the following, but I get the "Variable does not exist" error for contentDocumentLinks, tempParentId and expenseIds.

Where and how do I define the variables?  Apologies, I am still new to apex.
trigger AttachmentonCDL on ContentDocumentLink (after insert) 
{ 
    Set<Id> setParentId = new Set<Id>();
    List<Expense__c> Expenselst = new List<Expense__c>();

    
 for (ContentDocumentLink cdl : contentDocumentLinks ) {
			tempParentId = cdl.LinkedEntityId;
     
			if (tempParentId.left(3) =='a0X') {
				System.debug('Debug : found a0X');
				System.debug('Debug : content document id ' + cdl.ContentDocumentId );
				expenseIds.add(cdl.LinkedEntityId);
			}
		}
    Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
     
     For(Expense__c e : Expenselst)
     {
        e.HasAttachment__c = True;
     }

     update Expenselst;
}



 
Andrew GAndrew G
no worries.

To be more precise: 
The expense claim would already exist as a Content Document and would be related via the Content Document Link.  If it appears in your Files & Notes related list in the Expense record, then it exists as a Content Document Link.  What we are doing is is writing a trigger on the Contect Document Link record to detect if it is related to an Expense record and then updating the check box on that record.

In this way, if someone later "links" the expense change to a content document, it will also mark the checkbox in your expense record.
note: this is an after Insert trigger, so it won't update the check box on Expense records with existing Content Documents (links).

Code fix:
trigger AttachmentonCDL on ContentDocumentLink (after insert) 
{ 
	String tempParentId;
	Set<Id> setParentId = new Set<Id>();
	List<Expense__c> Expenselst = new List<Expense__c>();
	
 for (ContentDocumentLink cdl : trigger.new ) {
			tempParentId = cdl.LinkedEntityId;
	 
			if (tempParentId.left(3) =='a0X') {
				System.debug('Debug : found a0X');
				System.debug('Debug : content document id ' + cdl.ContentDocumentId );
				setParentId.add(cdl.LinkedEntityId);
			}
		}
	Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
	 
	 For(Expense__c e : Expenselst)
	 {
		e.HasAttachment__c = True;
	 }

	 update Expenselst;
}
1. Ok, we missed declaring the tempParentId.
2. We had declared the Set setParentId but we were trying to update the list expenseIds which we hadn't declare - so updated the code to use the Set we declared.
3. The contentDocumentLinks was a list of CDLs because I prefer to write TriggerHandlers rather than write code direct in Triggers. I have changed contentDocumentLinks  to Trigger.new which will work directly in the trigger.

Give that a whirl

Regards
Andrew

 
This was selected as the best answer
Andrew GAndrew G
Edit: just noticed what i wrote so to clarify / simplify part of the explanation which is a little wrong in saying
"The expense claim would already exist as a Content Document"

The Expense record exists as its own object.
When you "attach" a file, the attached File becomes a Content Document record.  So if a person attaches a receipt, that is the Content Document.
The Content Document Link establishes the link between the Content Document and the record, in this case the Expense record.

Regards
Andrew
Jess coreJess core
Hi Andrew,

Amazing - it works!  Thanks so much for your help!

If I would like to modify the code so that any existing expense records that has an attachment will have HasAttachment = TRUE, then do I add a before update trigger in line 1?  Should I add anything else to the code?  

Same with 'after delete' - if I want HasAttachment = FALSE once the attachment is deleted, how can I modify the code?

Jess
Andrew GAndrew G
Hi Jess
Remember that it is a trigger.  The trigger will on fire on the event - in this case Insert of the ContentDocumentLink record.  

You could add the After Update to the Trigger in line one, and it will fire if the ContentDocumentLink is updated.  It won't magically update the expense records as there needs to be that Trigger event. 
To update the existing records, there are two options:
1.  Do a report or SQL query on the ContentDocumentLink, extracting the Expense Record Ids, and do a dataloader to update field directly.
2. Add the After Update to the trigger, extract all the contentdocumentLink records and do an update directly to the linking record.

The thing to remember is that the content document link in not generally updated in every day activities.  So adding the After Update may not bring a lot of results - remember that when viewing the Expense record, the Content Document Link record is somewhat invisible as it works as a joining record to the ContentDocument - which if you update, won't trigger the contentdocumentlink trigger.

The After Delete code would be different as you won't want to clear the field just because the documentLink is deleted as there may be other content documents inked to the expense record.  Something like this should do the trick.
 
if (trigger.isAfter && trigger.isDelete ) {
		for (ContentDocumentLink cdl : trigger.old ) {
			tempParentId = cdl.LinkedEntityId;
			if (tempParentId.left(3) =='a0X') {
				System.debug('Debug : found a0X');
				System.debug('Debug : content document id ' + cdl.ContentDocumentId );
				setParentId.add(cdl.LinkedEntityId);
			}
		}

		Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
	//	let's query to see if theere are other CDLs for the Expense records where we deleted a link
		List<ContentDocumentLink> cdlList - new List<ContentDocumentLink>();
		cdlList = [SELECT Id FROM ContentDocumentLink WHERE LinkedEntityId in :setParentId];
		List<String> cdlIds = new List<String>();
		for (ContentDocumentLink cdl : cdlList ) {
			cdlIds.add(cdl.Id);
		}
		For(Expense__c e : Expenselst) {
			if (cdlIds.contains(e.Id)) {
				e.HasAttachment__c = True;	
			} else {
				e.HasAttachment__c = False;	
			}
			
		}
		update Expenselst;
	}

**not tested in my environment, but the logic looks ok at first glance.

Psuedo code would be:
for the deleted CDLs, get a list of parent Ids if parent is Expense
get a list of Expenses that have had a CDL deleted
get a list of any other CDLs that have the same ParentId (expense record)
for the Expenses that had a CDL deleted, if the Id exists in the other CDL list, then other CDLs exist for that expense record.

The conversion from list of CDL to list of Strings is required because the Select returns a list of Objects, and we want to use an Id in the Contains method.

You will need the after delete in your first line of the trigger
And you will want to group the previous code in an if statement like 

if (trigger.isAfter && trigger.isInsert)




give that a whirl

Regards
Andrew

P.s.
after thought - if you have a situation where other code ensures that no more than one attachment per Expense record, then your first thoughts on simply looping the trigger.old and setting to False would work.
 

Jess coreJess core
Hi Andrew,

I added the After Delete code but it is not working - HasAttachment is still equals to TRUE.  The trigger works fine when inserting a file to the record.
 
trigger AttachmentonCDL on ContentDocumentLink (after insert, after delete) 
{
//Define variables 
    String tempParentId;
    Set<Id> setParentId = new Set<Id>();
    List<Expense__c> Expenselst = new List<Expense__c>();

     if(Trigger.isAfter && Trigger.isInsert) {
    for (ContentDocumentLink cdl : trigger.new ) {
            tempParentId = cdl.LinkedEntityId;
     
            if (tempParentId.left(3) =='a0X') {
                System.debug('Debug : found a0X');
                System.debug('Debug : content document id ' + cdl.ContentDocumentId );
                setParentId.add(cdl.LinkedEntityId);
            }
        }
    Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
     
     For(Expense__c e : Expenselst)
     {
        e.HasAttachment__c = True;
     }

     update Expenselst;
    }

            if (trigger.isAfter && trigger.isDelete ) {
        for (ContentDocumentLink cdl : trigger.old ) {
            tempParentId = cdl.LinkedEntityId;
            
            if (tempParentId.left(3) =='a0X') {
                System.debug('Debug : found a0X');
                System.debug('Debug : content document id ' + cdl.ContentDocumentId );
                setParentId.add(cdl.LinkedEntityId);
            }
        }

        Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
                
    //  let's query to see if there are other CDLs for the Expense record where we deleted a link
        List<ContentDocumentLink> cdlList = new List<ContentDocumentLink>();
        cdlList = [SELECT Id FROM ContentDocumentLink WHERE LinkedEntityId in :setParentId];
        List<String> cdlIds = new List<String>();
        for (ContentDocumentLink cdl : cdlList ) {
            cdlIds.add(cdl.Id);
        }
        For(Expense__c e : Expenselst) {
            if (cdlIds.contains(e.Id)) {
                e.HasAttachment__c = True;  
            } else {
                e.HasAttachment__c = False; 
            }
            
        }
        update Expenselst;
    }
}


 
Andrew GAndrew G
Hi Jess

I just did a check on the code.  I set the code up in my dev environment against the Case record.
It appears that when you delete from the File Related list, you are removing the actual Content Document, not the Content Document Link. Checking the forums, it seems this is the case.  This would be why the trigger for Delete does not fire. 
https://success.salesforce.com/answers?id=9063A000000eMnqQAE

The question becomes "Is the update on delete required?"  If so, then a trigger on Content Document delete would be required.
This would add some complexity to handle, as the process would be to detect the delete on the Content Document, find all related Content Document Links, (not sure if/how Versions would then come into play) then find all related Expense records, then using their ids query all Content Document Links and then recalculate the check box.

Apologies on my misunderstanding on how the Content Document Links are removed.

Regards
Andrew


 
Alejandro VollbrechthausenAlejandro Vollbrechthausen
Hi Jess,

Could you provide the tests you used to deploy this trigger to production, please?

regards,
Alejandro
Dinesh RajendranDinesh Rajendran
Hi Alejandro,

Does the Delete Functionality Works!! Could you share the test case for this trigger.

Thanks
Rushikesh Pawar 7Rushikesh Pawar 7
Dont Refer the values(a0X) above.
Simple if you just want any object's field(checkbox) to be true when files are uploaded then this the way.
Code:-
trigger AttachmentonCDL on ContentDocumentLink (after insert) 
{
    string tempParentId;
    Set<Id> setParentId = new Set<Id>();
    List<Expense__c> Expenselst = new List<Expense__c>();
 for (ContentDocumentLink cdl : trigger.new ) {
            tempParentId = cdl.LinkedEntityId;
     
            if (tempParentId != null) {
                System.debug('Debug : found Notnull');
                System.debug('Debug : content document id ' + cdl.ContentDocumentId );
                setParentId.add(cdl.LinkedEntityId);
            }
        }
Expenselst = [select Id , HasAttachment__c from Expense__c where Id IN :setParentId];
     
     For(Expense__c e : Expenselst)
     {
        e.HasAttachment__c = True;
     }

     update Expenselst;
}
Sam LauSam Lau
Hi All,

I'm trying to achieve something similiar where I need to update a record with the deleted attachment name and size.  I'm still getting use to APEX, and had come across a few examples to help me with the following trigger.   We're still utilizing the Attachment object as well.   Any assistance would be appreciated.   Currently I'm getting many errors on the SOQL query for TaskLst which I can't figure out at the moment.   Thanks!

Sam

trigger AttachmentHCLTask on Attachment (before delete) 

    String tempParentId;
    String tempAttName;
    Integer tempAttSize;
    Set<Id> setParentId = new Set<Id>();
    List<BMCServiceDesk__Task__c> Tasklst = new List<BMCServiceDesk__Task__c>();
    
 for (Attachment HCLTask : trigger.new ) {
            tempParentId = HCLTask.Id;
             tempAttName = HCLTask.Name;
             tempAttSize = HCLTask.BodyLength;
     
            if (tempParentId.left(10) =='a290H00000') {
                System.debug('Debug : found a290H00000');
                System.debug('Debug : Attachment id ' + HCLTask.Id );
                setParentId.add(HCLTask.Id);
            }
        }
    Tasklst = [select Id from BMCServiceDesk__Task__c where Id = setParentId AND HCL_Task_Creation_Sent__c = true];
     
     For(BMCServiceDesk__Task__c e : Tasklst)
     {
        e.HCL_Attachment_Remove_File_Name__c = tempAttName;
        e.HCL_Attachment_Remove_File_Size__c = tempAttSize;
     }

     update Tasklst;
}